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):

View File

@@ -94,8 +94,24 @@ def parse_h2_slugs(content):
return re.findall(r'^## (.+)$', content, re.MULTILINE)
def parse_contributing_files(content, slug):
"""Find the Contributing files value for a given slug H2 in content."""
# Find the H2 block for slug, then look for Contributing files line
"""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:
- **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.
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.
"""
pattern = re.compile(
r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)',
re.MULTILINE | re.DOTALL
@@ -104,10 +120,40 @@ def parse_contributing_files(content, slug):
if not m:
return None
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]
# 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
return cf_m.group(1).strip()
rest = block[cf_m.end():]
files = []
for line in rest.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):
"""Find the Research doc value for a given slug H2 in content."""
@@ -300,10 +346,8 @@ research_docs_seen = {} # abs_path → set of slugs in sources.md that referenc
for slug in parse_h2_slugs(sources_content):
# Check 4: Contributing files exist
cf_value = parse_contributing_files(sources_content, slug)
if cf_value and not cf_value.startswith("(none"):
# Split by comma
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(skill_dir, cf_rel)
if not os.path.isfile(cf_abs):
@@ -374,8 +418,8 @@ for rd_abs, (rd_rel, known_slugs) in research_docs_seen.items():
# Parse this slug's Contributing files and Status in the research doc
rd_cf = parse_contributing_files(rd_content, rd_slug)
rd_status = parse_status(rd_content, rd_slug)
# Skip if contributing files start with (none
if rd_cf and rd_cf.startswith("(none"):
# Skip if the research doc explicitly records no contributing files
if rd_cf == []:
continue
# Skip if status is not `extracted`
if rd_status != "`extracted`":

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):

View File

@@ -94,8 +94,24 @@ def parse_h2_slugs(content):
return re.findall(r'^## (.+)$', content, re.MULTILINE)
def parse_contributing_files(content, slug):
"""Find the Contributing files value for a given slug H2 in content."""
# Find the H2 block for slug, then look for Contributing files line
"""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:
- **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.
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.
"""
pattern = re.compile(
r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)',
re.MULTILINE | re.DOTALL
@@ -104,10 +120,40 @@ def parse_contributing_files(content, slug):
if not m:
return None
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]
# 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
return cf_m.group(1).strip()
rest = block[cf_m.end():]
files = []
for line in rest.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):
"""Find the Research doc value for a given slug H2 in content."""
@@ -300,10 +346,8 @@ research_docs_seen = {} # abs_path → set of slugs in sources.md that referenc
for slug in parse_h2_slugs(sources_content):
# Check 4: Contributing files exist
cf_value = parse_contributing_files(sources_content, slug)
if cf_value and not cf_value.startswith("(none"):
# Split by comma
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(skill_dir, cf_rel)
if not os.path.isfile(cf_abs):
@@ -374,8 +418,8 @@ for rd_abs, (rd_rel, known_slugs) in research_docs_seen.items():
# Parse this slug's Contributing files and Status in the research doc
rd_cf = parse_contributing_files(rd_content, rd_slug)
rd_status = parse_status(rd_content, rd_slug)
# Skip if contributing files start with (none
if rd_cf and rd_cf.startswith("(none"):
# Skip if the research doc explicitly records no contributing files
if rd_cf == []:
continue
# Skip if status is not `extracted`
if rd_status != "`extracted`":