fix(gates): parse the bullet form of Contributing files

validate-provenance.sh matched Contributing files only as a single inline
line beginning "- **Contributing files:**". Seven skills write it as a
bare "**Contributing files:**" heading above a bullet list, so
parse_contributing_files returned None and checks 4 (contributing file
exists) and 5 (bidirectional source_keys) silently verified nothing on
git-branches, git-remotes, git-submodules, git-workflow, git-worktrees,
gitea-files and gitea-releases.

Those are among the skills this branch changed most — git-branches alone
gained five reference files — and the retrofit's mandatory sources.md
collateral went in unchecked. Demonstrated rather than argued: planting a
nonexistent contributing path in git-remotes yields 0 findings under the
old parser and 1 FAIL under the new one.

Both forms are now accepted. The bullet form is parsed per bullet rather
than by splitting a joined value, because its per-file notes contain
commas that would otherwise be read as path separators. The return type
becomes a list of note-stripped paths, with "(none)" as an empty list and
an absent entry as None, so the two callers no longer re-split a string.

Applied to agent-audit's copy as well. No agent ships a sources.md today,
so it is latent there, but it is the same defect.

This is a third gate blind spot alongside #117 and #118, and was unfiled.
One real defect surfaced immediately and is fixed separately.

Refs #99
This commit is contained in:
2026-08-30 20:51:56 +00:00
parent 164948a0bc
commit 484357a3b9
4 changed files with 182 additions and 26 deletions

View File

@@ -131,6 +131,15 @@ def parse_h2_slugs(content):
return re.findall(r'^## (.+)$', content, re.MULTILINE)
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.
"""
pattern = re.compile(
r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)',
re.MULTILINE | re.DOTALL
@@ -139,10 +148,36 @@ def parse_contributing_files(content, slug):
if not m:
return None
block = m.group(1)
def strip_note(entry):
return re.sub(r'\s*\(.*$', '', entry).strip()
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]
cf_m = re.search(r'^\*\*Contributing files:\*\*\s*$', block, re.MULTILINE)
if not cf_m:
return None
return cf_m.group(1).strip()
files = []
for line in block[cf_m.end():].splitlines():
line = line.strip()
if not line:
if files:
break
continue
if not line.startswith("- "):
break
entry = line[2:].strip()
if entry.startswith("(none"):
return []
entry = strip_note(entry)
if entry:
files.append(entry)
return files
def parse_research_doc(content, slug):
pattern = re.compile(
@@ -242,9 +277,8 @@ for fpath, keys in [(agent_file, given_keys)]:
# --- Checks 3, 4, 5: Per-slug checks in sources.md ---
for slug in parse_h2_slugs(sources_content):
# Check 3: Contributing files exist (paths relative to plugin root)
cf_value = parse_contributing_files(sources_content, slug)
if cf_value and not cf_value.startswith("(none"):
cf_files = [p.strip() for p in cf_value.split(",") if p.strip()]
cf_files = parse_contributing_files(sources_content, slug)
if cf_files:
for cf_rel in cf_files:
cf_abs = os.path.join(plugin_root, cf_rel)
if not os.path.isfile(cf_abs):