diff --git a/spec/audit.py b/spec/audit.py index bf47e2b3..90383066 100644 --- a/spec/audit.py +++ b/spec/audit.py @@ -340,11 +340,22 @@ def check_interface(path, contract, text): return findings +# A `uses: @<40-hex sha>` pin, plus only a trailing Dependabot version comment (` # v1.2.3` - the +# leading `v`-or-digit is required). Dependabot bumps both per repo, so that drift is governed (like EOL), not a +# fidelity deviation. Anchored to `uses:`, so a 64-hex docker digest and a tag/branch ref (`@v4`) do not match. +# Hex is case-insensitive, and a hand-written note on a pin is not version-shaped, so it survives to be compared. +_ACTION_PIN = re.compile(r"(\buses:[ \t]*[^\s@]+)@[0-9a-fA-F]{40}(?:[ \t]+#[ \t]*v?[0-9][\w.\-]*)?") + + def normalize(text): - """Reduce a carried unit to its comparable form: neutralize line endings, since EOL variance is governed - separately, not a fidelity deviation. No placeholder masking - see spec/fidelity-model.md "Normalization". + """Reduce a carried unit to its comparable form: neutralize line endings (EOL variance is governed + separately, not a fidelity deviation) and neutralize a Dependabot-owned action pin - the 40-hex commit a + `uses: @` line pins, together with its trailing ` # vN` version comment - since Dependabot + bumps those per repo and that drift is governed, same category as EOL. This is NOT placeholder masking of + declared per-file tokens; see spec/fidelity-model.md "Normalization". """ - return text.replace("\r\n", "\n").replace("\r", "\n") + text = text.replace("\r\n", "\n").replace("\r", "\n") + return _ACTION_PIN.sub(r"\1@", text) @functools.lru_cache(maxsize=1024) # bounded; the keys that recur across repos are the canonical and its history @@ -773,6 +784,26 @@ def _selftest(): if got != want: ok = False print(f" {'ok ' if got == want else 'FAIL'} want={str(want):>8} got={str(got):>8} verbatim: {label}") + # Action-pin neutralization: a Dependabot uses:@ bump (both the 40-hex sha and its ` # vN` comment) + # must not count as verbatim drift, but a changed action name must. This is what lets a verbatim workflow + # region survive routine action bumps while still catching a real fork. + pin_a = " - uses: actions/checkout@" + "a" * 40 + " # v7.0.0\n" + pin_b = " - uses: actions/checkout@" + "B" * 40 + " # v7.0.1\n" # uppercase hex + version bump + pin_struct = " - uses: actions/setup-node@" + "a" * 40 + " # v7.0.0\n" + note_x = " - uses: actions/checkout@" + "a" * 40 + " # kept for the audited build\n" + note_y = " - uses: actions/checkout@" + "a" * 40 + " # kept for a different reason\n" + if content_hash(pin_a) != content_hash(pin_b): + ok = False + print(" FAIL action-pin: a uses:@ bump (sha + # vN, uppercase hex) should normalize equal") + elif content_hash(pin_a) == content_hash(pin_struct): + ok = False + print(" FAIL action-pin: a changed action name must still hash differently") + elif content_hash(note_x) == content_hash(note_y): + ok = False + print(" FAIL action-pin: a hand-written (non-version) pin comment must survive to be compared") + else: + print(" ok action-pin: version bump normalizes equal, changed action differs, hand-written note survives") + # Region extraction and hashing: a forked github-release block must hash differently from the canonical. region = split_jobs(rel_ok).get("github-release") forked_region = split_jobs(rel_ok.replace(" merge-multiple: true\n", " artifact-ids: 1\n")).get("github-release") diff --git a/spec/fidelity-model.md b/spec/fidelity-model.md index e8f53f23..145a3f37 100644 --- a/spec/fidelity-model.md +++ b/spec/fidelity-model.md @@ -12,7 +12,7 @@ Each [`spec/files.json`][files] entry declares one `fidelity`, defaulting to `pr - **presence** - the unit exists (a file, or a markdown section heading). The audit's baseline check. - **intent** - carried faithfully but judged by meaning, not bytes. A downstream copy legitimately differs (a governed divergence or a paraphrase), and equivalence is a human call via `intentRef`. The audit asserts nothing beyond presence. -- **verbatim** - byte-identical to the hub's canonical after line-ending normalization. The audit content-hashes the downstream copy against canonical. It applies to a whole file, a workflow job region (a job selected by key), or a markdown section region (a `## heading` block selected by name). The section granularity lets one file be **intent overall while a few of its sections are verbatim** - a universal rule block stays byte-identical fleet-wide even though the rest of the document is a repo-adapted paraphrase, so a stale section or a missing rule is caught while its heading still passes the presence check. +- **verbatim** - byte-identical to the hub's canonical after line-ending and action-pin normalization. The audit content-hashes the downstream copy against canonical. It applies to a whole file, a workflow job region (a job selected by key), or a markdown section region (a `## heading` block selected by name). The section granularity lets one file be **intent overall while a few of its sections are verbatim** - a universal rule block stays byte-identical fleet-wide even though the rest of the document is a repo-adapted paraphrase, so a stale section or a missing rule is caught while its heading still passes the presence check. - **interface** - an overridable body that must honor a named contract. The audit checks the contract by name and wiring, never the body. Fidelity is a declared field defaulting to `presence`, never inferred from `whole`/`placeholders`. `.editorconfig` and `.markdownlint-cli2.jsonc` are both whole with no placeholders yet sit at opposite fidelity, because the discriminator is governance, not field shape. @@ -30,7 +30,7 @@ The fixed interface of a workflow is stated in [`AGENTS.md`][agents] ("Orchestra ## Normalization -A verbatim check compares content by hash after **line-ending normalization only** - EOL variance is governed by the line-ending rules, not a fidelity deviation. It does **not** mask placeholders: a verbatim unit carries none. The files that declare a `placeholders` list (for example `.github/copilot-instructions.md` with ``, ``, ``) are fidelity `intent`, judged by hand and never hashed. Masking could not serve a hash anyway - a downstream copy holds the substituted value (`ptr727`), not the token (``), so masking the token in the canonical alone would guarantee a mismatch. A verbatim unit that ever needed a per-repo substitution would require template-matching (the canonical as a pattern, the copy as an instance), not this content hash. None does today. +A verbatim check compares content by hash after **line-ending and action-pin normalization** - EOL variance is governed by the line-ending rules, and a `uses: @` pin (with its trailing `# vN` comment) is Dependabot-owned and bumped per repo, so both are governed drift rather than a fidelity deviation. This keeps a verbatim workflow job region (the `github-release` job) from flagging on every routine action bump while still catching a real structural fork. It does **not** mask placeholders: a verbatim unit carries none. The files that declare a `placeholders` list (for example `.github/copilot-instructions.md` with ``, ``, ``) are fidelity `intent`, judged by hand and never hashed. Masking could not serve a hash anyway - a downstream copy holds the substituted value (`ptr727`), not the token (``), so masking the token in the canonical alone would guarantee a mismatch. A verbatim unit that ever needed a per-repo substitution would require template-matching (the canonical as a pattern, the copy as an instance), not this content hash. None does today. ## Stale Versus Modified