#!/usr/bin/env bash # lib-contributing-files.sh — SOURCED, never executed. # # The shared Contributing-files parser, as ONE copy for this skill. Both of # validate-provenance.sh's modes compose it into the Python program they run. # Before the merge it was embedded twice. The two copies drifted once # (484357a) into different spellings of the bullet loop — behaviourally # identical, but unchecked while a docstring asserted they matched — and were # re-unified at 598a7c3, so they were byte-identical when ADR-0025 merged them. # tests/test-adr0020-contract.sh pins this file as the sole authority so that # a second copy cannot reappear. # # Held in a shell variable filled from a QUOTED here-doc for the same reason as # lib-boundary-resolver.sh's block: nothing inside is expanded, so the text # between the two markers below stays byte-identical to the copies the contract # test reads, and the markers stay on lines of their own, at column 0, exactly # once each, so the same sed range extracts the same span. # # The here-doc is consumed by the `read` BUILTIN rather than by `$(cat <<...)`. # validate-provenance.sh sources this file before either mode's python3 # preflight, so a `cat` here made coreutils a hard dependency ahead of python3: # on a PATH with neither, the script exited 127 naming `cat` instead of reaching # the preflight that names python3. `read -r -d ''` reads to a NUL that never # arrives and so returns non-zero at EOF — hence the `|| true` — and it keeps the # last line's newline, which the joining newline in the caller would otherwise # double — hence the single strip after the delimiter. It removes exactly ONE # newline, never a run: blank lines at the end of a chunk are program text, and # stripping every trailing newline deleted them. The here-doc itself is # unchanged. # # Consumed by: validate-provenance.sh (both modes), via # $KYBERFORGE_CONTRIBUTING_FILES_PY. # shellcheck shell=bash # shellcheck disable=SC2034 IFS='' read -r -d '' KYBERFORGE_CONTRIBUTING_FILES_PY <<'KYBERFORGE_CONTRIBUTING_FILES' || true # ===== BEGIN SHARED CONTRIBUTING-FILES PARSER ===== # ONE parser, and since ADR-0025 exactly one copy of it: this file, sourced by # validate-provenance.sh for both the skill and the agent flow. It used to be # embedded verbatim in skill-audit's and agent-audit's separate # validate-provenance.sh copies, because a cache-installed plugin's scripts # cannot read files outside their own plugin directory and no single file was # reachable by both skills. Merging those skills removed that constraint: two # files in ONE skill directory can source a third. The two copies had drifted # once before (cosmetically, and re-unified before the merge) while a docstring # claimed they had not, which is why tests/test-adr0020-contract.sh now pins # this file as the SOLE authority — that it exists, that validate-provenance.sh # sources it, and that nothing anywhere has re-inlined the parser. Do not paste # this block into a caller. # # Before the contract test pinned it, the agent-side copy's docstring merely # ASSERTED the two copies were "behaviourally identical" and nothing checked # it — which is how the two diverged spellings of the bullet loop went # unnoticed at 484357a. # # 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 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)', re.MULTILINE | re.DOTALL ) m = pattern.search(content) 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] 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 ===== KYBERFORGE_CONTRIBUTING_FILES KYBERFORGE_CONTRIBUTING_FILES_PY="${KYBERFORGE_CONTRIBUTING_FILES_PY%$'\n'}"