How Rulebook.Generate decides the action of one id in one endpoint, how it names the input that decided it, and how the effective diff shows a change in those terms. Every example below runs against the test fixture tests/fixtures/repos/valid-minimal and is asserted by tests/Rulebook.Generate.Tests.ps1.
Status: written by WP03 (#5). The contract is rulebook/composition.md section 3; this page works through it. Decisions D19, D22, D23, D27 and D41 in adr/.
- What the effective diff is
- Provenance tokens
- The fixture
- Precedence worked through
- Diff examples
- Reading the output
An endpoint is sparse: it lists only the ids whose effective action differs from the analyzer default (D22). A change to one input (an override, a level file, the catalog) can change many endpoints, and a changed catalog default can change what the compiler does without changing a single line in rulesets/. A JSON diff of rulesets/ shows neither well. The effective diff compares, per endpoint and id, the effective action before and after, with the input that decided it on each side.
Compare-RulebookEndpoints -RepositoryRoot <path> -Ref <git ref> computes it between a git ref (before) and the working tree (after). The module takes the ref it is given and throws when the ref does not resolve. Choosing the ref is the Validate action's contract: the pull request's base branch on a pull request (fetched when absent); on a push, the commit before the push from the event payload, else the last commit (HEAD~1), and "no diff" in the job summary, never a failure, when the ref does not resolve (ARCHITECTURE.md sections 5.3, 5.5 and 7.1). Later workflows that open pull requests (scan, ChangeRule, update) can reuse it for their bodies.
Get-EffectiveAction returns Id, Action, Source, Detail, Default and Listed. Source is one of six tokens, in precedence order:
| Token | The action came from | Detail |
|---|---|---|
override |
The matching overrides.json entry with the most non-wildcard selectors; the later entry on a tie. |
The entry's justification. |
twins |
The twins setting: appsource sets the PerTenantExtensionCop side of every pair in base/twins.json to None, pte the AppSourceCop side. |
The pair's title. |
stage:<slug> |
stages/<slug>.json, applied because the level result is not None (S-4). |
The stage entry's justification. |
level:<slug> |
The last file on the level chain that mentions the id; <slug> names that file, which may be a level the endpoint is based on. |
The level entry's justification. |
quarantine |
quarantine.<stage>.json, for an id no file on the chain mentions. |
The quarantine entry's justification. |
default |
Nothing else: the analyzer default from catalog/diagnostics.json. |
none |
Default is the analyzer default (defaultSeverity when enabledByDefault, else None; $null for an id the catalog does not know). Listed is true when the endpoint file lists the id: the action differs from the default, and an unknown default never equals anything. The dashboard export uses the same tokens (dashboard.md section 5).
The precedence, in the two-step form of composition.md section 3:
levelResult = chain where a file mentions the id -> level:<slug>
else None if the stage quarantines it -> quarantine
else the analyzer default -> default
effective = matching override -> override
else None for the losing twin side -> twins
else the stage entry, if levelResult is not None (S-4, D41) -> stage:<slug>
else levelResult
valid-minimal ships the four levels Essential, Recommended (based on Essential), Strict (based on Recommended) and Complete (based on Strict), the stages default, CI and vNext, twins: both, and a 30-id catalog. The inputs used below:
| File | Entries used here |
|---|---|
base/essential.ruleset.json |
AL0200, AL0432, AA0001, AA0072, AS0084, LC0015, LC0029, LC0089i, DC0001 at None |
base/recommended.ruleset.json |
AL0200, AL0432, AA0001, AA0072, LC0029, DC0001 Warning; AS0084 Error; LC0015, LC0089i Info; AC0001 Warning; AW0006 Error |
base/strict.ruleset.json |
AL0603 Info, AA0137 Error, LC0015 Warning, FC0001 Info, CM0001 Info |
stages/ci.json |
AL0432, AL0603, AL1026 Info |
overrides.json |
AA0072 Info for ["*"]/["*"] ("House style"); LC0029 None for ["recommended"]/["ci"] ("Backlog DEV-1234") |
quarantine.ci.json, quarantine.default.json |
LC0099 |
| catalog defaults | AL0200, AL0432, AL0603, AL1026, AA0072, AW0006, LC0001, LC0029, LC0099 Warning; AC0001, LC0015 Info; LC0054 disabled (None) |
Every candidate id (chain ids, stage ids, override ids, twin sides, the stage's quarantine ids) with its result:
| Id | Effective | Source | Default | Listed | Why |
|---|---|---|---|---|---|
| AA0072 | Info | override |
Warning | yes | The ["*"] override beats the chain's Warning. |
| LC0029 | None | override |
Warning | yes | The override names recommended and ci. In strict.ci it does not match (an override matches only the endpoint's own level slug, never levels based on it). |
| AL0432 | Info | stage:ci |
Warning | yes | Chain Warning is not None, so the stage entry applies. |
| AL0603 | Info | stage:ci |
Warning | yes | No chain file mentions it; the level result is the default Warning, so the stage entry applies. |
| AL1026 | Info | stage:ci |
Warning | yes | The same: a stage entry on an id enabled by default. |
| AW0006 | Error | level:recommended |
Warning | yes | The chain raises it. |
| AC0001 | Warning | level:recommended |
Info | yes | The chain raises it. |
| AL0200 | Warning | level:recommended |
Warning | no | Recommended's delta replaces Essential's None with the default; nothing to write. |
| LC0099 | None | quarantine |
Warning | yes | No chain file mentions it and quarantine.ci.json lists it. |
| PTE0003 | Error | default |
Error | no | With twins: both the twin sides run at their default. |
The endpoint file lists the rows with "yes", sorted by id (prefix order AL, AA, AW, PTE, AS, PC, AC, LC, DC, FC, TA, CM, then number, then the i suffix):
{
"name": "Rulebook Recommended / CI",
"description": "Level recommended, stage ci, twins both. Generated from base/essential.ruleset.json, base/recommended.ruleset.json plus stages/ci.json, overrides.json and quarantine.ci.json; do not edit. Ids at their analyzer default are not listed.",
"rules": [
{ "id": "AL0432", "action": "Info" },
{ "id": "AL0603", "action": "Info" },
{ "id": "AL1026", "action": "Info" },
{ "id": "AA0072", "action": "Info" },
{ "id": "AW0006", "action": "Error" },
{ "id": "AC0001", "action": "Warning" },
{ "id": "LC0029", "action": "None" },
{ "id": "LC0099", "action": "None" }
]
}Essential sets AL0432 to None. stages/ci.json says Info, but a stage never activates a rule: the level result is None, so the stage entry is skipped and AL0432 stays None with level:essential. AL0603 and AL1026, which Essential does not mention, still get Info from the stage because their level result is the enabled default.
The fixture quarantined-stage-entry adds { "id": "LC0099", "action": "Info" } to stages/ci.json. LC0099 is in quarantine.ci.json and no level file mentions it. Its level result is None (quarantine), so the stage entry does not apply and LC0099 stays None in every *.ci endpoint; the endpoints are byte-identical to valid-minimal. Validation reports the stage entry as C15, a warning: it is dead until a level file adopts LC0099, after which the chain wins over quarantine and the stage entry applies to the chain's action.
The generator decides quarantine per endpoint, from that level's chain. So the same stage entry can be dead in one level (no file on its chain mentions the id) and live in another (a file on its chain does). C15 fires only when the entry is dead in every published level.
For one id and endpoint, every matching override entry is a candidate. The one with more non-wildcard selectors wins (["recommended"]/["*"] beats ["*"]/["*"], ["recommended"]/["ci"] beats both), whatever the order in the file. Between equally specific entries the later one wins. An override whose action equals the analyzer default is valid and unlists the id: AA0072 Warning for ["essential"] gives Warning (override), equal to the default, so essential.* stop listing it.
With twins: appsource (fixture twins-appsource) both PTE sides become None in all 12 endpoints, with twins as source and the pair title as detail; the AS sides stay at their default and are not listed. In recommended.ci:
| Id | Effective | Source | Detail |
|---|---|---|---|
| PTE0003 | None | twins |
Procedures must not subscribe to CompanyOpen events |
| PTE0011 | None | twins |
The publisher name is too long |
An override on a twin side beats the setting: PTE0003 Warning for ["*"]/["*"] gives Warning (override).
Each example commits valid-minimal, changes one input in the working tree and runs Compare-RulebookEndpoints -Ref HEAD. The text column is each row's Text property.
| Change | Rows | Example row |
|---|---|---|
The LC0029 override goes from None to Info |
1, recommended.ci, action |
LC0029: None (override, "Backlog DEV-1234") -> Info (override, "Backlog DEV-1234") |
| The LC0029 override is removed | 1, recommended.ci, action |
LC0029: None (override, "Backlog DEV-1234") -> Warning (level:recommended) |
| LC0001's catalog default goes from Warning to Info; no file mentions LC0001 | 12, one per endpoint, action |
LC0001: Warning (default) -> Info (default) |
| AW0006's catalog default goes from Warning to Error, the action Recommended already sets | 9 listing rows (recommended.*, strict.*, complete.*), 3 action rows (essential.*) |
AW0006: Error (level:recommended, "...") -> Error (level:recommended, "...") with ListedBefore true, ListedAfter false |
| A level Paranoid based on Complete is added | endpoint-added rows for paranoid.default, paranoid.ci, paranoid.vnext, one per listed id |
TA0001: (absent) -> Error (level:paranoid) |
The changed default is why the diff compares effective actions: LC0001 changes what the compiler does in every endpoint while rulesets/ stays byte-identical, and AW0006 leaves the effective action unchanged while nine endpoint files lose a line. Every catalog id whose default differs between the two sides is compared, mentioned by an input or not.
A ref without .github/Rulebook-Settings.json (a commit before the rulebook existed, or the first pull request of a fixture) is an empty rulebook: every endpoint is endpoint-added. An unknown ref, or one that starts with -, throws; the action reports that as "no diff" and does not fail.
Compare-RulebookEndpoints returns one row per endpoint and id, ordered by endpoint (settings order, then endpoints only the ref has) and then by id:
| Property | Meaning |
|---|---|
Endpoint, File |
strict.ci and rulesets/strict.ci.ruleset.json (the default stage has no suffix in File) |
Id |
The diagnostic id; $null only on the one row of an added or removed endpoint that lists nothing |
Before, After |
The effective action at the ref and in the working tree; $null on the side where the endpoint does not exist |
BeforeSource, AfterSource |
The provenance tokens of section 2 |
BeforeDetail, AfterDetail |
The deciding entry's justification or the twin pair title |
ListedBefore, ListedAfter |
Whether the endpoint file lists the id on each side |
Change |
action (the effective action changed), listing (same action, listed on one side only, which happens when the analyzer default moves), endpoint-added, endpoint-removed |
Text |
<Id>: <Before> (<source>[, "<detail>"]) -> <After> (<source>[, "<detail>"]), with (absent) for a missing side |
The Validate job summary groups the rows into one table per changed endpoint, | Id | Before | After | Decided by |, where "Decided by" is the after side's source and detail; "No effective change" when there are no rows.
The ChangeRule pull request (WP09) does not use this diff: its body has one | Endpoint | Before | After | Note | table per change with a row for every endpoint the selectors match, changed or not, each side written as Warning (level:recommended) or None (override, "<justification>") with the tokens of section 2. It is computed from two in-memory input sets (the working tree and the candidate), because this diff lists changed rows only; the git-based diff of the pushed commit goes into the job summary of the change run (change-mechanics.md section 6).