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:
@@ -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):
|
||||
|
||||
@@ -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`":
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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`":
|
||||
|
||||
Reference in New Issue
Block a user