diff --git a/plugins/git/.apm/skills/git-branches/references/orchestrator-contract.md b/plugins/git/.apm/skills/git-branches/references/orchestrator-contract.md index 7fceb33..954c835 100644 --- a/plugins/git/.apm/skills/git-branches/references/orchestrator-contract.md +++ b/plugins/git/.apm/skills/git-branches/references/orchestrator-contract.md @@ -1,3 +1,7 @@ +--- +source_keys: [] +--- + # Orchestrator request contract `git-orchestrate` and other calling agents send this shape. The result shape they parse back is in diff --git a/plugins/git/skills/git-branches/references/orchestrator-contract.md b/plugins/git/skills/git-branches/references/orchestrator-contract.md index 7fceb33..954c835 100644 --- a/plugins/git/skills/git-branches/references/orchestrator-contract.md +++ b/plugins/git/skills/git-branches/references/orchestrator-contract.md @@ -1,3 +1,7 @@ +--- +source_keys: [] +--- + # Orchestrator request contract `git-orchestrate` and other calling agents send this shape. The result shape they parse back is in diff --git a/plugins/kyberforge/.apm/skills/agent-audit/scripts/validate-provenance.sh b/plugins/kyberforge/.apm/skills/agent-audit/scripts/validate-provenance.sh index 82395c3..a5525d1 100755 --- a/plugins/kyberforge/.apm/skills/agent-audit/scripts/validate-provenance.sh +++ b/plugins/kyberforge/.apm/skills/agent-audit/scripts/validate-provenance.sh @@ -130,15 +130,55 @@ def parse_source_keys(fm): def parse_h2_slugs(content): return re.findall(r'^## (.+)$', content, re.MULTILINE) +# ===== BEGIN SHARED CONTRIBUTING-FILES PARSER ===== +# ONE parser, embedded VERBATIM in two scripts: +# plugins/kyberforge/.apm/skills/skill-audit/scripts/validate-provenance.sh +# plugins/kyberforge/.apm/skills/agent-audit/scripts/validate-provenance.sh +# The block between these markers must stay byte-identical in both. It is +# copied rather than imported because a cache-installed plugin's scripts cannot +# read files outside their own plugin directory, so there is no single file both +# can share — the same constraint that forces the ADR-0020 boundary resolver to +# be duplicated across three scripts. Edit one copy, then paste it over the +# other. +# +# tests/test-adr0020-contract.sh hashes both copies and fails on drift. Before +# it did, the agent-audit copy's docstring merely ASSERTED the two were +# "behaviourally identical" and nothing checked it — which is how the two +# already-diverged spellings of the bullet loop went unnoticed. +# +# Requires: re (imported by the host script). + + def parse_contributing_files(content, slug): """Find the Contributing files for a given slug H2 in content. - Accepts the inline form and the bullet form; recognising only the - inline one silently skips the contributing-file checks on every - sources.md written the other way. Returns a list of paths with any - trailing parenthetical note stripped; "(none)" returns an empty list - and a slug with no entry returns None. Kept behaviourally identical to - skill-audit's copy, which is where the bug was found. + Both authored forms are accepted, because both are in use across the + corpus and only recognising the first silently skipped the contributing- + file checks on every sources.md written the other way: + + - **Contributing files:** SKILL.md, references/a.md + + **Contributing files:** + - SKILL.md (what this source contributed) + - references/a.md (what this source contributed) + + Returns a list of paths with any trailing parenthetical note stripped. + Note the bullet form's notes may themselves contain commas, so the list + is built per bullet rather than by splitting the joined value. + + The three return values are NOT interchangeable, and callers depend on + the distinction: + + [path, ...] the entry names contributing files + [] the entry EXPLICITLY records "(none)" + None the entry says nothing this parser can read + + Only an explicit "(none)" yields []. A "Contributing files:" heading + followed by a numbered list, by `*` bullets, or by prose parses nothing + and returns None, never [] — a caller reads [] as a deliberate "no + contributing files" record and SKIPS its check on that basis, so a parse + failure returning [] would silently disable the check instead of leaving + the unreadable entry exposed to it. """ pattern = re.compile( r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)', @@ -150,15 +190,19 @@ def parse_contributing_files(content, slug): block = m.group(1) def strip_note(entry): + # "references/a.md (why)" -> "references/a.md" return re.sub(r'\s*\(.*$', '', entry).strip() + # Inline form: value on the same line, comma-separated, no notes. cf_m = re.search(r'^\- \*\*Contributing files:\*\* (.+)$', block, re.MULTILINE) if cf_m: value = cf_m.group(1).strip() if value.startswith("(none"): return [] - return [p for p in (strip_note(x) for x in value.split(",")) if p] + return [p for p in (strip_note(x) for x in value.split(",")) + if p] or None + # Bullet form: heading on its own line, one file per following bullet. cf_m = re.search(r'^\*\*Contributing files:\*\*\s*$', block, re.MULTILINE) if not cf_m: return None @@ -177,7 +221,8 @@ def parse_contributing_files(content, slug): entry = strip_note(entry) if entry: files.append(entry) - return files + return files or None +# ===== END SHARED CONTRIBUTING-FILES PARSER ===== def parse_research_doc(content, slug): pattern = re.compile( diff --git a/plugins/kyberforge/.apm/skills/skill-audit/scripts/validate-provenance.sh b/plugins/kyberforge/.apm/skills/skill-audit/scripts/validate-provenance.sh index 619ac47..5524605 100755 --- a/plugins/kyberforge/.apm/skills/skill-audit/scripts/validate-provenance.sh +++ b/plugins/kyberforge/.apm/skills/skill-audit/scripts/validate-provenance.sh @@ -18,11 +18,16 @@ Checks performed: 0 source_keys present but references/sources.md absent 1 FILL IN: placeholders in sources.md 2 source_keys in SKILL.md → slug exists in sources.md - 3 source_keys in references/*.md → slug exists in sources.md (INFO if no source_keys) + 3 source_keys in references/*.md → slug exists in sources.md (INFO if no + source_keys; an explicit 'source_keys: []' declares the file house-authored + and passes silently) 4 Contributing files listed in sources.md exist on disk 5 Contributing files back-reference the parent slug in their source_keys 6 Research doc field present and not placeholder - 7 Slug in sources.md present in upstream research doc (INFO only) + 7 Slug in sources.md present in upstream research doc (INFO only). A section + annotation ('§ ...', '→ ...', '(...)') is stripped before the path is + resolved; a path that still does not resolve is reported as an INFO saying + checks 7 and 8 did not run, never skipped silently. 8 Extracted non-(none) slug in research doc present in sources.md EOF } @@ -89,16 +94,53 @@ def parse_source_keys(fm): in_metadata = False return keys +# An explicit `source_keys: []` — top-level or under metadata: — is a +# DECLARATION that the file is house-authored and has no external source. +# parse_source_keys() returns [] both for that and for a file with no +# source_keys key at all, so the two are indistinguishable downstream and +# check 3 emitted the same INFO for each (#111). That left no honest way to +# record "this file has no external source": the only ways to silence the INFO +# were to invent a slug or borrow an unrelated one, both false provenance +# claims that then have to be maintained in sources.md as well. A bare +# `source_keys:` with nothing after it is NOT accepted here — that reads as a +# truncated or half-written entry, not a decision. +EMPTY_SOURCE_KEYS_RE = re.compile(r'^\s*source_keys:\s*\[\s*\]\s*$') + +def declares_empty_source_keys(fm): + """True when frontmatter carries an explicit, empty `source_keys: []`.""" + if fm is None: + return False + return any(EMPTY_SOURCE_KEYS_RE.match(line) for line in fm.splitlines()) + def parse_h2_slugs(content): """Return list of H2 heading values from a markdown file.""" return re.findall(r'^## (.+)$', content, re.MULTILINE) +# ===== BEGIN SHARED CONTRIBUTING-FILES PARSER ===== +# ONE parser, embedded VERBATIM in two scripts: +# plugins/kyberforge/.apm/skills/skill-audit/scripts/validate-provenance.sh +# plugins/kyberforge/.apm/skills/agent-audit/scripts/validate-provenance.sh +# The block between these markers must stay byte-identical in both. It is +# copied rather than imported because a cache-installed plugin's scripts cannot +# read files outside their own plugin directory, so there is no single file both +# can share — the same constraint that forces the ADR-0020 boundary resolver to +# be duplicated across three scripts. Edit one copy, then paste it over the +# other. +# +# tests/test-adr0020-contract.sh hashes both copies and fails on drift. Before +# it did, the agent-audit copy's docstring merely ASSERTED the two were +# "behaviourally identical" and nothing checked it — which is how the two +# already-diverged spellings of the bullet loop went unnoticed. +# +# Requires: re (imported by the host script). + + def parse_contributing_files(content, slug): """Find the Contributing files for a given slug H2 in content. Both authored forms are accepted, because both are in use across the - corpus and only recognising the first silently skipped checks 4 and 5 - on every skill using the second: + corpus and only recognising the first silently skipped the contributing- + file checks on every sources.md written the other way: - **Contributing files:** SKILL.md, references/a.md @@ -107,10 +149,22 @@ def parse_contributing_files(content, slug): - references/a.md (what this source contributed) Returns a list of paths with any trailing parenthetical note stripped. - A "(none)" value returns an empty list; a slug with no Contributing - files entry at all returns None. Note the bullet form's notes may - themselves contain commas, so the list is built per bullet rather than - by splitting the joined value. + Note the bullet form's notes may themselves contain commas, so the list + is built per bullet rather than by splitting the joined value. + + The three return values are NOT interchangeable, and callers depend on + the distinction: + + [path, ...] the entry names contributing files + [] the entry EXPLICITLY records "(none)" + None the entry says nothing this parser can read + + Only an explicit "(none)" yields []. A "Contributing files:" heading + followed by a numbered list, by `*` bullets, or by prose parses nothing + and returns None, never [] — a caller reads [] as a deliberate "no + contributing files" record and SKIPS its check on that basis, so a parse + failure returning [] would silently disable the check instead of leaving + the unreadable entry exposed to it. """ pattern = re.compile( r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)', @@ -131,15 +185,15 @@ def parse_contributing_files(content, slug): value = cf_m.group(1).strip() if value.startswith("(none"): return [] - return [p for p in (strip_note(x) for x in value.split(",")) if p] + return [p for p in (strip_note(x) for x in value.split(",")) + if p] or None # Bullet form: heading on its own line, one file per following bullet. cf_m = re.search(r'^\*\*Contributing files:\*\*\s*$', block, re.MULTILINE) if not cf_m: return None - rest = block[cf_m.end():] files = [] - for line in rest.splitlines(): + for line in block[cf_m.end():].splitlines(): line = line.strip() if not line: if files: @@ -153,7 +207,8 @@ def parse_contributing_files(content, slug): entry = strip_note(entry) if entry: files.append(entry) - return files + return files or None +# ===== END SHARED CONTRIBUTING-FILES PARSER ===== def parse_research_doc(content, slug): """Find the Research doc value for a given slug H2 in content.""" @@ -170,6 +225,36 @@ def parse_research_doc(content, slug): return None return rd_m.group(1).strip() +# A Research doc value is a path, and very often a path PLUS an annotation +# naming the section the slug came from: +# +# plugins/git/docs/research/docs/git/gitflow.md (whole-document reference) +# plugins/git/docs/research/docs/git/remotes.md → `## Pushing (`git push`)` +# .../pre-commit/hooks-reference.md § "pre-commit-hooks (official collection)" +# +# os.path.isfile() is false for every one of those strings, and checks 7 and 8 +# used to skip SILENTLY whenever the path did not resolve. The effect was that +# both checks were dead on eight of the nine git skills — git-history, the one +# skill writing a bare path, was the only place they ran, which is why it was +# the only skill ever reporting a check-7 INFO. Strip the annotation before +# resolving, and report when the result still does not resolve: a check that +# quietly does not run is worse than one that fails. +RESEARCH_DOC_ANNOTATION_RE = re.compile(r'[§→(]') + +def strip_research_doc_annotation(value): + """Path part of a Research doc value, with any section annotation removed.""" + return RESEARCH_DOC_ANNOTATION_RE.split(value, maxsplit=1)[0].strip() + +def research_doc_is_none(value): + """True when a Research doc value declares that no research doc backs the slug. + + Both '(none)' and the bare 'none — org convention, ...' spelling are in + use; recognising only the parenthesised one would report the other as an + unresolvable path. Checked BEFORE the annotation strip, because '(none)' + is itself a parenthesis and would strip to the empty string. + """ + return re.match(r'\(?none\b', value.strip(), re.IGNORECASE) is not None + def parse_status(content, slug): """Find the Status value for a given slug H2 in content.""" pattern = re.compile( @@ -321,11 +406,17 @@ if os.path.isdir(refs_dir): ref_fm, _ = parse_frontmatter(ref_content) ref_keys = parse_source_keys(ref_fm) if not ref_keys: + # An explicit `source_keys: []` is a deliberate declaration that + # the file is house-authored, and passes silently. The INFO is for + # files that never said either way. + if declares_empty_source_keys(ref_fm): + continue emit_info( f"No source_keys frontmatter", rel, "This references file has no source_keys — provenance cannot be verified. " - "Add source_keys frontmatter listing the slugs from references/sources.md that informed this file." + "Add source_keys frontmatter listing the slugs from references/sources.md that informed this file, " + "or declare an explicit 'source_keys: []' if the file is house-authored and has no external source." ) else: for slug in ref_keys: @@ -390,24 +481,51 @@ for slug in parse_h2_slugs(sources_content): f"The '## {slug}' entry has an unfilled Research doc value.", f"Set '- **Research doc:**' to a real path relative to repo root, or '(none)' if not applicable." ) - else: - # Check 7: Upstream forward — slug should appear in research doc - if repo_root and not rd_value.startswith("(none"): - rd_abs = os.path.join(repo_root, rd_value) - if os.path.isfile(rd_abs): + elif not research_doc_is_none(rd_value): + # Check 7: Upstream forward — slug should appear in research doc. + # Every path out of here that does NOT run the check says so out loud. + rd_path = strip_research_doc_annotation(rd_value) + if not repo_root: + emit_info( + f"Upstream checks skipped for '{slug}' — no repo root above the skill directory", + f"references/sources.md (## {slug})", + f"'{rd_value}' is a path relative to the repo root, but no ancestor of the skill directory contains a .git entry, " + f"so it cannot be resolved. Checks 7 and 8 did not run for this slug. " + f"Run this script against a skill inside a checkout." + ) + elif not rd_path: + emit_info( + f"Upstream checks skipped for '{slug}' — Research doc value names no path", + f"references/sources.md (## {slug})", + f"The Research doc value '{rd_value}' is entirely annotation — stripping the section marker leaves no path. " + f"Checks 7 and 8 did not run for this slug. " + f"Give the value a file path relative to the repo root, or record '(none)' if no research doc backs this entry." + ) + else: + rd_abs = os.path.join(repo_root, rd_path) + if not os.path.isfile(rd_abs): + emit_info( + f"Upstream checks skipped for '{slug}' — research doc '{rd_path}' does not exist", + f"references/sources.md (## {slug})", + f"'{rd_value}' resolves to '{rd_path}' relative to the repo root and no file is there. " + f"Checks 7 and 8 did not run for this slug, so nothing verified that the research doc still backs it. " + f"Point the value at one existing file — a brace expansion, a comma-separated list of paths, or a bare section title does not resolve — " + f"or record '(none)' if no research doc backs this entry." + ) + else: with open(rd_abs) as f: rd_content = f.read() rd_slugs = set(parse_h2_slugs(rd_content)) if slug not in rd_slugs: emit_info( - f"Slug '{slug}' not found as H2 in research doc '{rd_value}'", + f"Slug '{slug}' not found as H2 in research doc '{rd_path}'", f"references/sources.md (## {slug})", - f"The research doc '{rd_value}' does not have a '## {slug}' heading. " + f"The research doc '{rd_path}' does not have a '## {slug}' heading. " f"The provenance link may be imprecise — the slug name in sources.md may differ from the research doc's heading." ) # Track for Check 8 if rd_abs not in research_docs_seen: - research_docs_seen[rd_abs] = (rd_value, set()) + research_docs_seen[rd_abs] = (rd_path, set()) research_docs_seen[rd_abs][1].add(slug) # --- Check 8: Upstream reverse --- diff --git a/plugins/kyberforge/skills/agent-audit/scripts/validate-provenance.sh b/plugins/kyberforge/skills/agent-audit/scripts/validate-provenance.sh index 82395c3..a5525d1 100755 --- a/plugins/kyberforge/skills/agent-audit/scripts/validate-provenance.sh +++ b/plugins/kyberforge/skills/agent-audit/scripts/validate-provenance.sh @@ -130,15 +130,55 @@ def parse_source_keys(fm): def parse_h2_slugs(content): return re.findall(r'^## (.+)$', content, re.MULTILINE) +# ===== BEGIN SHARED CONTRIBUTING-FILES PARSER ===== +# ONE parser, embedded VERBATIM in two scripts: +# plugins/kyberforge/.apm/skills/skill-audit/scripts/validate-provenance.sh +# plugins/kyberforge/.apm/skills/agent-audit/scripts/validate-provenance.sh +# The block between these markers must stay byte-identical in both. It is +# copied rather than imported because a cache-installed plugin's scripts cannot +# read files outside their own plugin directory, so there is no single file both +# can share — the same constraint that forces the ADR-0020 boundary resolver to +# be duplicated across three scripts. Edit one copy, then paste it over the +# other. +# +# tests/test-adr0020-contract.sh hashes both copies and fails on drift. Before +# it did, the agent-audit copy's docstring merely ASSERTED the two were +# "behaviourally identical" and nothing checked it — which is how the two +# already-diverged spellings of the bullet loop went unnoticed. +# +# Requires: re (imported by the host script). + + def parse_contributing_files(content, slug): """Find the Contributing files for a given slug H2 in content. - Accepts the inline form and the bullet form; recognising only the - inline one silently skips the contributing-file checks on every - sources.md written the other way. Returns a list of paths with any - trailing parenthetical note stripped; "(none)" returns an empty list - and a slug with no entry returns None. Kept behaviourally identical to - skill-audit's copy, which is where the bug was found. + Both authored forms are accepted, because both are in use across the + corpus and only recognising the first silently skipped the contributing- + file checks on every sources.md written the other way: + + - **Contributing files:** SKILL.md, references/a.md + + **Contributing files:** + - SKILL.md (what this source contributed) + - references/a.md (what this source contributed) + + Returns a list of paths with any trailing parenthetical note stripped. + Note the bullet form's notes may themselves contain commas, so the list + is built per bullet rather than by splitting the joined value. + + The three return values are NOT interchangeable, and callers depend on + the distinction: + + [path, ...] the entry names contributing files + [] the entry EXPLICITLY records "(none)" + None the entry says nothing this parser can read + + Only an explicit "(none)" yields []. A "Contributing files:" heading + followed by a numbered list, by `*` bullets, or by prose parses nothing + and returns None, never [] — a caller reads [] as a deliberate "no + contributing files" record and SKIPS its check on that basis, so a parse + failure returning [] would silently disable the check instead of leaving + the unreadable entry exposed to it. """ pattern = re.compile( r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)', @@ -150,15 +190,19 @@ def parse_contributing_files(content, slug): block = m.group(1) def strip_note(entry): + # "references/a.md (why)" -> "references/a.md" return re.sub(r'\s*\(.*$', '', entry).strip() + # Inline form: value on the same line, comma-separated, no notes. cf_m = re.search(r'^\- \*\*Contributing files:\*\* (.+)$', block, re.MULTILINE) if cf_m: value = cf_m.group(1).strip() if value.startswith("(none"): return [] - return [p for p in (strip_note(x) for x in value.split(",")) if p] + return [p for p in (strip_note(x) for x in value.split(",")) + if p] or None + # Bullet form: heading on its own line, one file per following bullet. cf_m = re.search(r'^\*\*Contributing files:\*\*\s*$', block, re.MULTILINE) if not cf_m: return None @@ -177,7 +221,8 @@ def parse_contributing_files(content, slug): entry = strip_note(entry) if entry: files.append(entry) - return files + return files or None +# ===== END SHARED CONTRIBUTING-FILES PARSER ===== def parse_research_doc(content, slug): pattern = re.compile( diff --git a/plugins/kyberforge/skills/skill-audit/scripts/validate-provenance.sh b/plugins/kyberforge/skills/skill-audit/scripts/validate-provenance.sh index 619ac47..5524605 100755 --- a/plugins/kyberforge/skills/skill-audit/scripts/validate-provenance.sh +++ b/plugins/kyberforge/skills/skill-audit/scripts/validate-provenance.sh @@ -18,11 +18,16 @@ Checks performed: 0 source_keys present but references/sources.md absent 1 FILL IN: placeholders in sources.md 2 source_keys in SKILL.md → slug exists in sources.md - 3 source_keys in references/*.md → slug exists in sources.md (INFO if no source_keys) + 3 source_keys in references/*.md → slug exists in sources.md (INFO if no + source_keys; an explicit 'source_keys: []' declares the file house-authored + and passes silently) 4 Contributing files listed in sources.md exist on disk 5 Contributing files back-reference the parent slug in their source_keys 6 Research doc field present and not placeholder - 7 Slug in sources.md present in upstream research doc (INFO only) + 7 Slug in sources.md present in upstream research doc (INFO only). A section + annotation ('§ ...', '→ ...', '(...)') is stripped before the path is + resolved; a path that still does not resolve is reported as an INFO saying + checks 7 and 8 did not run, never skipped silently. 8 Extracted non-(none) slug in research doc present in sources.md EOF } @@ -89,16 +94,53 @@ def parse_source_keys(fm): in_metadata = False return keys +# An explicit `source_keys: []` — top-level or under metadata: — is a +# DECLARATION that the file is house-authored and has no external source. +# parse_source_keys() returns [] both for that and for a file with no +# source_keys key at all, so the two are indistinguishable downstream and +# check 3 emitted the same INFO for each (#111). That left no honest way to +# record "this file has no external source": the only ways to silence the INFO +# were to invent a slug or borrow an unrelated one, both false provenance +# claims that then have to be maintained in sources.md as well. A bare +# `source_keys:` with nothing after it is NOT accepted here — that reads as a +# truncated or half-written entry, not a decision. +EMPTY_SOURCE_KEYS_RE = re.compile(r'^\s*source_keys:\s*\[\s*\]\s*$') + +def declares_empty_source_keys(fm): + """True when frontmatter carries an explicit, empty `source_keys: []`.""" + if fm is None: + return False + return any(EMPTY_SOURCE_KEYS_RE.match(line) for line in fm.splitlines()) + def parse_h2_slugs(content): """Return list of H2 heading values from a markdown file.""" return re.findall(r'^## (.+)$', content, re.MULTILINE) +# ===== BEGIN SHARED CONTRIBUTING-FILES PARSER ===== +# ONE parser, embedded VERBATIM in two scripts: +# plugins/kyberforge/.apm/skills/skill-audit/scripts/validate-provenance.sh +# plugins/kyberforge/.apm/skills/agent-audit/scripts/validate-provenance.sh +# The block between these markers must stay byte-identical in both. It is +# copied rather than imported because a cache-installed plugin's scripts cannot +# read files outside their own plugin directory, so there is no single file both +# can share — the same constraint that forces the ADR-0020 boundary resolver to +# be duplicated across three scripts. Edit one copy, then paste it over the +# other. +# +# tests/test-adr0020-contract.sh hashes both copies and fails on drift. Before +# it did, the agent-audit copy's docstring merely ASSERTED the two were +# "behaviourally identical" and nothing checked it — which is how the two +# already-diverged spellings of the bullet loop went unnoticed. +# +# Requires: re (imported by the host script). + + def parse_contributing_files(content, slug): """Find the Contributing files for a given slug H2 in content. Both authored forms are accepted, because both are in use across the - corpus and only recognising the first silently skipped checks 4 and 5 - on every skill using the second: + corpus and only recognising the first silently skipped the contributing- + file checks on every sources.md written the other way: - **Contributing files:** SKILL.md, references/a.md @@ -107,10 +149,22 @@ def parse_contributing_files(content, slug): - references/a.md (what this source contributed) Returns a list of paths with any trailing parenthetical note stripped. - A "(none)" value returns an empty list; a slug with no Contributing - files entry at all returns None. Note the bullet form's notes may - themselves contain commas, so the list is built per bullet rather than - by splitting the joined value. + Note the bullet form's notes may themselves contain commas, so the list + is built per bullet rather than by splitting the joined value. + + The three return values are NOT interchangeable, and callers depend on + the distinction: + + [path, ...] the entry names contributing files + [] the entry EXPLICITLY records "(none)" + None the entry says nothing this parser can read + + Only an explicit "(none)" yields []. A "Contributing files:" heading + followed by a numbered list, by `*` bullets, or by prose parses nothing + and returns None, never [] — a caller reads [] as a deliberate "no + contributing files" record and SKIPS its check on that basis, so a parse + failure returning [] would silently disable the check instead of leaving + the unreadable entry exposed to it. """ pattern = re.compile( r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)', @@ -131,15 +185,15 @@ def parse_contributing_files(content, slug): value = cf_m.group(1).strip() if value.startswith("(none"): return [] - return [p for p in (strip_note(x) for x in value.split(",")) if p] + return [p for p in (strip_note(x) for x in value.split(",")) + if p] or None # Bullet form: heading on its own line, one file per following bullet. cf_m = re.search(r'^\*\*Contributing files:\*\*\s*$', block, re.MULTILINE) if not cf_m: return None - rest = block[cf_m.end():] files = [] - for line in rest.splitlines(): + for line in block[cf_m.end():].splitlines(): line = line.strip() if not line: if files: @@ -153,7 +207,8 @@ def parse_contributing_files(content, slug): entry = strip_note(entry) if entry: files.append(entry) - return files + return files or None +# ===== END SHARED CONTRIBUTING-FILES PARSER ===== def parse_research_doc(content, slug): """Find the Research doc value for a given slug H2 in content.""" @@ -170,6 +225,36 @@ def parse_research_doc(content, slug): return None return rd_m.group(1).strip() +# A Research doc value is a path, and very often a path PLUS an annotation +# naming the section the slug came from: +# +# plugins/git/docs/research/docs/git/gitflow.md (whole-document reference) +# plugins/git/docs/research/docs/git/remotes.md → `## Pushing (`git push`)` +# .../pre-commit/hooks-reference.md § "pre-commit-hooks (official collection)" +# +# os.path.isfile() is false for every one of those strings, and checks 7 and 8 +# used to skip SILENTLY whenever the path did not resolve. The effect was that +# both checks were dead on eight of the nine git skills — git-history, the one +# skill writing a bare path, was the only place they ran, which is why it was +# the only skill ever reporting a check-7 INFO. Strip the annotation before +# resolving, and report when the result still does not resolve: a check that +# quietly does not run is worse than one that fails. +RESEARCH_DOC_ANNOTATION_RE = re.compile(r'[§→(]') + +def strip_research_doc_annotation(value): + """Path part of a Research doc value, with any section annotation removed.""" + return RESEARCH_DOC_ANNOTATION_RE.split(value, maxsplit=1)[0].strip() + +def research_doc_is_none(value): + """True when a Research doc value declares that no research doc backs the slug. + + Both '(none)' and the bare 'none — org convention, ...' spelling are in + use; recognising only the parenthesised one would report the other as an + unresolvable path. Checked BEFORE the annotation strip, because '(none)' + is itself a parenthesis and would strip to the empty string. + """ + return re.match(r'\(?none\b', value.strip(), re.IGNORECASE) is not None + def parse_status(content, slug): """Find the Status value for a given slug H2 in content.""" pattern = re.compile( @@ -321,11 +406,17 @@ if os.path.isdir(refs_dir): ref_fm, _ = parse_frontmatter(ref_content) ref_keys = parse_source_keys(ref_fm) if not ref_keys: + # An explicit `source_keys: []` is a deliberate declaration that + # the file is house-authored, and passes silently. The INFO is for + # files that never said either way. + if declares_empty_source_keys(ref_fm): + continue emit_info( f"No source_keys frontmatter", rel, "This references file has no source_keys — provenance cannot be verified. " - "Add source_keys frontmatter listing the slugs from references/sources.md that informed this file." + "Add source_keys frontmatter listing the slugs from references/sources.md that informed this file, " + "or declare an explicit 'source_keys: []' if the file is house-authored and has no external source." ) else: for slug in ref_keys: @@ -390,24 +481,51 @@ for slug in parse_h2_slugs(sources_content): f"The '## {slug}' entry has an unfilled Research doc value.", f"Set '- **Research doc:**' to a real path relative to repo root, or '(none)' if not applicable." ) - else: - # Check 7: Upstream forward — slug should appear in research doc - if repo_root and not rd_value.startswith("(none"): - rd_abs = os.path.join(repo_root, rd_value) - if os.path.isfile(rd_abs): + elif not research_doc_is_none(rd_value): + # Check 7: Upstream forward — slug should appear in research doc. + # Every path out of here that does NOT run the check says so out loud. + rd_path = strip_research_doc_annotation(rd_value) + if not repo_root: + emit_info( + f"Upstream checks skipped for '{slug}' — no repo root above the skill directory", + f"references/sources.md (## {slug})", + f"'{rd_value}' is a path relative to the repo root, but no ancestor of the skill directory contains a .git entry, " + f"so it cannot be resolved. Checks 7 and 8 did not run for this slug. " + f"Run this script against a skill inside a checkout." + ) + elif not rd_path: + emit_info( + f"Upstream checks skipped for '{slug}' — Research doc value names no path", + f"references/sources.md (## {slug})", + f"The Research doc value '{rd_value}' is entirely annotation — stripping the section marker leaves no path. " + f"Checks 7 and 8 did not run for this slug. " + f"Give the value a file path relative to the repo root, or record '(none)' if no research doc backs this entry." + ) + else: + rd_abs = os.path.join(repo_root, rd_path) + if not os.path.isfile(rd_abs): + emit_info( + f"Upstream checks skipped for '{slug}' — research doc '{rd_path}' does not exist", + f"references/sources.md (## {slug})", + f"'{rd_value}' resolves to '{rd_path}' relative to the repo root and no file is there. " + f"Checks 7 and 8 did not run for this slug, so nothing verified that the research doc still backs it. " + f"Point the value at one existing file — a brace expansion, a comma-separated list of paths, or a bare section title does not resolve — " + f"or record '(none)' if no research doc backs this entry." + ) + else: with open(rd_abs) as f: rd_content = f.read() rd_slugs = set(parse_h2_slugs(rd_content)) if slug not in rd_slugs: emit_info( - f"Slug '{slug}' not found as H2 in research doc '{rd_value}'", + f"Slug '{slug}' not found as H2 in research doc '{rd_path}'", f"references/sources.md (## {slug})", - f"The research doc '{rd_value}' does not have a '## {slug}' heading. " + f"The research doc '{rd_path}' does not have a '## {slug}' heading. " f"The provenance link may be imprecise — the slug name in sources.md may differ from the research doc's heading." ) # Track for Check 8 if rd_abs not in research_docs_seen: - research_docs_seen[rd_abs] = (rd_value, set()) + research_docs_seen[rd_abs] = (rd_path, set()) research_docs_seen[rd_abs][1].add(slug) # --- Check 8: Upstream reverse --- diff --git a/tests/test-adr0020-contract.sh b/tests/test-adr0020-contract.sh index daea839..1a2dc36 100755 --- a/tests/test-adr0020-contract.sh +++ b/tests/test-adr0020-contract.sh @@ -1,8 +1,7 @@ #!/usr/bin/env bash -# Regression test for the three STRUCTURAL claims the ADR-0020 gate family makes -# about itself. None of them was pinned anywhere before this file, and each one -# fails silently — which is the whole reason they need a test rather than a -# comment: +# Regression test for the STRUCTURAL claims the ADR-0020 gate family makes about +# itself. None of them was pinned anywhere before this file, and each one fails +# silently — which is the whole reason they need a test rather than a comment: # # 1. "ONE resolver, embedded VERBATIM in three scripts." The block between the # BEGIN/END markers is copied, not imported, because a cache-installed @@ -11,6 +10,10 @@ # one-line edit to a single copy is invisible: every constant-agreement # assertion in tests/test-skill-size-check.sh still passes, because the # CONSTANTS are not what drifted. +# 1b. The same claim, one directory over, for the Contributing-files parser +# embedded in both validate-provenance.sh copies. That one was worse: the +# agent-audit copy's docstring ASSERTED it was kept behaviourally identical +# to skill-audit's, and the two had already drifted. # 2. Both interpreter preflights, in all three scripts. python3 and PyYAML are # declared HARD dependencies precisely so a missing one cannot turn into a # vacuous pass, and the two are checked separately so the message names the @@ -93,6 +96,69 @@ else fi fi +# --------------------------------------------------------------------------- +# 1b. The shared Contributing-files parser is byte-identical in both copies +# --------------------------------------------------------------------------- +# Same defect class, one directory over. parse_contributing_files() is embedded +# in both validate-provenance.sh copies for the same reason the resolver is +# embedded three times, and until this assertion existed the agent-audit copy's +# docstring merely CLAIMED it was "kept behaviourally identical to skill-audit's +# copy" — an invariant nothing checked, and the two had already drifted into +# different spellings of the bullet loop. The parser decides whether checks 4, +# 5 and 8 run at all, so a one-sided edit disables a check in one script while +# every other test stays green. +echo "" +echo "--- the shared Contributing-files parser is byte-identical in both validate-provenance.sh copies ---" + +CF_BEGIN='# ===== BEGIN SHARED CONTRIBUTING-FILES PARSER =====' +CF_END='# ===== END SHARED CONTRIBUTING-FILES PARSER =====' +SKILL_PROV="$REPO_ROOT/plugins/kyberforge/.apm/skills/skill-audit/scripts/validate-provenance.sh" +AGENT_PROV="$REPO_ROOT/plugins/kyberforge/.apm/skills/agent-audit/scripts/validate-provenance.sh" + +CF_MARKERS_OK=true +for f in "$SKILL_PROV" "$AGENT_PROV"; do + if [[ ! -f "$f" ]]; then + fail "script not found: $f" + CF_MARKERS_OK=false + continue + fi + b="$(grep -cFx "$CF_BEGIN" "$f" || true)" + e="$(grep -cFx "$CF_END" "$f" || true)" + if [[ "$b" == "1" && "$e" == "1" ]]; then + pass "${f#"$REPO_ROOT/"} carries exactly one BEGIN and one END parser marker" + else + fail "${f#"$REPO_ROOT/"} has $b BEGIN and $e END parser markers, expected 1 and 1" + CF_MARKERS_OK=false + fi +done + +if ! $CF_MARKERS_OK; then + fail "skipping the parser byte-identity comparison — the marker pairs are not well-formed, so any extraction would measure the wrong span" +else + CF_HASHES=() + CF_LINECOUNTS=() + for f in "$SKILL_PROV" "$AGENT_PROV"; do + out="$TMPDIR_T/cfblock-$(echo "$f" | md5sum | cut -c1-8).txt" + sed -n "/^${CF_BEGIN}\$/,/^${CF_END}\$/p" "$f" > "$out" + CF_HASHES+=("$(md5sum < "$out" | cut -d' ' -f1)") + CF_LINECOUNTS+=("$(wc -l < "$out" | tr -d ' ')") + done + if [[ "${CF_HASHES[0]}" == "${CF_HASHES[1]}" ]]; then + pass "both copies hash to ${CF_HASHES[0]} (${CF_LINECOUNTS[0]} lines) — agreement by construction, not by coincidence" + else + fail "the shared Contributing-files parser has DRIFTED: skill-audit=${CF_HASHES[0]} (${CF_LINECOUNTS[0]} lines), agent-audit=${CF_HASHES[1]} (${CF_LINECOUNTS[1]} lines). Edit one copy, then paste it over the other." + fi + # Two identical EMPTY spans would hash equal and assert nothing, exactly as + # for the resolver above. The parser block is ~93 lines; 40 is a floor low + # enough never to need maintenance and high enough that a gutted block — or + # one reduced to its docstring — cannot sneak past. + if [[ "${CF_LINECOUNTS[0]}" -gt 40 ]]; then + pass "the extracted parser block is ${CF_LINECOUNTS[0]} lines — the comparison is over real content, not an empty span" + else + fail "the extracted parser block is only ${CF_LINECOUNTS[0]} lines — two identical empty spans would compare equal and assert nothing" + fi +fi + # --------------------------------------------------------------------------- # 2. Both interpreter preflights, in all three scripts # ---------------------------------------------------------------------------