validate-provenance.sh passes a source_keys entry that asserts a contribution which never happened #118
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found during the #99 ADR-0020 retrofit, wave 4 (
forge).What happens
validate-provenance.shchecks that a slug exists, thatContributing filesentries resolve on disk, and that each contributing file back-references its parent slug (check 5). It never checks whether the asserted contribution is true — that the named source actually contributed anything to the named file.So an entry can be confidently wrong and the script exits 0, silently.
The concrete case
forge's retrofit rewrotereferences/sources.md'sclaude-code-plugins-docsentry:Contributing fileswent(none)→references/apm-routes.md.Neither is true. Nothing in
apm-routes.mdderives fromhttps://code.claude.com/docs/en/plugins: its content is the ADR-0015 supersession (a repo decision), the always-inline reasoning (a repo decision), the no-recheck reasoning (a repo decision), and twoapmCLI commands — which are Microsoft apm, provenanced under a different slug entirely and not inforge's chain at all.The rewritten description is also internally inconsistent by the validator's own rule: it claims the source grounds a table that lives in
SKILL.md, butSKILL.mdis not listed as a contributing file, and the slug is absent fromSKILL.md'smetadata.source_keys. AddingSKILL.mdto close that gap would then fail check 5. The two halves of the entry cannot both be right — and the script reported nothing either way.Why it matters
Provenance exists so a later reader can trace a claim to its source. An entry that is merely incomplete degrades gracefully — you look elsewhere. An entry that asserts a wrong source sends the reader to a document that does not support the text, and it survives every automated check, so nothing ever surfaces it. It is worse than the
(none)it replaced.This is also a predictable failure mode of the retrofit work itself: a compression pass rewrites prose, and rewriting a hedge into a confident claim reads like an improvement. Wave 4 produced three separate instances of invented-during-compression claims across three skills; this is the one that lives in the provenance layer, where the gate looks like it is watching.
Suggested fix
Full semantic verification is not automatable, but two cheap things would have caught this:
sources.mddescription names a file (e.g. "Step 2's dispatch table" →SKILL.md), that file should appear in the entry's ownContributing files. A mismatch between the prose and the structured field is mechanically detectable and was present here.Contributing filestransition away from(none)as needing review in the audit rubric, rather than treating it as ordinary.(none)is a deliberate honest declaration; changing it is a new claim.The deeper point is that
skill-audit's provenance dimension should treat a changed provenance assertion as something to verify by reading, since the script structurally cannot. Worth stating in the rubric.Files
plugins/kyberforge/.apm/skills/skill-audit/scripts/validate-provenance.sh— check 5 and the surrounding checksplugins/kyberforge/.apm/skills/forge/references/sources.md— the case above, now correctedRelated
Found during #99 wave 4. See also #111 (no way to declare a reference file house-authored — the same file, the opposite gap: honest declaration is impossible while dishonest declaration is unchecked).
Still open — but a material change on
refactor/adr0020-skill-retrofitstrengthens the caseThe defect this issue describes is unfixed:
validate-provenance.shstill cannot tell a true contribution claim from a false one. What changed is that checks 7 and 8 were effectively dead across the entiregitplugin and are now live, which means the checks this issue says are insufficient were, until now, not even running.The mechanism
A
sources.mdentry'sResearch doc:value is a path, and very often a path plus an annotation:The whole string was treated as the path. It did not resolve, so checks 7-8 skipped — silently. The script now strips the annotation at the first
§,→or((with(none)recognised before the strip, since it is itself a parenthesis), and emits an INFO naming the slug whenever checks 7-8 still cannot run, instead of declining without saying so.How dead it was
Counted across the nine skills in
plugins/git/.apm/skills/:Research doc:entriesEight of nine skills carried at least one annotated value.
git-historywas the only skill in the plugin where checks 7-8 ever ran — which is precisely why its three INFOs were the only ones anyone had seen.Result of turning them on
Corpus-wide, running
skill-audit/scripts/validate-provenance.shover all 39 skills:main: 7 INFO, 0 FAILThat is 32 findings that did not exist before and zero new FAILs. No skill turns out to be missing a
sources.mdentry it should have had; the checks were simply not looking. (Three ofmain's seven INFOs were the house-authored false positives resolved by #111 — the net movement is 7 → 36.)Why this argues for the issue rather than against it
Checks 7-8 now run across the plugin and report 32 findings, and not one of them is the failure this issue describes. The
forge/claude-code-plugins-docsentry would still pass every check on the branch. The gate got measurably more thorough without getting any closer to detecting a confidently wrong provenance claim — which is the point: the two cheap cross-checks suggested here (prose naming a file that is absent fromContributing files; a transition away from(none)flagged for review) remain unimplemented, and the rubric note about treating a changed provenance assertion as read-to-verify has not been written.One caveat: the annotation-stripping change is not pinned by a test.
skill-audit/tests/validate-provenance.batsis 17 cases and none covers it.Implementation note
Two approaches from the suggested fix were tried and rejected before implementing, worth recording so they aren't re-proposed blind:
.mdfile absent from Contributing files?") — tested empirically against the real 95-entry corpus: 3/95 flagged, all three false positives (a research-doc path mentioned as provenance-only, a research doc's own filename named while explicitly disclaiming it informed anything, a self-reference tosources.md/CONTEXT.md). It also would not have caught the actual bug this issue describes — the bad description said "Grounds... Step 2's dispatch table," never namingSKILL.mdliterally. Not implemented.What shipped, defense in depth across two layers:
validate-provenance.shcheck 9 (new): diffs each sources.md entry'sDescription:/Contributing files:text against a base ref (git merge-base HEAD origin/mainby default, overridable via--base-ref/VALIDATE_PROVENANCE_BASE_REF). Always INFO, never FAIL — it flags "go verify this," it isn't itself proof of a defect. A brand-new entry absent at the base ref is a creation, not a change, and isn't flagged (avoids the false-positive class above). An unresolvable base ref emits one graceful INFO for the whole check, never a silent skip, matching this script's existing checks 7/8 convention. 8 new bats tests (73 total, 0 failures, independently re-run).validation-scripts.md: when check 9 fires, the auditor must actually open the named Contributing file(s) and the slug'sResearch doc:and confirm the claim by reading, not just relay the INFO.skill-author/references/retrofit.md: a new checklist bullet (distinct from the existing Contributing-files-staleness one, which covers a different failure mode — forgetting to update after moving content) warning against upgrading an honest hedge into an unsupported confident claim during compression, with the rule to re-read the upstream research doc before strengthening any provenance wording.The concrete case (
forge'sclaude-code-plugins-docsentry) was already corrected onmainprior to this session, per the issue body — no content fix needed there, only the gate/rubric/guardrail work above.