Add the verbatim engine - content-hash fixed units against canonical (#374, PR-C) - #377
Conversation
…374, PR-C) Content-hash a carried unit against the hub's canonical to detect a change to content meant to stay fixed, and classify a mismatch as stale (matches a past hub revision, re-vendor) or modified (matches none, the repo changed fixed content) via the canonical's git history. Integrity is by hash, never a version stamp. Runs at DRIFT: a byte diff is a hint to review, not proof of breakage. Normalization is line-endings only. A verbatim unit carries no per-repo placeholder: the files that declare `placeholders` are fidelity intent, judged by hand and never hashed, and masking could not serve a hash anyway - a copy holds the substituted value, not the token, so masking the token in the canonical alone would guarantee a mismatch. A unit that ever needed a per-repo substitution would require template-matching, not this hash; none does today. Enable two real units to exercise both granularities: the whole .markdownlint-cli2.jsonc file, and the canonical github-release job region of build-release-task.yml (extracted by job key from both sides). Self-tests cover identical/EOL-only match, body edit -> violated, past-revision -> stale, and a forked region hashing differently. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds the second fidelity engine to spec/audit.py: a verbatim content-hash check (EOL-normalized) that can classify mismatches as stale (matches a past canonical revision) vs violated (matches no canonical revision), and wires it into the baseline via spec/files.json.
Changes:
- Extend
spec/audit.pywith EOL-only normalization + sha256 hashing and a verbatim checker that consults the hub file’s git history. - Mark the
github-releasejob region inbuild-release-task.ymlas verbatim-pinned via the interface contract (verbatimJobs) and document the model update. - Update audit procedure docs to include verbatim checks in the mechanized subset.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| spec/files.json | Adds verbatimJobs for the build-release interface contract and points canonical comparison at the catalog snippet via reference. |
| spec/fidelity-model.md | Updates the verbatim definition/normalization rules to EOL-only and documents stale-vs-violated semantics. |
| spec/audit.py | Implements verbatim hashing/classification and integrates interface+verbatim checks into the audit loop and selftests. |
| AUDIT.md | Documents verbatim checks as part of the mechanized audit subset (needs terminology alignment per review comment). |
…#377) The verbatim verdict for the non-stale case was the enum 'violated' in code while every doc and DRIFT message said 'modified' - two labels for one outcome. Rename the enum to 'modified' so the code matches the guidance. git_file_history() shelled out per audited repo; one canonical is compared against every repo, so it recomputed the same history O(repos * commits) times. Cache it per rel_path for the run. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Narrow the verbatim-region claim to what the engine implements: a whole file or a workflow job region. Markdown-section-by-heading extraction is not wired, so the doc no longer promises it. - Rename the "Stale Versus Violated" heading to "Stale Versus Modified" - the last place still using the old verdict word. - Trim the normalize() docstring to terse form, pointing at the fidelity-model "Normalization" section instead of duplicating the design narrative. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
spec/audit.py:303
- check_verbatim() always walks the canonical's git history (git_file_history) even when the downstream content matches the current canonical. That forces at least one full history scan per canonical per run, even for clean repos, and also does unnecessary work before discovering an extract region is missing. Compute the current canonical match first (after any extract) and only call git_file_history() when there's an actual mismatch to classify as stale vs modified.
try:
canon_text = (ROOT / canonical_rel).read_text(encoding="utf-8")
except OSError:
return [("DRIFT", f"verbatim: {label} canonical {canonical_rel} is unreadable from the hub (spec error?)")]
history = git_file_history(canonical_rel)
if extract is not None:
…shing (#377) classify_verbatim already hashed the canonical once and each past revision once per call with an early exit, but across the ~21 per-repo audits the same canonical and history texts were re-hashed every time. Memoize content_hash (bounded input domain) so those recurring hashes are computed once - the hashing analogue of the git_file_history cache - while keeping the early-exit loop rather than a full set scan. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#377) - Memoize content_hash on the normalized form, not the raw text, so EOL-only variants (CRLF vs LF) share one cache entry as they normalize equal. - Decode git history (git log/show) as UTF-8 with replacement, and read the canonical the same way, matching the downstream base64 -> decode("utf-8", "replace") path. A locale-default or strict decode on one source could hash otherwise-equal content differently and fabricate a mismatch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
spec/audit.py:250
- The normalize() docstring is longer than it needs to be. AGENTS.md comment guidance says one line is the default; this can be expressed tersely without wrapping while still pointing to the spec doc for rationale.
"""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".
"""
Collapse the content_hash cache-key comment and the git_file_history docstring to one line each, and tighten the history-decode comment, per the AGENTS.md one-line-default comment guidance. No behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The contents-decode guard used truthiness (`if content.get("content")`), so a
legitimately empty file (encoding "base64", content "") was misread as no inline
content and flagged for hand-verification. Guard on `encoding == "base64"`
instead: an empty file decodes to "", while a genuinely non-inline payload
(too large, encoding "none") still yields None and is flagged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
spec/audit.py:250
- normalize()'s docstring is still a multi-line design narrative. Per AGENTS.md comment guidance (one line is the default), this can be collapsed to a single-line docstring that states the non-obvious behavior (EOL normalization) and points to spec/fidelity-model.md for rationale.
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".
"""
- Cap _hash_normalized's lru_cache at 1024 entries instead of unbounded. The keys that recur across repos are the canonical and its history (few); unique downstream copies only cycle through, so a bound cannot cost a real hit while it removes the unbounded-growth risk on a large fleet run. - Recast three semicolons that joined independent clauses (AUDIT.md verbatim sentence, the history-decode comment, the fidelity-model template-matching note) into separate sentences, per the prose style guidance. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
spec/audit.py:251
- normalize()'s docstring is wrapped over multiple lines with design rationale. Per AGENTS.md comment guidance (one line default), collapse this to a single-line docstring that just states the non-obvious behavior and points to the model doc for rationale (AGENTS.md:146-148).
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".
"""
return text.replace("\r\n", "\n").replace("\r", "\n")
spec/audit.py:254
- This inline comment uses a semicolon to join two independent clauses, which the repo's prose rules forbid for agent-authored text (AGENTS.md:158). Recast without a semicolon.
@functools.lru_cache(maxsize=1024) # bounded; the keys that recur across repos are the canonical and its history
PR-C of the fidelity series (#374). Adds the second engine: verbatim content-hashing of fixed units against the hub's canonical.
What it does
normalize(EOL-only) →content_hash(sha256) →classify_verbatim: None (matches current canonical), stale (matches a past hub revision → re-vendor), or modified (matches no revision → the repo changed fixed content). The discriminator is the content hash walked over the canonical's git history, never a version stamp (spoofable)..markdownlint-cli2.jsonc.github-releasejob ofbuild-release-task.yml, extracted by job key from both sides (reference→ catalog snippet,contract.verbatimJobs).Normalization is EOL-only (design note)
A verbatim unit carries no per-repo placeholder. The files that declare
placeholdersare fidelityintent(judged by hand, never hashed). Masking could not serve a hash anyway: a downstream copy holds the substituted value (ptr727), not the token (<owner>), so masking the token in the canonical alone would guarantee a mismatch. A unit that ever needed per-repo substitution would require template-matching, not this hash — none does today.Verification
python3 spec/audit.py --selftest— interface (14) + verbatim: identical / CRLF / bare-CR match, body edit → violated, past-revision → stale, stale-modulo-EOL, no-revision → violated, forked region hashes differently.python3 spec/validate.pygreen (21 cataloged)..markdownlint-cli2.jsoncand the snippetgithub-releaseregion self-hash clean; both deliberate forks → DRIFT.audit.py PlexCleanerexit 0 — surfaced a real advisory finding (PlexCleaner forked the canonicalgithub-releaseorchestration).Held on
develop; nodevelop → mainpromotion.🤖 Generated with Claude Code