#!/usr/bin/env bash set -euo pipefail usage() { cat < Validate that a skill's sources provenance chain is complete and internally consistent. Arguments: skill-dir Path to the skill directory to validate. Exit codes: 0 All checks passed (or nothing to validate) 1 One or more checks failed 2 Usage error, or the argument is not a skill directory An exit code of 2 is NOT a finding. SKILL.md tells the auditor to surface a non-zero exit as findings, so a usage error leaving exit 1 with nothing on stdout was indistinguishable from a clean-but-failing run. Environment and argument problems exit 2; only real findings exit 1. 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; an explicit 'source_keys: []' declares the file house-authored and passes silently) 4 Contributing files listed in sources.md exist on disk. An explicit '(none)' skips silently; a Contributing files block this parser cannot read is reported as an INFO saying checks 4 and 5 did not run, never skipped silently. 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). 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 Checks 7 and 8 apply ONLY when the Research doc value names a research SOURCE INDEX — a file whose basename is sources.md, whose H2 headings ARE source slugs. A Research doc pointing at a topic document is reported as an INFO saying the two checks are not applicable, and every other reason they do not run is announced the same way. EOF } if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then usage exit 0 fi # Usage and environment problems exit 2, findings exit 1. See the usage text # above for why the two must not share a code. This is a deliberate divergence # from validate.sh, which has no 2 tier: validate.sh always prints PASS lines, # so a usage error there is visibly not a findings report. This script prints # NOTHING on a clean run, so exit 1 plus empty stdout was the only signal a # caller got either way. if [[ $# -lt 1 ]]; then echo "Error: skill-dir is required." >&2 echo "" >&2 usage >&2 exit 2 fi # Extra positional arguments were silently dropped, so a typo'd flag or a second # path looked like it had been honoured. if [[ $# -gt 1 ]]; then echo "Error: expected exactly one argument, got $#: $*" >&2 echo "" >&2 usage >&2 exit 2 fi # python3 is a HARD dependency. Without this preflight a missing interpreter # produced 'line NN: python3: command not found' and exit 127 — an exit code no # caller maps to anything, from a message that names this script's line number # rather than the missing dependency. if ! command -v python3 > /dev/null 2>&1; then echo "Error: python3 is required but was not found on PATH." >&2 echo " Why: skipping the provenance checks entirely would be a vacuous pass." >&2 echo " Fix: install python3 (pre-commit itself is a Python application, so it is almost certainly already present)." >&2 exit 2 fi # A path that is not a directory, or a directory that is not a skill, used to # reach the Python body, find no sources.md and no source_keys, take the # "nothing to validate" early exit and report exit 0 with no output — which # references/validation-scripts.md explicitly told the auditor to read as a # pass. A typo'd target was therefore indistinguishable from a clean skill. # vale-wrap.sh hard-errors on a nonexistent path for exactly this reason. if [[ ! -d "$1" ]]; then echo "Error: not a directory: $1" >&2 echo " Why: a nonexistent target would otherwise report a silent pass." >&2 echo " Fix: pass the path of the skill directory to validate." >&2 exit 2 fi if [[ ! -f "$1/SKILL.md" ]]; then echo "Error: not a skill directory (no SKILL.md): $1" >&2 echo " Why: a directory with no SKILL.md has no provenance chain to validate, and reporting that as a pass hides the wrong-target mistake." >&2 echo " Fix: pass the skill directory itself, not its parent or its references/ subdirectory." >&2 exit 2 fi python3 -u - "$1" <<'PYTHON' import sys import os import re # Output is UTF-8 for the same reason input is: under LC_ALL=C the streams # default to ASCII, and every finding this script prints contains an em dash. # Pinning only the reads moved the crash from the read to the write — a # UnicodeEncodeError inside print_findings(), which loses the whole report # after all the checks have already run. for _stream in (sys.stdout, sys.stderr): try: _stream.reconfigure(encoding='utf-8') except AttributeError: # pragma: no cover — Python < 3.7 pass skill_dir = os.path.abspath(sys.argv[1]) sources_md_path = os.path.join(skill_dir, "references", "sources.md") refs_dir = os.path.join(skill_dir, "references") # --- Helpers --- # The trailing character class used to be CONSUMING — `[^`\n]` — so a # `FILL IN:` at end of line matched nothing and escaped checks 1 and 6 # entirely. `- **Description:** FILL IN:` is the most likely spelling of a # half-written entry, and it was the one spelling the placeholder gate could # not see. The exclusion it was really expressing is "not inside backticks", # which a lookahead states without eating a character. PLACEHOLDER_RE = re.compile(r'(? "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] 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 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 or None # ===== END SHARED CONTRIBUTING-FILES PARSER ===== def parse_research_docs(content, slug): """Every Research doc value under a given slug H2, in document order. The caller uses the first and reports the rest. Returning only the first — what this did before — meant a second '- **Research doc:**' line in one entry was silently ignored, so an author who added a doc rather than replacing one got checks 7 and 8 run against the old path and no hint that the new one was never looked at. """ pattern = re.compile( r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)', re.MULTILINE | re.DOTALL ) m = pattern.search(content) if not m: return [] block = m.group(1) return [v.strip() for v in re.findall(r'^\- \*\*Research doc:\*\* (.+)$', block, re.MULTILINE)] # 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 # The Status value is what gates check 8, so every spelling this parser fails # to read is a check that does not run. Two were unreadable: # # - **Status:** `extracted` — partial fetch (a trailing note) # **Status:** (the bullet form, the same # - `extracted` shape parse_contributing_files # already accepts) # # Both used to parse to a string that compared unequal to "`extracted`", and # check 8 skipped on that inequality without a word. Returning the BACKTICKED # TOKEN — not the whole line — is what makes the trailing note harmless, and it # lets the caller name the actual status when it announces a skip. STATUS_TOKEN_RE = re.compile(r'^`([^`]*)`') def parse_status(content, slug): """Find the Status value for a given slug H2 in content. Returns the status with its backticks stripped ('extracted', 'referenced', 'no content extracted'), or None when the entry has no Status line. """ pattern = re.compile( r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)', re.MULTILINE | re.DOTALL ) m = pattern.search(content) if not m: return None block = m.group(1) raw = None st_m = re.search(r'^\- \*\*Status:\*\* (.+)$', block, re.MULTILINE) if st_m: raw = st_m.group(1).strip() else: st_m = re.search(r'^\*\*Status:\*\*\s*$', block, re.MULTILINE) if not st_m: return None for line in block[st_m.end():].splitlines(): line = line.strip() if not line: continue if not line.startswith("- "): break raw = line[2:].strip() break if raw is None: return None token = STATUS_TOKEN_RE.match(raw) return token.group(1).strip() if token else raw def find_repo_root(start_dir): """Walk up from start_dir until we find a directory containing .git.""" current = start_dir while True: if os.path.exists(os.path.join(current, ".git")): return current parent = os.path.dirname(current) if parent == current: return None current = parent findings = [] has_fail = False # A finding identical in every field is the same finding, and the same file is # now reached by more than one check — the walk that looks for source_keys and # check 3 both read every references/*.md, so an unreadable one would otherwise # be reported twice with the same words. Distinct findings about the same file # still both appear. def _record(entry): if entry not in findings: findings.append(entry) def emit_fail(desc, fpath, why, fix): global has_fail has_fail = True _record(("FAIL", desc, fpath, why, fix, None)) def emit_info(desc, fpath, note): _record(("INFO", desc, fpath, None, None, note)) def print_findings(): for entry in findings: kind = entry[0] desc = entry[1] fpath = entry[2] why = entry[3] fix = entry[4] note = entry[5] if kind == "FAIL": print(f"FAIL {desc} — {fpath}") print(f" Why: {why}") print(f" Fix: {fix}") print() else: print(f"INFO {desc} — {fpath}") print(f" Note: {note}") print() # --- Scan for any file with source_keys --- def emit_unreadable(rel, exc): """Report a file this script cannot decode. Never a silent skip.""" emit_fail( f"File is {exc}", rel, f"'{rel}' cannot be decoded, so its frontmatter — and any source_keys in it — " f"cannot be read. This used to be swallowed by a bare 'except Exception: return False', " f"which reported the unreadable file as having no source_keys and therefore as clean.", f"Re-save '{rel}' as UTF-8." ) def file_has_source_keys(fpath, rel): try: content = read_text(fpath) except EncodingError as exc: emit_unreadable(rel, exc) return False fm, _ = parse_frontmatter(content) if fm is None: return False return bool(parse_source_keys(fm)) def find_files_with_source_keys(): """Return list of (relative_path, abs_path) for all skill files with source_keys.""" results = [] for root, dirs, files in os.walk(skill_dir): # Skip hidden dirs dirs[:] = [d for d in dirs if not d.startswith('.')] for fname in sorted(files): if fname.endswith('.md'): abs_path = os.path.join(root, fname) rel = os.path.relpath(abs_path, skill_dir) if file_has_source_keys(abs_path, rel): results.append((rel, abs_path)) return results sources_md_exists = os.path.isfile(sources_md_path) files_with_source_keys = find_files_with_source_keys() # Early exit: nothing to validate. The scan itself can raise a finding — an # unreadable file — so print before leaving; the clean case still prints # nothing and exits 0. if not sources_md_exists and not files_with_source_keys: print_findings() sys.exit(1 if has_fail else 0) # Load sources.md if it exists sources_content = None if sources_md_exists: try: sources_content = read_text(sources_md_path) except EncodingError as exc: emit_unreadable("references/sources.md", exc) print_findings() sys.exit(1) sources_slugs = set(parse_h2_slugs(sources_content)) else: sources_slugs = set() # --- Check 0: source_keys without sources.md --- if not sources_md_exists: for rel, abs_path in files_with_source_keys: emit_fail( f"source_keys declared but references/sources.md is absent", rel, "source_keys references research provenance that has no sources index to validate against.", "Create references/sources.md with an H2 entry for each slug referenced by source_keys." ) print_findings() sys.exit(1) # --- Check 1: FILL IN: placeholders in sources.md --- for line in sources_content.splitlines(): if PLACEHOLDER_RE.search(line): emit_fail( "Unfilled FILL IN: placeholder", "references/sources.md", "sources.md contains an unfilled placeholder, meaning provenance is incomplete.", "Replace all 'FILL IN:' values in references/sources.md with real content." ) break # --- Check 2: source_keys in SKILL.md → slug exists in sources.md --- skill_md_path = os.path.join(skill_dir, "SKILL.md") if os.path.isfile(skill_md_path): try: skill_content = read_text(skill_md_path) except EncodingError as exc: emit_unreadable("SKILL.md", exc) skill_content = "" skill_fm, _ = parse_frontmatter(skill_content) skill_source_keys = parse_source_keys(skill_fm) for slug in skill_source_keys: if slug not in sources_slugs: emit_fail( f"source_keys slug '{slug}' not found in sources.md", "SKILL.md", f"SKILL.md declares '{slug}' as a source but there is no '## {slug}' heading in references/sources.md.", f"Add '## {slug}' entry to references/sources.md or remove '{slug}' from SKILL.md source_keys." ) # --- Check 3: source_keys in references/*.md → slug exists in sources.md (INFO if no source_keys) --- # os.walk, not os.listdir: find_files_with_source_keys() above already walks # references/ recursively, so a source_keys-bearing file in # references// was collected there — and then never validated here, # because the flat listdir could not see it. The two halves of the same check # disagreed about which files exist. if os.path.isdir(refs_dir): ref_paths = [] for root, dirs, files in os.walk(refs_dir): dirs[:] = sorted(d for d in dirs if not d.startswith('.')) for fname in sorted(files): if not fname.endswith('.md'): continue fpath = os.path.join(root, fname) if os.path.relpath(fpath, refs_dir) == "sources.md": continue ref_paths.append(fpath) for fpath in ref_paths: rel = os.path.relpath(fpath, skill_dir) try: ref_content = read_text(fpath) except EncodingError as exc: emit_unreadable(rel, exc) continue 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, " "or declare an explicit 'source_keys: []' if the file is house-authored and has no external source." ) else: for slug in ref_keys: if slug not in sources_slugs: emit_fail( f"source_keys slug '{slug}' not found in sources.md", rel, f"'{rel}' declares '{slug}' as a source but there is no '## {slug}' heading in references/sources.md.", f"Add '## {slug}' entry to references/sources.md or remove '{slug}' from {rel} source_keys." ) # --- Checks 4, 5, 6, 7, 8: Per-slug checks in sources.md --- repo_root = find_repo_root(skill_dir) # Collect all research doc paths we'll check (for Check 8) research_docs_seen = {} # abs_path → (rel_path, slugs referencing it, content) # Every per-slug parser below — parse_contributing_files, parse_research_docs, # parse_status — locates its block with pattern.search(), so a slug written # twice resolves to the FIRST block every time. Iterating the raw heading list # therefore checked the first block's fields twice and the second block's # never: a duplicated slug is half-validated, and looked fully validated. The # duplicate is announced and the repeat visit dropped. all_slugs = parse_h2_slugs(sources_content) unique_slugs = [] for _slug in all_slugs: if _slug in unique_slugs: continue unique_slugs.append(_slug) _count = all_slugs.count(_slug) if _count > 1: emit_info( f"Duplicate '## {_slug}' entry in sources.md — only the first block is checked", f"references/sources.md (## {_slug})", f"'## {_slug}' appears {_count} times. Every field parser here takes the first match, so the " f"second and later blocks' Contributing files, Research doc and Status are never validated — " f"checks 4, 5, 6, 7 and 8 did not run for them. " f"Merge the blocks into one entry, or give each a distinct slug and reference it from source_keys." ) for slug in unique_slugs: # Checks 4 and 5: Contributing files exist, and back-reference the slug. # `[]` and None are NOT the same answer here. `[]` is the author writing # "(none)" — there is nothing to check and the skip is correct. None is a # Contributing-files block this parser cannot read, and skipping THAT # silently disables both checks on the one entry least likely to be right, # which is the failure mode parse_contributing_files' own docstring warns # about. Say so out loud instead, the same way an unresolvable Research doc # value does. cf_files = parse_contributing_files(sources_content, slug) if cf_files is None: emit_info( f"Contributing-file checks skipped for '{slug}' — the Contributing files block could not be parsed", f"references/sources.md (## {slug})", f"The '## {slug}' entry has no Contributing files list this parser can read — a missing field, a bare heading, '*' bullets, a numbered list, or prose all read as unparsable rather than as an empty declaration. " f"Checks 4 and 5 did not run for this slug, so nothing verified that its contributing files exist or name it back. " f"Write the value as '- **Contributing files:** ', or as a '**Contributing files:**' heading followed by '- ' bullets — " f"or record '(none)' if this source contributed no files." ) elif cf_files: for cf_rel in cf_files: cf_abs = os.path.join(skill_dir, cf_rel) if not os.path.isfile(cf_abs): emit_fail( f"Contributing file '{cf_rel}' does not exist", f"references/sources.md (## {slug})", f"sources.md claims '{cf_rel}' was contributed to by slug '{slug}' but the file does not exist.", f"Create '{cf_rel}' relative to the skill directory, or correct the path in sources.md." ) else: # Check 5: Bidirectional — file should list slug in its source_keys # Skip sources.md itself if cf_rel == "references/sources.md": continue try: cf_content = read_text(cf_abs) except EncodingError as exc: emit_unreadable(cf_rel, exc) continue cf_fm, _ = parse_frontmatter(cf_content) cf_keys = parse_source_keys(cf_fm) if slug not in cf_keys: emit_fail( f"Contributing file '{cf_rel}' does not list '{slug}' in its source_keys", f"references/sources.md (## {slug})", f"sources.md says '{cf_rel}' was informed by '{slug}', but '{cf_rel}' does not declare '{slug}' in its source_keys frontmatter.", f"Add '{slug}' to the source_keys frontmatter of '{cf_rel}'." ) # Check 6: Research doc field required rd_values = parse_research_docs(sources_content, slug) if len(rd_values) > 1: emit_info( f"Multiple '- **Research doc:**' lines for '{slug}' — only the first is used", f"references/sources.md (## {slug})", f"The '## {slug}' entry has {len(rd_values)} Research doc lines; checks 7 and 8 ran against the first " f"('{rd_values[0]}') and never looked at the rest. " f"Keep one Research doc line per entry — if a slug genuinely came from two documents, split it into two slugs, " f"or name the extra document inside the first value's annotation where it is at least visible." ) rd_value = rd_values[0] if rd_values else None if rd_value is None: emit_fail( f"Research doc field missing", f"references/sources.md (## {slug})", f"The '## {slug}' entry in sources.md has no '- **Research doc:**' line.", f"Add '- **Research doc:** ' to the '## {slug}' entry in references/sources.md." ) elif rd_value == "" or PLACEHOLDER_RE.search(rd_value): emit_fail( f"Research doc field is empty or placeholder", f"references/sources.md (## {slug})", 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." ) 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." ) elif os.path.basename(rd_path) != "sources.md": # Checks 7 and 8 both assume the Research doc is a research # SOURCE INDEX — a sources.md whose H2 headings ARE source # slugs. 30 of the 121 corpus entries point instead at a TOPIC # DOCUMENT (remotes.md, gitflow.md, api-reference.md), whose # H2s are headings like '## Core Philosophy'. A slug can never # match one, so check 7 reported all 30 as "slug not found" — # every one a false positive — and check 8, aimed at documents # that carry no '- **Status:**' line at all, was saved from a # matching flood of false FAILs only by an UNANNOUNCED skip on # that missing status. The premise, not the corpus, was wrong. # # A topic-document reference is a legitimate, useful value; it # just is not something these two checks can verify. Say that # once, out loud, instead of failing 30 entries for it. emit_info( f"Upstream checks not applicable for '{slug}' — research doc '{rd_path}' is a topic document, not a source index", f"references/sources.md (## {slug})", f"Checks 7 and 8 match slugs against the H2 headings of a research source index — a file named 'sources.md', " f"where each H2 IS a source slug. '{os.path.basename(rd_path)}' is a topic document, so its H2s are section " f"headings and no slug will ever match one. Checks 7 and 8 did not run for this slug. " f"This needs no fix: point the value at the research corpus's own sources.md only if you want the " f"provenance link machine-verified." ) else: try: rd_content = read_text(rd_abs) except EncodingError as exc: emit_info( f"Upstream checks skipped for '{slug}' — research doc '{rd_path}' is {exc}", f"references/sources.md (## {slug})", f"'{rd_path}' could not be decoded, so checks 7 and 8 did not run for this slug. " f"Re-save the research doc as UTF-8." ) continue 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_path}'", f"references/sources.md (## {slug})", 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. The content is carried with the entry so # check 8 reuses this read rather than decoding the file a # second time, with a second chance to fail differently. if rd_abs not in research_docs_seen: research_docs_seen[rd_abs] = (rd_path, set(), rd_content) research_docs_seen[rd_abs][1].add(slug) # --- Check 8: Upstream reverse --- for rd_abs, (rd_rel, known_slugs, rd_content) in research_docs_seen.items(): for rd_slug in parse_h2_slugs(rd_content): # 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 the research doc explicitly records no contributing files if rd_cf == []: continue # Skip if status is not `extracted` — and say so when the skip is what # kept the slug out of the FAIL below. A status of `referenced` or # `no content extracted` is a real reason not to demand the slug, but # it was applied in silence, so an entry that should have been in # sources.md and a status line nobody had updated produced the same # output: nothing. Only a MATERIAL skip is announced; when the slug is # already in sources.md the check passes either way and there is no # fail-open to disclose. if rd_status != "extracted": if rd_slug not in sources_slugs: shown = f"`{rd_status}`" if rd_status else "absent" emit_info( f"Check 8 skipped for research-doc slug '{rd_slug}' — its Status is {shown}, not `extracted`", f"{rd_rel} (## {rd_slug})", f"'{rd_rel}' has '## {rd_slug}' with contributing files but Status {shown}, and this skill's " f"sources.md has no '## {rd_slug}' entry. Check 8 only demands an entry for an `extracted` slug, " f"so it did not run here. If that status is stale — the content was extracted and the line was never " f"updated — this skill is missing a source entry; if it is accurate, nothing needs doing." ) continue # This slug should be in sources.md if rd_slug not in sources_slugs: emit_fail( f"Research doc slug '{rd_slug}' missing from skill sources.md", f"references/sources.md", f"The research doc '{rd_rel}' has '## {rd_slug}' with status `extracted` and contributing files, " f"but this skill's sources.md has no '## {rd_slug}' entry.", f"Add '## {rd_slug}' to references/sources.md or mark it as '(none)' in the research doc's Contributing files." ) print_findings() sys.exit(1 if has_fail else 0) PYTHON