diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index caa074e..e4da5c4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -220,6 +220,21 @@ repos: pass_filenames: false always_run: true + - id: check-provenance-corpus + name: Check provenance across the skill corpus + description: Run factory-audit's validate-provenance.sh over every plugins/*/.apm/skills/*/ that has references/sources.md and fail on any FAIL (ADR-0028, #121) + entry: bash scripts/check-provenance-corpus.sh + language: system + stages: [pre-push] + pass_filenames: false + always_run: true + # Nothing else runs validate-provenance.sh over the real corpus -- + # check-scope-walkup-sync exercises it against synthetic fixtures only -- + # so ADR-0028's FAIL tier for a Research doc mismatch would be inert + # without this caller. The skill set is globbed, not counted, and + # discovering zero skills is an error (exit 2), not a pass. Needs no + # network; needs python3, which the validator's own preflight names. + - id: check-skill-version-bump name: Check changed skills bump metadata.version description: On every push, fail if a skill directory changed (tests/ excluded) since the merge-base with main without its SKILL.md metadata.version rising above both that merge-base's and main's tip's (ADR-0022) diff --git a/CONTEXT.md b/CONTEXT.md index 5bbbd08..62ad8ec 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -81,6 +81,13 @@ topic docs and a `sources.md`; the author skill records which sources informed w and internally consistent. _Avoid_: sources, citations, attribution +**Research registry**: +A plugin's research `sources.md` (e.g. `plugins/git/docs/research/docs/git/sources.md`), whose `## H2` +headings are the source slugs. A skill's `Research doc:` field names exactly one, and +`factory-audit` resolves each entry's slug against it. An entry with no registry declares +`Research doc: none` and names what it was actually drawn from in `Basis:`. +_Avoid_: bare "research doc" (the noun; `Research doc:` is the field name), sources file, topic doc (a topic doc is a digest of sources, not the registry) + ### Governance **HITL** (human-in-the-loop): diff --git a/docs/adr/0004-skill-audit-info-finding-level.md b/docs/adr/0004-skill-audit-info-finding-level.md index e6610f0..eaea053 100644 --- a/docs/adr/0004-skill-audit-info-finding-level.md +++ b/docs/adr/0004-skill-audit-info-finding-level.md @@ -5,6 +5,9 @@ merged into `factory-audit`, which dispatches to a skill flow and an agent flow `skill-audit` below as `factory-audit`'s skill flow. The decision itself is unchanged — ADR-0025 carried every audit criterion, tier and finding level across as-is. +**Amended by ADR-0028 (2026-09-21).** INFO stays for a check that cannot run. A check that ran and +found a mismatch in `Research doc:` is now a FAIL, so INFO no longer covers it. + `skill-audit` shipped with two finding levels: FAIL (blocks shipping) and SUGGESTION (optional improvement). Provenance validation introduced observations that are worth surfacing but not actionable: a `references/*.md` file with no diff --git a/docs/adr/0028-research-doc-names-the-research-registry.md b/docs/adr/0028-research-doc-names-the-research-registry.md new file mode 100644 index 0000000..5d89350 --- /dev/null +++ b/docs/adr/0028-research-doc-names-the-research-registry.md @@ -0,0 +1,143 @@ +# `Research doc:` names one Research registry; entries without one declare `none` and a `Basis:` + +**Status: accepted (2026-09-21).** Resolves #121. Extends ADR-0004's INFO level: it keeps INFO for +the case where a check cannot run and promotes the case where it ran and found a mismatch. + +Each entry in a skill's `references/sources.md` carries a `Research doc:` field. The spec +(`skill-author/references/create.md`) says it names the plugin's research `sources.md`, the file +whose `## H2` headings are the source slugs. The corpus did something else: 29 of 30 mismatched +entries pointed at a research topic doc annotated `(whole-document reference)`, and 6 values were not +a single path (comma-separated lists and shell brace expansion, plus a semicolon pair in +`gitea-releases`). Checks 7 and 8 of `validate-provenance.sh` look the slug up as an H2 in the named +file, so 36 entries reported INFO and nothing failed. Measured by running the script over all 38 skill +directories (27 with a `references/sources.md`, 11 without), since nothing else runs it over the corpus. + +We decided that `Research doc:` names exactly one **Research registry** (the term is in +`CONTEXT.md`), as the spec always said. Slug-to-H2 lookup in the registry is the only provenance link +that can be verified deterministically; a topic doc has no per-source H2 to check against. A link to +the topic doc that digested a source stays as free-text annotation and is not checked. + +## Considered options + +**Q1 — what `Research doc:` refers to.** + +- **(a) The Research registry (chosen).** Check 7 stays as designed (check 8 is retired, see Q6); the + 29 entries repoint mechanically. +- **(b) The topic docs a source fed into (rejected).** Matches what the authors wrote, and is arguably + the more useful pointer for a reader. Rejected because it changes the spec and the checker, and the + slug check has nothing to run against. +- **(c) Both, as two fields (rejected).** Doubles the schema for a link nobody gates on. + +**Q2 — how an entry with no registry declares that honestly.** + +- **(a) `Research doc: none` plus a `Basis:` field (chosen).** `Basis:` takes repeated bullets of + repo paths (ADRs, `core/instructions/*.md`, a live example) and is checked for existence only. + `research_doc_is_none` already parses `none`, and `git-workflow` already writes it. Same shape as + #111: there was no honest way to declare the truthful thing. +- **(b) A non-corpus path stays legal in `Research doc:` (rejected).** Leaves one field meaning two + things depending on its value, and the INFO it produces can never be cleared. +- **(c) Move non-corpus entries out of `sources.md` (rejected).** A larger restructure than the + issue warrants. + +Lists are not needed under Q1(a): the four `pc-author` and `pc-run` brace expansions are one +registry, and the `gitea-releases` pair collapses to one registry. Brace expansion and semicolon +pairs are rejected outright, since nothing expands them in a markdown field. + +**Q3 — tier once the grammar is settled.** + +- **(b) FAIL when the path resolves and check 7 finds a mismatch; INFO when the path does not + resolve (chosen).** Check 8 is not part of this: see Q6. A topic doc in `Research doc:` is now + simply wrong and is a FAIL. An + unresolvable path stays INFO because `skill-file-structure.md` treats `sources.md` pointers as + development-time, and a deployed copy of a skill outside this repo will not have the research docs. + This repo's own corpus is audited from the authoring source, where every path resolves. +- **(a) Everything stays INFO (rejected).** Under ADR-0004 INFO implies no action, which is how 36 + mismatches went unnoticed. +- **(c) Everything FAIL (rejected).** Fails a correctly-provenanced skill audited from a deployed + copy, which the file-structure exemption exists to prevent. + +**Q4 — enforcement.** A corpus-wide sweep gate lands in the same change: a test or pre-push hook that +runs `validate-provenance.sh` over every `plugins/*/.apm/skills/*/` and fails on any FAIL. Deferring it +was rejected because without a caller the FAIL tier is inert; nothing but `check-scope-walkup-sync.sh` +(on fixtures) invokes the validator today. + +**Q5 — parser parity.** `parse_research_doc` accepts the bullet spelling (`- **Research doc:**`) as +`parse_contributing_files` already does, with a regression test. `parse_status` was removed from the +validator during this change, so it gets no test. Included because it is the same failure shape as +#111 and #118 (a parser returns "nothing found", the caller reads it as "nothing declared"), sits in +the same file, and `gitea-releases` already writes the unhyphenated form. + +**Q6 — what happens to check 8.** Found unsatisfiable during the migration, after Q3 was decided. +Check 8 requires every `extracted` slug in the research doc to appear in the skill's `sources.md`. +That worked while entries pointed at topic docs, and was dormant. Under Q1(a) the named file is a +registry shared by many skills (`git/sources.md` backs seven), and nothing ties a registry slug to one +skill, so every skill would fail permanently. The direction that matters, that each slug a skill lists +exists in the registry, is already check 7. + +- **(a) Retire check 8 (chosen).** Check 7 is the FAIL. Under registry semantics check 8 has no + satisfiable meaning. +- **(b) Keep it as an INFO (rejected).** Recreates the noise ADR-0004 warns about: an observation with + no action that every skill emits forever. +- **(c) Redefine it as a registry-side coverage report (rejected for now).** "Registry slugs that no + skill uses" is a coherent check, but it is a report across all skills and separate work from this + issue. + +**Q7 — `Basis:` paths that no longer exist.** Found in the same migration: `git-commits` and +`git-workflow` cite `core/instructions/git.md` and `commits.md`, deleted in `5deed07`. An existence +check on every `Basis:` bullet would fail them. + +- **(a) A bullet annotated `(removed in )` skips the existence check (chosen).** The check stays + for live paths, which is what catches a renamed ADR, and deletion becomes an explicit, auditable + annotation. The annotation is anchored at the end of the value and the sha is 7-40 hex characters. + Weakness: the annotation can be written on any bullet to avoid the check. Verifying the sha with + `git cat-file -e` would close that; the user decided against it as over-engineering for three + bullets, so the sha is format-checked only, not verified. +- **(b) `Basis:` becomes free prose with no existence check (rejected).** Gives up the one check that + catches a renamed or moved ADR. +- **(c) Drop those `Basis:` lines and keep `none` with a prose reason (rejected).** Loses the + machine-readable record of what the entry was drawn from. + +Form: one path per bullet, `- **Basis:** ` repeated, not a header with sub-bullets. + +**Q8 — the `lint` entry with no verifiable basis.** `house-vale-3-15-2-repro` in `vale-config` and +`vale-run` said `none` and claimed six behaviours were "established by running it against purpose-built +fixtures in this repo". No such fixture or test exists in the tree or in history: the entry was added +in `d1afdbe` with no test files, and the only vale test ever deleted (`4de5b6b`) guards an unrelated +`E100`. Under Q2 it FAILed for a missing `Basis:`. + +- **(e) Remove the entry and its `source_keys` citations (chosen, as the interim state).** The stated + basis was false, so there is nothing honest to declare. The behavioural rules stay in the skills; + only the provenance claim goes. The gate needs no allowlist. +- **(a) `Basis: tests/test-vale-wrap.sh` (rejected).** Backs about one of six claims and overstates the + rest. +- **(b) Commit reproduction fixtures (chosen, supersedes the interim removal).** The user decided to + commit real Vale reproduction fixtures under `plugins/lint` rather than soften the wording. The + `house-vale-3-15-2-repro` claim is restored only once it is backed by committed fixtures, and it + names them via `Basis:` (with `Research doc: none`). Until they land, the claim stays absent. +- **(c) Allow `none` without `Basis:` for "house-verified" entries (rejected).** Reopens Q2 and gives + an escape hatch for unverified claims. +- **(d) Keep the entry and allowlist the two skills in the gate (rejected).** Keeps a false claim in + place and adds a list that can rot. + +`configuration-reference.md` still says its rows were "reproduced against Vale 3.15.2"; that wording +now has no provenance entry behind it and is left for a separate decision. + +## Consequences + +- About 40 `references/sources.md` entries migrate: roughly 30 repoint from a topic doc to the registry, + about 4 move to `Research doc: none` with a `Basis:` list (`provider-adapter-author`, + `git-commits` `org-commit-conventions`, `agentsmd-audit` `governance-secrets-hard-prohibition`, + `git-workflow`), and the `gitea-releases` pair collapses to one path. +- `Basis:` is a new field: `create.md` step 6, `skill-file-structure.md` and the validator's usage text + must state it, and the validator must check that each listed path exists, except a bullet annotated + `(removed in )`. Each `Basis:` path is one bullet. +- Check 7 gains a FAIL tier for resolved-path mismatches. INFO remains for a path that does not + resolve. A topic doc named in `Research doc:` is no longer legal: it is a FAIL, since a topic doc has + no per-source `## H2` to check the slug against. +- Check 8 is retired: remove it from `lib-provenance-skill.sh`, its usage text and the tests, and drop + its mention from `skill-file-structure.md` and `create.md` where present. +- The corpus-wide sweep is a new gate: register it in `docs/spec/gates.md` and + `.pre-commit-config.yaml`. The corpus must be migrated in the same change or the suite goes red. +- The validator rejects an absolute path or one that escapes the repo with `..` in `Research doc:` and + `Basis:`, and rejects a `Research doc:` value with internal whitespace, backticks, or a comma list. +- Reversing this means re-migrating the same entries, which is why it is recorded. diff --git a/docs/spec/gates.md b/docs/spec/gates.md index a25124c..7af78c4 100644 --- a/docs/spec/gates.md +++ b/docs/spec/gates.md @@ -21,12 +21,12 @@ Install hooks via `pc-run`, wiring **all three stages**. This repo's `.pre-commi `default_install_hook_types`, so a plain install silently skips `commit-msg` (Conventional Commits) and `pre-push` (everything below). -The pre-push command reports **10** hooks, not 8. The extra two are pre-commit's own `meta` hooks, +The pre-push command reports **11** hooks, not 9. The extra two are pre-commit's own `meta` hooks, `check-hooks-apply` and `check-useless-excludes`: they declare no `stages:`, so they run at every stage including this one. Both are declared in this repo's `.pre-commit-config.yaml` like everything -else — what separates them is `repo: meta` (pre-commit's own built-ins) from `repo: local`. Eight +else — what separates them is `repo: meta` (pre-commit's own built-ins) from `repo: local`. Nine is the count of hooks this repo authors itself, and `--hook-stage pre-push --all-files` is a full -rehearsal of all eight. A PR merged through Gitea's merge button runs none of them: no local push +rehearsal of all nine. A PR merged through Gitea's merge button runs none of them: no local push happens at all. A real push has a gap of its own. When one `git push` carries several refs @@ -42,7 +42,7 @@ is checked out. Push one ref at a time when the gate matters. ## The pre-push gate -Eight hooks, grouped below by what they guard rather than by the order `.pre-commit-config.yaml` declares them in. +Nine hooks, grouped below by what they guard rather than by the order `.pre-commit-config.yaml` declares them in. **Core checks** @@ -66,6 +66,7 @@ version-blind, so a stale key deploys fine (see [apm gates](#apm-gates)). | Hook | Guards | |---|---| | `check-apm-agents-valid` | runs `factory-audit`'s `validate.sh` over every real `plugins/*/.apm/agents/*.agent.md` (see [Agent files](#agent-files-take-the-description-gates-not-the-body-gate)) | +| `check-provenance-corpus` | runs `factory-audit`'s `validate-provenance.sh` over every real `plugins/*/.apm/skills/*/` that has a `references/sources.md`, failing on any FAIL (see [The provenance corpus sweep](#the-provenance-corpus-sweep-adr-0028)) | **apm's own gates** @@ -576,6 +577,38 @@ follows symlinks with `find -L` because vale does. on `files:` patterns that match single markdown files, and only the `-d "$arg"` branch mirrors a directory. The exposed caller is the hand-invoked `vale-wrap.sh `. +## The provenance corpus sweep (ADR-0028) + +`check-provenance-corpus` runs `validate-provenance.sh` over every real +`plugins/*/.apm/skills/*/` directory that has a `references/sources.md`, and fails on any FAIL. The set +is discovered by glob, not counted, so a new skill is covered the moment it grows a `sources.md`, and +**discovering zero skills is an error, not a pass**. + +The hook exists because nothing else ran the validator over the real corpus. +`check-scope-walkup-sync` invokes it only against synthetic `mktemp` fixtures, and `factory-audit`'s +bats suite does the same. So a `Research doc:` naming the wrong file, or a slug absent from its +Research registry, could only be found by hand-running the validator in a loop. That is how 36 +mismatches (#121) reported INFO while every gate stayed green. ADR-0028 promotes "the check ran and +found a mismatch" from INFO to FAIL; without a caller across the corpus that FAIL tier would be inert. + +It reuses the validators' exit contract (see +[the three exit tiers](#the-three-exit-tiers-of-factory-audits-validators)) and keeps the tiers apart: + +| Exit | Means | +|---|---| +| **0** | every skill validated. INFO-only findings are printed, never swallowed | +| **1** | at least one skill FAILed. The summary line names the failing skills | +| **2** | the gate could not run: the validator is missing, a skill's validator run exited 2 ("not auditable"), or no skill with a `references/sources.md` was found | + +A validator exit 2 is reported as a gate error, not as a FAIL about that skill: it says the audit never +happened, and the skill has not been shown to be wrong. + +An unresolvable `Research doc:` path stays INFO by design, because a deployed copy of a skill outside +this repo will not carry the research docs (see `skill-file-structure.md`'s `sources.md` exemption). +This repo's own corpus is audited from the authoring source, where every path resolves, so an INFO +printed here is worth reading. Needs no network; needs `python3`, which the validator's own preflight +names. + ## Current retrofit status The ADR-0020 gates ship hot, with no baseline file — a shrinking baseline was considered and diff --git a/plugins/core/.apm/skills/agentsmd-audit/SKILL.md b/plugins/core/.apm/skills/agentsmd-audit/SKILL.md index efb5814..bb2a763 100644 --- a/plugins/core/.apm/skills/agentsmd-audit/SKILL.md +++ b/plugins/core/.apm/skills/agentsmd-audit/SKILL.md @@ -14,7 +14,7 @@ metadata: - context7-websites-agents-md - context7-agentsmd-agents-md - governance-secrets-hard-prohibition - version: "0.1.3" + version: "0.1.4" --- ## Gotchas diff --git a/plugins/core/.apm/skills/agentsmd-audit/references/sources.md b/plugins/core/.apm/skills/agentsmd-audit/references/sources.md index dfd3d9a..a5cfb22 100644 --- a/plugins/core/.apm/skills/agentsmd-audit/references/sources.md +++ b/plugins/core/.apm/skills/agentsmd-audit/references/sources.md @@ -28,6 +28,7 @@ - **URL:** (org convention — not a plugin research corpus entry) - **Description:** Hard prohibition on placing secrets, API keys, tokens, or credentials in code, config, prompts, or any output. Grounds the secrets/credentials check in `scripts/validate-secrets.sh` and Step 1 of SKILL.md — AGENTS.md is committed content, so an embedded real secret is a hard-prohibition violation, not a style nit. -- **Research doc:** core/instructions/governance.md (org convention file, not a plugin research corpus entry; content is inlined here since plugins must be self-contained and this file may not exist wherever the plugin is installed) +- **Research doc:** none — org convention, not a plugin research corpus entry +- **Basis:** core/instructions/governance.md (content is inlined here since plugins must be self-contained and this file may not exist wherever the plugin is installed) - **Contributing files:** SKILL.md - **Status:** `extracted` diff --git a/plugins/core/.apm/skills/provider-adapter-author/SKILL.md b/plugins/core/.apm/skills/provider-adapter-author/SKILL.md index 9f8b34d..f83d8ae 100644 --- a/plugins/core/.apm/skills/provider-adapter-author/SKILL.md +++ b/plugins/core/.apm/skills/provider-adapter-author/SKILL.md @@ -11,7 +11,7 @@ metadata: category: docs source_keys: - adr-0002-0003-two-tier-claude-md - version: "0.1.2" + version: "0.1.3" --- ## Gotchas diff --git a/plugins/core/.apm/skills/provider-adapter-author/references/sources.md b/plugins/core/.apm/skills/provider-adapter-author/references/sources.md index 654c51d..81d7268 100644 --- a/plugins/core/.apm/skills/provider-adapter-author/references/sources.md +++ b/plugins/core/.apm/skills/provider-adapter-author/references/sources.md @@ -4,6 +4,9 @@ - **URL:** (in-repo precedent — not an external source or plugin research corpus entry) - **Description:** This repo's own two-tier CLAUDE.md/AGENTS.md pattern: AGENTS.md is the provider-agnostic source of always-on rules; provider-specific files (CLAUDE.md) become thin adapters that import it (`@AGENTS.md` plus provider-specific additions). Grounds this skill's entire adapter-conversion design — the "thin adapter" shape, the `@`-import convention, and the size/duplication expectations enforced by `scripts/validate-adapter.sh`. -- **Research doc:** docs/adr/0002-two-tier-claude-md.md, docs/adr/0003-agents-md-provider-agnostic-entry-point.md, providers/claude-code/CLAUDE.md (in-repo ADRs and a live example, not a plugin research corpus entry; referenced here since this skill's design is modeled directly on an existing implementation rather than external research) +- **Research doc:** none — in-repo ADRs and a live example, not a plugin research corpus entry; this skill's design is modeled directly on an existing implementation rather than external research +- **Basis:** docs/adr/0002-two-tier-claude-md.md +- **Basis:** docs/adr/0003-agents-md-provider-agnostic-entry-point.md +- **Basis:** providers/claude-code/CLAUDE.md - **Contributing files:** SKILL.md, references/provider-matrix.md - **Status:** `extracted` diff --git a/plugins/git/.apm/skills/git-branches/SKILL.md b/plugins/git/.apm/skills/git-branches/SKILL.md index 3d09e5c..3f62511 100644 --- a/plugins/git/.apm/skills/git-branches/SKILL.md +++ b/plugins/git/.apm/skills/git-branches/SKILL.md @@ -9,7 +9,7 @@ description: > Not a Gitea remote's branches -> `gitea-branches`. metadata: - version: "1.0.5" + version: "1.0.6" category: git source_keys: - context7-git-htmldocs diff --git a/plugins/git/.apm/skills/git-branches/references/sources.md b/plugins/git/.apm/skills/git-branches/references/sources.md index 506756c..607e3ec 100644 --- a/plugins/git/.apm/skills/git-branches/references/sources.md +++ b/plugins/git/.apm/skills/git-branches/references/sources.md @@ -9,7 +9,7 @@ **Source:** https://nvie.com/posts/a-successful-git-branching-model/ -- **Research doc:** plugins/git/docs/research/docs/git/gitflow.md (whole-document reference) +- **Research doc:** plugins/git/docs/research/docs/git/sources.md (digest: plugins/git/docs/research/docs/git/gitflow.md — whole-document reference) **Contributing files:** - SKILL.md (Gitflow vs. GitHub Flow inference and the not-mixable rule) @@ -21,7 +21,7 @@ **Source:** https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow -- **Research doc:** plugins/git/docs/research/docs/git/gitflow.md (whole-document reference) +- **Research doc:** plugins/git/docs/research/docs/git/sources.md (digest: plugins/git/docs/research/docs/git/gitflow.md — whole-document reference) **Contributing files:** - SKILL.md (Gitflow vs. GitHub Flow inference and the not-mixable rule) @@ -34,7 +34,7 @@ **Source:** https://danielkummer.github.io/git-flow-cheatsheet/ -- **Research doc:** plugins/git/docs/research/docs/git/gitflow.md (whole-document reference) +- **Research doc:** plugins/git/docs/research/docs/git/sources.md (digest: plugins/git/docs/research/docs/git/gitflow.md — whole-document reference) **Contributing files:** - references/branch-patterns.md (feature/release/hotfix naming conventions) @@ -45,7 +45,7 @@ **Source:** context7:/git/htmldocs -- **Research doc:** plugins/git/docs/research/docs/git/branching-merging.md (whole-document reference) +- **Research doc:** plugins/git/docs/research/docs/git/sources.md (digest: plugins/git/docs/research/docs/git/branching-merging.md — whole-document reference) **Contributing files:** - SKILL.md (Gotchas — `git switch` abort-on-conflict behaviour, branch/tag name ambiguity) diff --git a/plugins/git/.apm/skills/git-commits/SKILL.md b/plugins/git/.apm/skills/git-commits/SKILL.md index 668497b..4d8a569 100644 --- a/plugins/git/.apm/skills/git-commits/SKILL.md +++ b/plugins/git/.apm/skills/git-commits/SKILL.md @@ -8,7 +8,7 @@ description: > Not branch lifecycle -> `git-branches`. metadata: - version: "0.1.7" + version: "0.1.8" category: git source_keys: - conventional-commits-spec diff --git a/plugins/git/.apm/skills/git-commits/references/sources.md b/plugins/git/.apm/skills/git-commits/references/sources.md index 960d8b3..4a3a2af 100644 --- a/plugins/git/.apm/skills/git-commits/references/sources.md +++ b/plugins/git/.apm/skills/git-commits/references/sources.md @@ -14,27 +14,29 @@ Sources extracted from the git plugin research phase. Only sources that directly ## conventional-commits-spec - **Description:** Conventional Commits Specification (v1.0.0) — message format, types, breaking changes, footer rules -- **Research doc:** plugins/git/docs/research/docs/git/commits.md § "Conventional Commits Specification (v1.0.0)" +- **Research doc:** plugins/git/docs/research/docs/git/sources.md (digest: plugins/git/docs/research/docs/git/commits.md § "Conventional Commits Specification (v1.0.0)") - **Contributing files:** SKILL.md, references/conventional-commits-spec.md, references/create-commit.md - **Status:** extracted ## commitlint-config-conventional - **Description:** commitlint config-conventional preset — validation constraints (max 100 chars header, no trailing periods, lowercase type, 11-type set enforcement) -- **Research doc:** plugins/git/docs/research/docs/git/commits.md § "commitlint Constraints (`config-conventional`)" +- **Research doc:** plugins/git/docs/research/docs/git/sources.md (digest: plugins/git/docs/research/docs/git/commits.md § "commitlint Constraints (`config-conventional`)") - **Contributing files:** SKILL.md, references/conventional-commits-spec.md, references/create-commit.md - **Status:** extracted ## org-commit-conventions - **Description:** Organization commit message body template and git conventions (atomic commits, no `--no-verify`, no force-push main/master, `rtk git` wrapper) — content fully embedded in this skill; the org's `core/instructions/git.md` and `core/instructions/commits.md` are provenance only and are not a live dependency -- **Research doc:** core/instructions/commits.md, core/instructions/git.md (org convention, not part of the plugin's research corpus) +- **Research doc:** none +- **Basis:** core/instructions/commits.md (removed in 5deed07) +- **Basis:** core/instructions/git.md (removed in 5deed07) - **Contributing files:** SKILL.md, references/commit-template.md, references/create-commit.md, references/rewrite-history.md - **Status:** extracted ## context7-git-htmldocs - **Description:** Official Git HTML documentation — `git commit --squash`/`--fixup`, `git rebase --autosquash`, and `git cherry-pick` range and abort semantics -- **Research doc:** plugins/git/docs/research/docs/git/cli-reference.md § "Committing", § "Rebasing", § "Cherry-picking" +- **Research doc:** plugins/git/docs/research/docs/git/sources.md (digest: plugins/git/docs/research/docs/git/cli-reference.md § "Committing", § "Rebasing", § "Cherry-picking") - **Contributing files:** SKILL.md, references/rewrite-history.md, references/cherry-pick.md - **Status:** extracted diff --git a/plugins/git/.apm/skills/git-history/SKILL.md b/plugins/git/.apm/skills/git-history/SKILL.md index 0ed23c4..dea78e5 100644 --- a/plugins/git/.apm/skills/git-history/SKILL.md +++ b/plugins/git/.apm/skills/git-history/SKILL.md @@ -8,7 +8,7 @@ description: > `git-commits`. Not a Gitea server's history -> `gitea-branches`. metadata: - version: "1.0.2" + version: "1.0.3" category: git source_keys: - git-scm-bisect-docs diff --git a/plugins/git/.apm/skills/git-history/references/sources.md b/plugins/git/.apm/skills/git-history/references/sources.md index fff5f45..7b98f21 100644 --- a/plugins/git/.apm/skills/git-history/references/sources.md +++ b/plugins/git/.apm/skills/git-history/references/sources.md @@ -10,7 +10,7 @@ source_keys: Git bisect documentation covering binary search through commit history to find the commit that introduced a bug. Includes manual flow, automated mode with exit codes, skip patterns, and visualization options. -- **Research doc:** plugins/git/docs/research/docs/git/history-inspection.md +- **Research doc:** plugins/git/docs/research/docs/git/sources.md (digest: plugins/git/docs/research/docs/git/history-inspection.md) - **Doc heading:** `## git bisect` - **Contributing files:** SKILL.md, references/bisect.md @@ -18,7 +18,7 @@ Git bisect documentation covering binary search through commit history to find t Git log documentation covering format presets, custom format placeholders (commit identity, author, committer, message, refs, GPG signature), pickaxe search (`-S` and `-G`), `--follow` for file renames, `--diff-filter`, and line-range history (`-L`). -- **Research doc:** plugins/git/docs/research/docs/git/history-inspection.md +- **Research doc:** plugins/git/docs/research/docs/git/sources.md (digest: plugins/git/docs/research/docs/git/history-inspection.md) - **Doc heading:** `## git log — Format and Filtering` - **Contributing files:** SKILL.md, references/git-log-format.md @@ -26,6 +26,6 @@ Git log documentation covering format presets, custom format placeholders (commi Git diff documentation covering output control (--stat, --name-only, --name-status, --word-diff) and whitespace handling flags. -- **Research doc:** plugins/git/docs/research/docs/git/history-inspection.md +- **Research doc:** plugins/git/docs/research/docs/git/sources.md (digest: plugins/git/docs/research/docs/git/history-inspection.md) - **Doc heading:** `## git diff — Output Control` - **Contributing files:** SKILL.md, references/git-log-format.md diff --git a/plugins/git/.apm/skills/git-remotes/SKILL.md b/plugins/git/.apm/skills/git-remotes/SKILL.md index 5ef896a..9b94ddd 100644 --- a/plugins/git/.apm/skills/git-remotes/SKILL.md +++ b/plugins/git/.apm/skills/git-remotes/SKILL.md @@ -10,7 +10,7 @@ description: > Not submodule pointers -> `git-submodules`. metadata: - version: "1.0.3" + version: "1.0.4" category: git source_keys: - git-scm-remote-docs diff --git a/plugins/git/.apm/skills/git-remotes/references/sources.md b/plugins/git/.apm/skills/git-remotes/references/sources.md index f2ad70e..31d4a78 100644 --- a/plugins/git/.apm/skills/git-remotes/references/sources.md +++ b/plugins/git/.apm/skills/git-remotes/references/sources.md @@ -9,7 +9,7 @@ **Source:** https://git-scm.com/docs/git-remote -- **Research doc:** plugins/git/docs/research/docs/git/remotes.md → `## Remote Management (`git remote`)` +- **Research doc:** plugins/git/docs/research/docs/git/sources.md (digest: plugins/git/docs/research/docs/git/remotes.md → `## Remote Management (`git remote`)`) **Contributing files:** - references/remote-config.md @@ -22,7 +22,7 @@ **Source:** https://git-scm.com/docs/git-fetch -- **Research doc:** plugins/git/docs/research/docs/git/remotes.md → `## Fetching (`git fetch`)` +- **Research doc:** plugins/git/docs/research/docs/git/sources.md (digest: plugins/git/docs/research/docs/git/remotes.md → `## Fetching (`git fetch`)`) **Contributing files:** - SKILL.md (Gotchas — prune does not touch tags) @@ -36,7 +36,7 @@ **Source:** https://git-scm.com/docs/git-push -- **Research doc:** plugins/git/docs/research/docs/git/remotes.md → `## Pushing (`git push`)` +- **Research doc:** plugins/git/docs/research/docs/git/sources.md (digest: plugins/git/docs/research/docs/git/remotes.md → `## Pushing (`git push`)`) **Contributing files:** - SKILL.md (Gotchas — `--force-with-lease` caveat; Step 1 force-push gate) @@ -50,7 +50,7 @@ **Source:** https://git-scm.com/docs/git-pull -- **Research doc:** plugins/git/docs/research/docs/git/remotes.md → `## Pulling (`git pull`)` +- **Research doc:** plugins/git/docs/research/docs/git/sources.md (digest: plugins/git/docs/research/docs/git/remotes.md → `## Pulling (`git pull`)`) **Contributing files:** - SKILL.md (Gotchas — pull default drift) @@ -64,7 +64,7 @@ **Source:** Context7 MCP / Git library -- **Research doc:** plugins/git/docs/research/docs/git/remotes.md (cross-cutting — no dedicated section) +- **Research doc:** plugins/git/docs/research/docs/git/sources.md (digest: plugins/git/docs/research/docs/git/remotes.md — cross-cutting — no dedicated section) **Contributing files:** - SKILL.md (all sections) diff --git a/plugins/git/.apm/skills/git-submodules/SKILL.md b/plugins/git/.apm/skills/git-submodules/SKILL.md index 560345b..6c0bdbb 100644 --- a/plugins/git/.apm/skills/git-submodules/SKILL.md +++ b/plugins/git/.apm/skills/git-submodules/SKILL.md @@ -9,7 +9,7 @@ description: > Not the superproject's own remotes -> `git-remotes`. metadata: - version: "1.0.1" + version: "1.0.2" category: git source_keys: - git-scm-submodule-docs diff --git a/plugins/git/.apm/skills/git-submodules/references/sources.md b/plugins/git/.apm/skills/git-submodules/references/sources.md index fd3adbc..fdc3c47 100644 --- a/plugins/git/.apm/skills/git-submodules/references/sources.md +++ b/plugins/git/.apm/skills/git-submodules/references/sources.md @@ -10,7 +10,7 @@ source_keys: **Source:** https://git-scm.com/docs/git-submodule -- **Research doc:** plugins/git/docs/research/docs/git/submodules.md (whole-document reference — the research doc is organized by descriptive prose headings such as "Concept Overview" and "Key Commands" rather than a heading matching this slug; this key covers the entire doc, not a single section) +- **Research doc:** plugins/git/docs/research/docs/git/sources.md (digest: plugins/git/docs/research/docs/git/submodules.md — whole-document reference — the research doc is organized by descriptive prose headings such as "Concept Overview" and "Key Commands" rather than a heading matching this slug; this key covers the entire doc, not a single section) **Contributing files:** - SKILL.md (all sections) diff --git a/plugins/git/.apm/skills/git-workflow/SKILL.md b/plugins/git/.apm/skills/git-workflow/SKILL.md index 528904d..f5069bc 100644 --- a/plugins/git/.apm/skills/git-workflow/SKILL.md +++ b/plugins/git/.apm/skills/git-workflow/SKILL.md @@ -8,7 +8,7 @@ description: > agent caller -> `git-orchestrate`. Not Gitea -> `gitea-workflow`. metadata: - version: "1.0.2" + version: "1.0.3" category: git source_keys: - nvie-gitflow-post diff --git a/plugins/git/.apm/skills/git-workflow/references/sources.md b/plugins/git/.apm/skills/git-workflow/references/sources.md index 1d8177c..bc03e99 100644 --- a/plugins/git/.apm/skills/git-workflow/references/sources.md +++ b/plugins/git/.apm/skills/git-workflow/references/sources.md @@ -9,7 +9,7 @@ **Source:** https://nvie.com/posts/a-successful-git-branching-model/ -- **Research doc:** plugins/git/docs/research/docs/git/gitflow.md (whole-document reference) +- **Research doc:** plugins/git/docs/research/docs/git/sources.md (digest: plugins/git/docs/research/docs/git/gitflow.md — whole-document reference) **Contributing files:** - SKILL.md (Interaction style — branching-model-aware tips) @@ -20,7 +20,7 @@ **Source:** https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow -- **Research doc:** plugins/git/docs/research/docs/git/gitflow.md (whole-document reference) +- **Research doc:** plugins/git/docs/research/docs/git/sources.md (digest: plugins/git/docs/research/docs/git/gitflow.md — whole-document reference) **Contributing files:** - SKILL.md (Interaction style — branching-model-aware tips) @@ -31,7 +31,7 @@ **Source:** https://danielkummer.github.io/git-flow-cheatsheet/ -- **Research doc:** plugins/git/docs/research/docs/git/gitflow.md (whole-document reference) +- **Research doc:** plugins/git/docs/research/docs/git/sources.md (digest: plugins/git/docs/research/docs/git/gitflow.md — whole-document reference) **Contributing files:** - SKILL.md (Interaction style — branching-model-aware tips) @@ -42,7 +42,7 @@ **Source:** context7:/git/htmldocs -- **Research doc:** plugins/git/docs/research/docs/git/overview.md (whole-document reference) +- **Research doc:** plugins/git/docs/research/docs/git/sources.md (digest: plugins/git/docs/research/docs/git/overview.md — whole-document reference) **Contributing files:** - SKILL.md (Workflow — general git operation vocabulary) @@ -53,7 +53,8 @@ **Source:** org-internal (formerly `core/instructions/git.md` in this repo, prior to its removal) -- **Research doc:** none — org convention, not part of the plugin's research corpus (no `plugins/git/docs/research/` topic file backs this entry) +- **Research doc:** none +- **Basis:** core/instructions/git.md (removed in 5deed07) **Contributing files:** - references/hard-rules.md (whole file — the eight hard rules and the conflict-handling rule) diff --git a/plugins/git/.apm/skills/git-worktrees/SKILL.md b/plugins/git/.apm/skills/git-worktrees/SKILL.md index ebd0422..20414d5 100644 --- a/plugins/git/.apm/skills/git-worktrees/SKILL.md +++ b/plugins/git/.apm/skills/git-worktrees/SKILL.md @@ -8,7 +8,7 @@ description: > Not interactive multi-step git guidance -> `git-workflow`. metadata: - version: "1.0.2" + version: "1.0.3" category: git source_keys: - git-scm-worktree-docs diff --git a/plugins/git/.apm/skills/git-worktrees/references/sources.md b/plugins/git/.apm/skills/git-worktrees/references/sources.md index 54fecc3..2a80855 100644 --- a/plugins/git/.apm/skills/git-worktrees/references/sources.md +++ b/plugins/git/.apm/skills/git-worktrees/references/sources.md @@ -9,7 +9,7 @@ **Source:** https://git-scm.com/docs/git-worktree -- **Research doc:** plugins/git/docs/research/docs/git/worktrees.md (whole-document reference — covers `## Concept Overview`, `## Key Commands`, `## Workflow Patterns`, `## Common Gotchas`, `## Configuration`) +- **Research doc:** plugins/git/docs/research/docs/git/sources.md (digest: plugins/git/docs/research/docs/git/worktrees.md — whole-document reference — covers `## Concept Overview`, `## Key Commands`, `## Workflow Patterns`, `## Common Gotchas`, `## Configuration`) **Contributing files:** - SKILL.md (Gotchas, Step 1 dispatch table and per-operation gates, Step 2 report format) diff --git a/plugins/git/.apm/skills/pc-author/SKILL.md b/plugins/git/.apm/skills/pc-author/SKILL.md index 9bddb84..e9be91f 100644 --- a/plugins/git/.apm/skills/pc-author/SKILL.md +++ b/plugins/git/.apm/skills/pc-author/SKILL.md @@ -6,7 +6,7 @@ description: > shellcheck"). Not running, installing, or updating hooks -> `pc-run`. allowed-tools: Bash Read Write Edit metadata: - version: "1.0.1" + version: "1.0.2" category: devtools source_keys: - context7-pre-commit-com diff --git a/plugins/git/.apm/skills/pc-author/references/sources.md b/plugins/git/.apm/skills/pc-author/references/sources.md index 24f21a3..424d352 100644 --- a/plugins/git/.apm/skills/pc-author/references/sources.md +++ b/plugins/git/.apm/skills/pc-author/references/sources.md @@ -5,7 +5,7 @@ - **URL:** context7:/pre-commit/pre-commit.com - **Description:** Official pre-commit.com documentation — installation, configuration schema, CLI reference, hook authoring, advanced features, troubleshooting - **Contributing files:** SKILL.md, references/create-config.md, references/modify-config.md, references/hooks-by-language.md -- **Research doc:** plugins/git/docs/research/docs/pre-commit/{overview,configuration,cli-reference,hook-authoring}.md +- **Research doc:** plugins/git/docs/research/docs/pre-commit/sources.md (digest: plugins/git/docs/research/docs/pre-commit/overview.md, plugins/git/docs/research/docs/pre-commit/configuration.md, plugins/git/docs/research/docs/pre-commit/cli-reference.md, plugins/git/docs/research/docs/pre-commit/hook-authoring.md) - **Status:** `extracted` ## pre-commit-com @@ -13,7 +13,7 @@ - **URL:** https://pre-commit.com/ - **Description:** Pre-commit framework homepage — full docs covering install, config, CLI, hook authoring, stages, local hooks, meta hooks, hazmat helpers, CI integration - **Contributing files:** SKILL.md, references/create-config.md, references/modify-config.md, references/hooks-by-language.md -- **Research doc:** plugins/git/docs/research/docs/pre-commit/{overview,configuration,cli-reference,hook-authoring}.md +- **Research doc:** plugins/git/docs/research/docs/pre-commit/sources.md (digest: plugins/git/docs/research/docs/pre-commit/overview.md, plugins/git/docs/research/docs/pre-commit/configuration.md, plugins/git/docs/research/docs/pre-commit/cli-reference.md, plugins/git/docs/research/docs/pre-commit/hook-authoring.md) - **Status:** `extracted` ## context7-pre-commit-hooks @@ -21,7 +21,7 @@ - **URL:** context7:/pre-commit/pre-commit-hooks - **Description:** Official pre-commit-hooks collection — all available hook IDs with options and examples - **Contributing files:** references/hooks-by-language.md -- **Research doc:** plugins/git/docs/research/docs/pre-commit/hooks-reference.md § pre-commit-hooks (official collection) +- **Research doc:** plugins/git/docs/research/docs/pre-commit/sources.md (digest: plugins/git/docs/research/docs/pre-commit/hooks-reference.md § pre-commit-hooks (official collection)) - **Status:** `extracted` ## pre-commit-hooks-github @@ -29,5 +29,5 @@ - **URL:** https://raw.githubusercontent.com/pre-commit/pre-commit-hooks/main/README.md - **Description:** Official pre-commit-hooks README — complete hook listing with all args, categories, deprecated hooks, and latest version (v6.0.0) - **Contributing files:** references/hooks-by-language.md -- **Research doc:** plugins/git/docs/research/docs/pre-commit/hooks-reference.md § pre-commit-hooks (official collection), § Deprecated hooks +- **Research doc:** plugins/git/docs/research/docs/pre-commit/sources.md (digest: plugins/git/docs/research/docs/pre-commit/hooks-reference.md § pre-commit-hooks (official collection), § Deprecated hooks) - **Status:** `extracted` diff --git a/plugins/git/.apm/skills/pc-run/SKILL.md b/plugins/git/.apm/skills/pc-run/SKILL.md index 90164c4..6f81157 100644 --- a/plugins/git/.apm/skills/pc-run/SKILL.md +++ b/plugins/git/.apm/skills/pc-run/SKILL.md @@ -8,7 +8,7 @@ description: > compatibility: Requires pre-commit installed and available on PATH. metadata: - version: "1.0.2" + version: "1.0.3" category: devtools source_keys: - context7-pre-commit-com diff --git a/plugins/git/.apm/skills/pc-run/references/sources.md b/plugins/git/.apm/skills/pc-run/references/sources.md index eb6a423..1cf5f18 100644 --- a/plugins/git/.apm/skills/pc-run/references/sources.md +++ b/plugins/git/.apm/skills/pc-run/references/sources.md @@ -5,7 +5,7 @@ - **URL:** context7:/pre-commit/pre-commit.com - **Description:** Official pre-commit.com documentation — installation, configuration schema, CLI reference, hook authoring, advanced features, troubleshooting - **Contributing files:** SKILL.md, references/install.md, references/autoupdate.md, references/clean.md, references/failure-patterns.md -- **Research doc:** plugins/git/docs/research/docs/pre-commit/{overview,cli-reference,troubleshooting}.md +- **Research doc:** plugins/git/docs/research/docs/pre-commit/sources.md (digest: plugins/git/docs/research/docs/pre-commit/overview.md, plugins/git/docs/research/docs/pre-commit/cli-reference.md, plugins/git/docs/research/docs/pre-commit/troubleshooting.md) - **Status:** `extracted` ## pre-commit-com @@ -13,7 +13,7 @@ - **URL:** https://pre-commit.com/ - **Description:** Pre-commit framework homepage — full docs covering install, config, CLI, hook authoring, stages, local hooks, meta hooks, hazmat helpers, CI integration - **Contributing files:** SKILL.md, references/install.md, references/autoupdate.md, references/clean.md, references/failure-patterns.md -- **Research doc:** plugins/git/docs/research/docs/pre-commit/{overview,cli-reference,troubleshooting}.md +- **Research doc:** plugins/git/docs/research/docs/pre-commit/sources.md (digest: plugins/git/docs/research/docs/pre-commit/overview.md, plugins/git/docs/research/docs/pre-commit/cli-reference.md, plugins/git/docs/research/docs/pre-commit/troubleshooting.md) - **Status:** `extracted` ## context7-pre-commit-hooks @@ -21,7 +21,7 @@ - **URL:** context7:/pre-commit/pre-commit-hooks - **Description:** Official pre-commit-hooks collection — all available hook IDs with options and examples - **Contributing files:** (none) -- **Research doc:** plugins/git/docs/research/docs/pre-commit/hooks-reference.md § "pre-commit-hooks (official collection)" +- **Research doc:** plugins/git/docs/research/docs/pre-commit/sources.md (digest: plugins/git/docs/research/docs/pre-commit/hooks-reference.md § "pre-commit-hooks (official collection)") - **Status:** `extracted` ## pre-commit-hooks-github @@ -29,5 +29,5 @@ - **URL:** https://raw.githubusercontent.com/pre-commit/pre-commit-hooks/main/README.md - **Description:** Official pre-commit-hooks README — complete hook listing with all args, categories, deprecated hooks, and latest version - **Contributing files:** (none) -- **Research doc:** plugins/git/docs/research/docs/pre-commit/hooks-reference.md § "pre-commit-hooks (official collection)" +- **Research doc:** plugins/git/docs/research/docs/pre-commit/sources.md (digest: plugins/git/docs/research/docs/pre-commit/hooks-reference.md § "pre-commit-hooks (official collection)") - **Status:** `extracted` diff --git a/plugins/gitea/.apm/skills/gitea-releases/SKILL.md b/plugins/gitea/.apm/skills/gitea-releases/SKILL.md index 6e7ccc6..0fb3189 100644 --- a/plugins/gitea/.apm/skills/gitea-releases/SKILL.md +++ b/plugins/gitea/.apm/skills/gitea-releases/SKILL.md @@ -14,7 +14,7 @@ compatibility: Requires Gitea MCP server configured with a token with write:repo metadata: category: integration - version: "0.1.2" + version: "0.1.3" source_keys: - gitea-mcp-repo - gitea-mcp-slim-go diff --git a/plugins/gitea/.apm/skills/gitea-releases/references/sources.md b/plugins/gitea/.apm/skills/gitea-releases/references/sources.md index c701c58..2886cbd 100644 --- a/plugins/gitea/.apm/skills/gitea-releases/references/sources.md +++ b/plugins/gitea/.apm/skills/gitea-releases/references/sources.md @@ -4,7 +4,7 @@ - **URL:** https://gitea.com/gitea/gitea-mcp - **Description:** Official gitea-mcp repository; operation/*.go source files documenting the MCP tools, their parameters, and CLI flags. Originally extracted at v1.3.0; the input parameter schemas in `references/call-signatures.md` were re-verified live via `ToolSearch` against the deployed server, **last verified at v1.7.0** as reported by `get_gitea_mcp_server_version`. -- **Research doc:** plugins/gitea/docs/research/docs/gitea/api-reference.md (Releases and Tags section); plugins/gitea/docs/research/docs/gitea/troubleshooting.md (`delete_release` numeric-id gotcha, `per_page` defaults) +- **Research doc:** plugins/gitea/docs/research/docs/gitea/sources.md (digest: plugins/gitea/docs/research/docs/gitea/api-reference.md, Releases and Tags section; also plugins/gitea/docs/research/docs/gitea/troubleshooting.md, `delete_release` numeric-id gotcha and `per_page` defaults) **Contributing files:** - SKILL.md (Dispatch table, Gotchas) @@ -16,7 +16,7 @@ - **URL:** https://gitea.com/gitea/gitea-mcp/raw/branch/main/operation/repo/slim.go - **Description:** Slim response shape structs from gitea-mcp source; defines exactly which fields the MCP server returns for tags and releases. -- **Research doc:** plugins/gitea/docs/research/docs/gitea/api-reference.md (Releases and Tags response shapes) +- **Research doc:** plugins/gitea/docs/research/docs/gitea/sources.md (digest: plugins/gitea/docs/research/docs/gitea/api-reference.md, Releases and Tags response shapes) **Contributing files:** - references/call-signatures.md (release/tag object shapes) @@ -27,7 +27,7 @@ - **URL:** context7:/websites/gitea - **Description:** Official Gitea docs mirror on Context7 (docs.gitea.com content) — release and tag semantics, draft/prerelease behavior. -- **Research doc:** plugins/gitea/docs/research/docs/gitea/workflow-conventions.md (Release and tag conventions section) +- **Research doc:** plugins/gitea/docs/research/docs/gitea/sources.md (digest: plugins/gitea/docs/research/docs/gitea/workflow-conventions.md, Release and tag conventions section) **Contributing files:** - SKILL.md (Gotchas — draft/prerelease as explicit flags) @@ -39,7 +39,7 @@ - **URL:** context7:/git_gitea_com/gitea_tea - **Description:** Official `tea` CLI (reference Gitea client) docs on Context7 — practitioner release/tag command patterns, semver tag conventions, draft/prerelease flags, release-notes-from-file conventions. -- **Research doc:** plugins/gitea/docs/research/docs/gitea/workflow-conventions.md (Release and tag conventions section) +- **Research doc:** plugins/gitea/docs/research/docs/gitea/sources.md (digest: plugins/gitea/docs/research/docs/gitea/workflow-conventions.md, Release and tag conventions section) **Contributing files:** - references/conventions.md (semver tag naming, release-notes sourcing) diff --git a/plugins/kyberforge/.apm/skills/factory-audit/SKILL.md b/plugins/kyberforge/.apm/skills/factory-audit/SKILL.md index 3fed83d..1a1f961 100644 --- a/plugins/kyberforge/.apm/skills/factory-audit/SKILL.md +++ b/plugins/kyberforge/.apm/skills/factory-audit/SKILL.md @@ -7,7 +7,7 @@ description: > fixes -> agent-author. allowed-tools: Bash Read metadata: - version: "1.0.3" + version: "1.0.4" category: factory source_keys: - agentskills-home diff --git a/plugins/kyberforge/.apm/skills/factory-audit/references/skill-file-structure.md b/plugins/kyberforge/.apm/skills/factory-audit/references/skill-file-structure.md index 99bb81d..52da19d 100644 --- a/plugins/kyberforge/.apm/skills/factory-audit/references/skill-file-structure.md +++ b/plugins/kyberforge/.apm/skills/factory-audit/references/skill-file-structure.md @@ -50,11 +50,15 @@ on-disk check. Flag any other spelling of a cross-skill reference. Two directories are exempt, and the exemptions are structural rather than discretionary: -- **`references/sources.md`.** Its `Research doc:` fields are development-time provenance pointers, - not runtime references. They are expected to be unresolvable after install, so - `validate-provenance.sh` does not treat an absent path as a FAIL — it emits an INFO naming the - slug and stating that checks 7 and 8 did not run for it. Flagging them as broken references - would make every correctly-provenanced skill fail. +- **`references/sources.md`.** Its `Research doc:` and `Basis:` fields are development-time + provenance pointers, not runtime references. A `Research doc:` path that does not resolve after + install is expected, so `validate-provenance.sh` does not treat an absent path as a FAIL — it + emits an INFO naming the slug and stating that check 7 did not run for it. Flagging them as + broken references would make every correctly-provenanced skill fail. Where the path DOES + resolve, it is checked: `Research doc:` names exactly one Research registry (a `sources.md` + whose H2 headings are the source slugs), and a slug missing from it, a topic document in its + place, or a list of paths is a FAIL. An entry with no registry writes `Research doc: none` plus + `Basis:` repo paths, which are existence-checked unless annotated `(removed in )`. - **`tests/`.** Test files are dev-only and may reference repo-level infrastructure such as a shared `tests/test_helper/`. The exemption is conditional on the dependency being declared: if `tests/` exists and `tests/README.md` is absent or does not document it, that is a FAIL. diff --git a/plugins/kyberforge/.apm/skills/factory-audit/scripts/lib-provenance-agent.sh b/plugins/kyberforge/.apm/skills/factory-audit/scripts/lib-provenance-agent.sh index f9649e3..160e34d 100755 --- a/plugins/kyberforge/.apm/skills/factory-audit/scripts/lib-provenance-agent.sh +++ b/plugins/kyberforge/.apm/skills/factory-audit/scripts/lib-provenance-agent.sh @@ -77,12 +77,13 @@ Checks performed: 4 Contributing files back-reference the parent slug in their source_keys 5 Research doc field present and not placeholder -Agent mode has no counterpart to skill mode's checks 6, 7 and 8 (Research -doc field / upstream forward / upstream reverse are numbered 6, 7, 8 there and -5 here): an agent at plugin scope is a single file with a plugin-root -sources.md, so there is no references/ tree to walk and no upstream research -source index to cross-check. parse_status() and the sources.md-basename gate -that those checks need exist only in lib-provenance-skill.sh. +Agent mode has no counterpart to skill mode's checks 6 and 7 (Research doc +field / slug in the Research registry are numbered 6 and 7 there, and the field +check is 5 here): an agent at plugin scope is a single file with a plugin-root +sources.md, so there is no references/ tree to walk and no Research registry to +cross-check. The sources.md-basename gate and the Basis: check that those checks +need exist only in lib-provenance-skill.sh. Skill mode's check 8 is retired +(ADR-0028). EOF } diff --git a/plugins/kyberforge/.apm/skills/factory-audit/scripts/lib-provenance-skill.sh b/plugins/kyberforge/.apm/skills/factory-audit/scripts/lib-provenance-skill.sh index f9fc8a1..0f283d6 100755 --- a/plugins/kyberforge/.apm/skills/factory-audit/scripts/lib-provenance-skill.sh +++ b/plugins/kyberforge/.apm/skills/factory-audit/scripts/lib-provenance-skill.sh @@ -63,12 +63,25 @@ Checks performed: 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 + 6 Research doc field present and not a placeholder, and exactly ONE path — the Research registry, a plugin's + research sources.md whose H2 headings are the source slugs. A brace + expansion, a comma-separated list, a semicolon-separated pair and a + repeated '- **Research doc:**' line are each a FAIL. An entry with no + registry writes 'Research doc: none' (a trailing annotation after an em + dash is fine) and names what it was drawn from in '- **Basis:**', one + repo path per bullet; a missing Basis, or a Basis path that does not + exist, is a FAIL. A Basis bullet annotated '(removed in )' skips + the existence check. + 7 Slug in sources.md present in the Research registry (FAIL). 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 + resolved. A path that does not resolve, or no repo root above the skill + directory, is reported as an INFO saying check 7 did not run, never + skipped silently. A Research doc that resolves to a file NOT named + sources.md (a topic document) is a FAIL. + 8 (retired — #121) The reverse check, "every extracted slug in the research + doc appears in this skill's sources.md", could not be satisfied when one + registry serves many skills. The number is left vacant so check 9 keeps + the name the rest of the repo cites. 9 Description or Contributing files text changed since --base-ref (INFO only — a bash script cannot verify the claim is still TRUE, only that it changed; the auditor reads the named files to check that). Wrapped values @@ -79,11 +92,10 @@ Checks performed: or references/sources.md is not tracked under this path at that ref, this is announced as ONE INFO for the whole check, never a silent skip. - 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. + Check 7 applies to a Research doc that names a Research registry — a file + whose basename is sources.md, whose H2 headings ARE source slugs. A topic + document is a FAIL, not a value the check skips, and every other reason it + does not run is announced as an INFO. EOF } @@ -344,25 +356,83 @@ KYBERFORGE_PROV_SKILL_PREAMBLE_PY="${KYBERFORGE_PROV_SKILL_PREAMBLE_PY%$'\n'}" IFS='' read -r -d '' KYBERFORGE_PROV_SKILL_BODY_PY <<'KYBERFORGE_PROV_SKILL_BODY' || true -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. - """ +def _entry_block(content, slug): + """The text under a '## slug' heading, or None when there is no such entry.""" pattern = re.compile( r'^## ' + re.escape(slug) + r'\s*\n(.*?)(?=^## |\Z)', re.MULTILINE | re.DOTALL ) m = pattern.search(content) - if not m: + return m.group(1) if m else None + +def parse_field_values(content, slug, label): + """Every value of a '**label:**' field under a slug H2, in document order. + + The SPELLING of a field must not decide whether it is read. Three + spellings are in the corpus and all three are accepted here: + + - **Label:** value (the documented form) + **Label:** value (no leading hyphen — gitea-releases writes Status so) + **Label:** (a header, then '- value' bullets) + - value + + A field parsed by a regex that knew only the first form returned "nothing + found" for the other two, and every caller read that as "nothing declared" + (#121, second comment; the same failure shape as #111 and #118). A header's + bullets stop at the first line that is neither blank nor a bullet, and a + '- **Other:**' bullet is the NEXT field, not a value of this one ('* ' + bullets count too, and a bold bullet with no colon is a value). + """ + block = _entry_block(content, slug) + if block is None: return [] - block = m.group(1) - return [v.strip() for v in - re.findall(r'^\- \*\*Research doc:\*\* (.+)$', block, re.MULTILINE)] + values = [] + lines = block.splitlines() + label_re = re.compile(r'^(?:[-*] )?\*\*' + re.escape(label) + r':\*\*[ \t]*(.*)$') + # A bullet that opens with a bold '**Other:**' label is the NEXT field. A + # bold bullet WITHOUT the colon ('- **docs/x.md**') is just a value. + next_field_re = re.compile(r'^[-*] \*\*[^*]*:\*\*') + i = 0 + while i < len(lines): + m = label_re.match(lines[i]) + i += 1 + if not m: + continue + inline = m.group(1).strip() + if inline: + values.append(inline) + continue + found = False + while i < len(lines): + line = lines[i].strip() + if not line: + i += 1 + continue + if not (line.startswith('- ') or line.startswith('* ')) or next_field_re.match(line): + break + values.append(line[2:].strip()) + found = True + i += 1 + if not found: + # The field is DECLARED but carries nothing: report an empty value, + # not an absent field, so callers say 'empty' rather than 'missing'. + values.append('') + return values + +def parse_research_docs(content, slug): + """Every Research doc value under a given slug H2, in document order. + + Research doc takes exactly ONE path, so the caller FAILs on a second value + rather than using the first and announcing the rest — an author who added a + doc rather than replacing one otherwise got check 7 run against the + old path and a verdict that looked complete. + """ + return parse_field_values(content, slug, 'Research doc') + +def parse_basis(content, slug): + """Every Basis value under a slug H2 — the repo paths an entry with no + Research registry was actually drawn from, one per bullet.""" + return parse_field_values(content, slug, 'Basis') # A Research doc value is a path, and very often a path PLUS an annotation # naming the section the slug came from: @@ -371,7 +441,7 @@ def parse_research_docs(content, slug): # 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 +# os.path.isfile() is false for every one of those strings, and check 7 # 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 @@ -381,8 +451,10 @@ def parse_research_docs(content, slug): 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() + """Path part of a Research doc value, with any section annotation removed + and surrounding backticks unwrapped ('`a/b.md`' resolves as 'a/b.md').""" + head = RESEARCH_DOC_ANNOTATION_RE.split(value, maxsplit=1)[0].strip() + return head.strip('`').strip() def research_doc_is_none(value): """True when a Research doc value declares that no research doc backs the slug. @@ -392,59 +464,61 @@ def research_doc_is_none(value): 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 + # 'none/foo.md' and 'none-of-these.md' are PATHS: after 'none' only the end, + # whitespace or an em/en dash may follow (or the parenthesised '(none)'). + return re.match(r'(?:\(none\)|none(?=$|\s|[\u2014\u2013]))', 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'^`([^`]*)`') +# A Research doc or Basis value names ONE path. The three list spellings seen +# in the corpus — a brace expansion, a comma-separated list and a +# semicolon-separated pair — are humans writing "several documents" into a +# single-path field. Nothing expands a brace in a markdown field, and the +# annotation strip above discards everything after the first '(' or section +# marker, so a second path parked after one was NEVER resolved and no check +# said so. Detected on the raw value, with commas and semicolons INSIDE the +# annotation left alone: those are prose ('cross-cutting; no dedicated +# section'), and only a second path-shaped token after a ';' is a list. +SECOND_PATH_AFTER_SEMICOLON_RE = re.compile(r'[;,]\s*[\w.\-]+/[\w./\-]*\.[A-Za-z]+') +# Only the LAST character class matters for the removal annotation: it must end +# the value, so '(removed in ) but still here' is not the annotation. +BASIS_REMOVED_RE = re.compile(r'\(removed in [0-9a-f]{7,40}\)\s*$') -def parse_status(content, slug): - """Find the Status value for a given slug H2 in content. +PAREN_GROUP_RE = re.compile(r'\([^()]*\)') - Returns the status with its backticks stripped ('extracted', 'referenced', - 'no content extracted'), or None when the entry has no Status line. +def names_more_than_one_path(value): + """True when a Research doc / Basis value is a list rather than one path. + + Three places to look, none of which is prose: + - the leading path token: whitespace inside it ('a.md b.md'), or any of + , ; { } or a stray backtick, is a list; + - the text after it, once balanced '(...)' annotations are removed (a + comma or semicolon INSIDE parentheses is prose): a bare , ; { } there + is a second path parked after the first ('a.md (x), b.md'); + - after a section marker (§, →) prose may hold commas, so only a + second path-SHAPED token after ',' or ';' counts. """ - 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() + head = strip_research_doc_annotation(value) + if re.search(r'[\s,;{}`]', head): + return True + rest = value[len(RESEARCH_DOC_ANNOTATION_RE.split(value, maxsplit=1)[0]):] + while True: + stripped = PAREN_GROUP_RE.sub('', rest) + if stripped == rest: break - if raw is None: - return None + rest = stripped + if rest.lstrip().startswith(('§', '→')): + return SECOND_PATH_AFTER_SEMICOLON_RE.search(rest) is not None + return re.search(r'[,;{}]', rest) is not None - token = STATUS_TOKEN_RE.match(raw) - return token.group(1).strip() if token else raw +def path_escapes_repo(repo_root, rel_path): + """True when rel_path is absolute or resolves (symlinks followed) outside + repo_root. Research doc and Basis are repo-relative, so anything else is + either a mistake or a way to make the checker read a file elsewhere.""" + if os.path.isabs(rel_path): + return True + root = os.path.realpath(repo_root) + real = os.path.realpath(os.path.join(root, rel_path)) + return not (real == root or real.startswith(root + os.sep)) def find_repo_root(start_dir): """Walk up from start_dir until we find a directory containing .git.""" @@ -460,7 +534,7 @@ def find_repo_root(start_dir): # --- Check 9 helpers --------------------------------------------------- # Check 9 needs a raw field VALUE (as text, to diff against an earlier # version), not the parsed structure parse_contributing_files() and -# parse_status() return. The ONE normalization applied is whitespace +# parse_field_values() return. The ONE normalization applied is whitespace # collapsing, which is what makes a re-wrap or a re-indent invisible; nothing # else is normalized away. # @@ -532,7 +606,7 @@ def parse_field_raw(content, slug, field_name): """Raw text of a '**:**' field under a slug H2, wrapping joined. Mirrors the two authored shapes parse_contributing_files() and - parse_status() already handle (inline value on the same line, or a + parse_field_values() already handle (inline value on the same line, or a bare heading followed by '- ' bullets), but returns text rather than a parsed structure, because check 9 diffs wording, not semantics. @@ -788,11 +862,8 @@ if os.path.isdir(refs_dir): 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 +# parse_basis — 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 @@ -810,7 +881,7 @@ for _slug in all_slugs: 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"checks 4, 5, 6 and 7 did not run for them. " f"Merge the blocks into one entry, or give each a distinct slug and reference it from source_keys." ) @@ -866,13 +937,13 @@ for slug in unique_slugs: # 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", + emit_fail( + f"Multiple '- **Research doc:**' lines for '{slug}' — Research doc takes exactly one path", 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." + f"The '## {slug}' entry has {len(rd_values)} Research doc lines. Research doc names one Research registry, " + f"so a second line is a list, and a list is not a grammar this field has.", + f"Keep one Research doc line, pointing at the plugin's research sources.md. If the entry has no registry, " + f"write '- **Research doc:** none' and name what it was drawn from in '- **Basis:**', one repo path per bullet." ) rd_value = rd_values[0] if rd_values else None if rd_value is None: @@ -880,16 +951,87 @@ for slug in unique_slugs: 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." + f"Add '- **Research doc:** ' to the '## {slug}' entry in references/sources.md, " + f"or '- **Research doc:** none' plus a '- **Basis:** ' line if no registry backs it." ) 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." + f"Set '- **Research doc:**' to the plugin's research sources.md (a path relative to the repo root), or to 'none' " + f"with a '- **Basis:** ' line if no registry backs this entry." ) - elif not research_doc_is_none(rd_value): + elif research_doc_is_none(rd_value): + # An entry with no Research registry must still say what it WAS drawn + # from. Basis names repo paths, one per bullet, and each is checked to + # exist — the honest way to record an org convention, an ADR or a + # house-verified reproduction, none of which has a registry entry. + basis_values = parse_basis(sources_content, slug) + if not basis_values: + emit_fail( + f"Basis missing for '{slug}' — Research doc is 'none'", + f"references/sources.md (## {slug})", + f"The '## {slug}' entry declares no Research registry ('{rd_value}') and no '- **Basis:**' line, " + f"so nothing records what the entry was drawn from.", + f"Add '- **Basis:** ' to the '## {slug}' entry, one line per path, naming the ADR, " + f"convention file or reproduction the entry rests on." + ) + for basis in basis_values: + basis_path = strip_research_doc_annotation(basis) + if PLACEHOLDER_RE.search(basis) or not basis_path: + emit_fail( + f"Basis is empty or placeholder for '{slug}'", + f"references/sources.md (## {slug})", + f"The '## {slug}' entry has an unfilled Basis value '{basis}'.", + f"Set '- **Basis:**' to one repo path." + ) + elif names_more_than_one_path(basis): + emit_fail( + f"Basis value names more than one path for '{slug}'", + f"references/sources.md (## {slug})", + f"The Basis value '{basis}' is a brace expansion or a comma- or semicolon-separated list.", + f"Write one '- **Basis:** ' line per path." + ) + elif BASIS_REMOVED_RE.search(basis): + # A path the entry HISTORICALLY rested on, annotated + # '(removed in )' at the end of the value, is a declaration + # that it is gone on purpose. The sha is not resolved + # (git cat-file was judged over-engineering, ADR-0028 Q7), and + # with no repo root there is nothing to check either way, so + # this skips silently in both cases. + continue + elif not repo_root: + emit_info( + f"Basis check skipped for '{slug}' — no repo root above the skill directory", + f"references/sources.md (## {slug})", + f"'{basis}' 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. Run this script against a skill inside a checkout." + ) + elif path_escapes_repo(repo_root, basis_path): + emit_fail( + f"Basis path '{basis_path}' is outside the repository for '{slug}'", + f"references/sources.md (## {slug})", + f"'{basis_path}' is absolute or resolves outside the repo root. Basis names repo paths.", + f"Use a path relative to the repo root that stays inside it." + ) + elif not os.path.exists(os.path.join(repo_root, basis_path)): + emit_fail( + f"Basis path '{basis_path}' does not exist", + f"references/sources.md (## {slug})", + f"'{basis}' resolves to '{basis_path}' relative to the repo root and nothing is there.", + f"Correct the path, or remove the Basis line if the entry no longer rests on it." + ) + elif names_more_than_one_path(rd_value): + emit_fail( + f"Research doc names more than one path for '{slug}'", + f"references/sources.md (## {slug})", + f"The Research doc value '{rd_value}' is a brace expansion or a comma- or semicolon-separated list. " + f"Research doc names exactly one Research registry.", + f"Point Research doc at the plugin's research sources.md. If the entry has no registry, write " + f"'- **Research doc:** none' and name what it was drawn from in '- **Basis:**', one repo path per bullet." + ) + else: # 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) @@ -898,7 +1040,7 @@ for slug in unique_slugs: 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"so it cannot be resolved. Check 7 did not run for this slug. " f"Run this script against a skill inside a checkout." ) elif not rd_path: @@ -906,8 +1048,15 @@ for slug in unique_slugs: 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." + f"Check 7 did not run for this slug. " + f"Give the value a file path relative to the repo root, or record 'none' plus a '- **Basis:**' if no registry backs this entry." + ) + elif path_escapes_repo(repo_root, rd_path): + emit_fail( + f"Research doc '{rd_path}' for '{slug}' is outside the repository", + f"references/sources.md (## {slug})", + f"'{rd_path}' is absolute or resolves outside the repo root. Research doc names a file in this repo.", + f"Point Research doc at the plugin's research sources.md, as a path relative to the repo root." ) else: rd_abs = os.path.join(repo_root, rd_path) @@ -916,33 +1065,26 @@ for slug in unique_slugs: 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." + f"Check 7 did not run for this slug, so nothing verified that the research doc still backs it. " + f"Point the value at the one existing Research registry (the plugin's research sources.md), " + f"or record 'none' plus a '- **Basis:**' if no registry 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", + # Check 7 matches slugs against the H2 headings of a + # Research registry — a sources.md whose H2s ARE source slugs. + # A topic document (remotes.md, gitflow.md) has section headings + # for H2s, so no slug can ever match one. Research doc names the + # registry (#121), so a topic document there is the wrong file, + # not a value these checks cannot verify. A pointer to the topic + # document that digested the source belongs in the free-text + # annotation after the path, where it is not checked. + emit_fail( + f"Research doc '{rd_path}' for '{slug}' is a topic document, not a Research registry", 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." + f"'{os.path.basename(rd_path)}' is not a sources.md, so its H2s are section headings and no slug can match one. " + f"Research doc names the plugin's Research registry — the sources.md whose H2s are source slugs.", + f"Repoint '{slug}' at the sibling sources.md in '{os.path.dirname(rd_path)}/', and keep the topic document in the " + f"annotation, e.g. ' (digested in {os.path.basename(rd_path)})'." ) else: try: @@ -951,63 +1093,19 @@ for slug in unique_slugs: 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"'{rd_path}' could not be decoded, so check 7 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( + emit_fail( 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." + f"The Research registry '{rd_path}' does not have a '## {slug}' heading, so the entry's provenance " + f"link resolves to nothing.", + f"Rename the slug to match a '## ' heading in '{rd_path}', or repoint Research doc at the registry that has it." ) - # 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." - ) # --- Check 9: Description / Contributing files changed since --base-ref --- # A structural fact — the field's TEXT differs from an earlier revision — is diff --git a/plugins/kyberforge/.apm/skills/factory-audit/tests/validate-provenance-skill.bats b/plugins/kyberforge/.apm/skills/factory-audit/tests/validate-provenance-skill.bats index 03d8fe3..3a51c53 100644 --- a/plugins/kyberforge/.apm/skills/factory-audit/tests/validate-provenance-skill.bats +++ b/plugins/kyberforge/.apm/skills/factory-audit/tests/validate-provenance-skill.bats @@ -53,6 +53,10 @@ EOF local slug="${2:-my-source}" local contrib="${3:-SKILL.md}" local research="${4:-(none)}" + # A 'none' Research doc must name its Basis (#121); SKILL.md exists in + # every fixture skill and, once commit_as_base has run, is a repo path. + local basis_line="" + if [[ "$research" == *none* ]]; then basis_line="- **Basis:** SKILL.md"; fi mkdir -p "$dir/references" cat > "$dir/references/sources.md" < "$skill/SKILL.md" < "$skill/SKILL.md" < "$fake_repo/docs/research/sources.md" < "$skill/references/sources.md" < "$skill/SKILL.md" < "$fake_repo/docs/research/sources.md" < "$skill/references/sources.md" < "$fake_repo/docs/research/sources.md" < "$fake_repo/docs/research/sources.md" < "$fake_repo/docs/research/sources.md" < "$fake_repo/docs/research/sources.md" < "$fake_repo/docs/research/sources.md" < "$fake_repo/docs/research/sources.md" < "$fake_repo/docs/research/sources.md" < "$fake_repo/docs/research/remotes.md" < "$fake_repo/docs/research/sources.md" < "$fake_repo/docs/research/sources.md" < "$fake_repo/docs/research/sources.md" < "$fake_repo/docs/research/sources.md" < "$skill/references/other.md" @@ -1886,6 +1518,7 @@ EOF - references/other.md (the rubric) - **Research doc:** (none) +- **Basis:** SKILL.md - **Status:** `extracted` EOF printf -- '---\nsource_keys:\n - my-source\n---\n\nnotes\n' > "$skill/references/other.md" @@ -1924,6 +1557,7 @@ EOF mkdir -p "$skill" make_skill_with_source_keys "$skill" make_sources_md "$skill" + sed -i 's#^- \*\*Basis:\*\* SKILL.md#- **Basis:** tracked-skill/SKILL.md#' "$skill/references/sources.md" commit_as_base "$repo" # A copy of the same skill at a path git does not know — the everyday case @@ -2060,3 +1694,532 @@ EOF refute_output "" assert_output --partial "$dir/notes.txt" } + +# --------------------------------------------------------------------------- +# Cycle 28 — #121: `Research doc:` names exactly one Research registry; +# an entry with no registry writes `none` plus `Basis:` +# +# Helper: a fake repo holding one skill whose single entry is written verbatim +# from the fields passed in — make_upstream_skill only varies the Research doc +# value, and the Basis cases need a second field. The registry lives at +# docs/research/sources.md and a basis file at docs/basis.md. +# --------------------------------------------------------------------------- + +make_entry_skill() { + local repo="$1" + local fields="$2" + local skill="$repo/my-skill" + mkdir -p "$skill/references" "$repo/docs/research" + cat > "$skill/SKILL.md" < "$repo/docs/basis.md" + printf '%s\n' "# Other" > "$repo/docs/other-basis.md" + { + printf '# Sources\n\n## my-source\n\n' + printf '%s\n' '- **URL:** https://example.com/my-source' + printf '%s\n' '- **Description:** A test source.' + printf '%s\n' '- **Contributing files:** SKILL.md' + printf '%s\n' "$fields" + printf '%s\n' '- **Status:** `extracted`' + } > "$skill/references/sources.md" + cat > "$repo/docs/research/sources.md" <)' Basis skips the existence check" { + local repo="$TMPDIR/fakerepo" + make_entry_skill "$repo" "$(printf '%s\n%s\n%s' '- **Research doc:** none' '- **Basis:** docs/basis.md' '- **Basis:** docs/deleted-adr.md (removed in 5b80f30)')" + run bash "$SCRIPT" "$repo/my-skill" + assert_success + assert_output "" +} + +@test "#121 FAIL: a Basis annotated with something other than '(removed in )' is still existence-checked" { + local repo="$TMPDIR/fakerepo" + make_entry_skill "$repo" "$(printf '%s\n%s' '- **Research doc:** none' '- **Basis:** docs/deleted-adr.md (removed later)')" + run bash "$SCRIPT" "$repo/my-skill" + assert_failure + assert_output --partial "Basis path 'docs/deleted-adr.md' does not exist" +} + +@test "#121 pass: a Basis with a trailing annotation resolves its path" { + local repo="$TMPDIR/fakerepo" + make_entry_skill "$repo" "$(printf '%s\n%s' '- **Research doc:** none' '- **Basis:** docs/basis.md (org convention file, not a corpus entry)')" + run bash "$SCRIPT" "$repo/my-skill" + assert_success + assert_output "" +} + +@test "#121 FAIL: check 7 — a slug missing from a resolved registry is a FAIL, not an INFO" { + local repo="$TMPDIR/fakerepo" + make_entry_skill "$repo" '- **Research doc:** docs/research/sources.md' + cat > "$repo/docs/research/sources.md" < "$repo/docs/research/remotes.md" + run bash "$SCRIPT" "$repo/my-skill" + assert_failure + assert_output --partial "is a topic document, not a Research registry" + refute_output --partial "not applicable" +} + +@test "#121 INFO: an unresolvable Research doc path stays INFO and exits 0" { + local repo="$TMPDIR/fakerepo" + make_entry_skill "$repo" '- **Research doc:** docs/research/gone/sources.md' + run bash "$SCRIPT" "$repo/my-skill" + assert_success + assert_output --partial "INFO" + assert_output --partial "does not exist" +} + +@test "#121 INFO: a Basis cannot be existence-checked with no repo root, and says so" { + local skill="$TMPDIR/my-skill" + make_skill_with_source_keys "$skill" + mkdir -p "$skill/references" + cat > "$skill/references/sources.md" < "$TMPDIR/outside/sources.md" + run bash "$SCRIPT" "$TMPDIR/fakerepo/my-skill" + assert_failure + assert_output --partial "outside the repository" +} + +@test "#121 FAIL: an absolute Basis path is outside the repo" { + basis_fixture '- **Basis:** /etc/passwd' + run bash "$SCRIPT" "$TMPDIR/fakerepo/my-skill" + assert_failure + assert_output --partial "outside the repository" +} + +@test "#121 FAIL: a '..' Basis escape is outside the repo even though the file exists" { + basis_fixture '- **Basis:** ../outside.md' + printf 'x\n' > "$TMPDIR/outside.md" + run bash "$SCRIPT" "$TMPDIR/fakerepo/my-skill" + assert_failure + assert_output --partial "outside the repository" +} + +@test "#121 FAIL: '(removed in abc)' is too short a sha to skip the check" { + basis_fixture '- **Basis:** docs/deleted-adr.md (removed in abc)' + run bash "$SCRIPT" "$TMPDIR/fakerepo/my-skill" + assert_failure + assert_output --partial "Basis path 'docs/deleted-adr.md' does not exist" +} + +@test "#121 FAIL: '(removed in )' followed by more text is not the annotation" { + basis_fixture '- **Basis:** docs/deleted-adr.md (removed in 5b80f30) but really still here' + run bash "$SCRIPT" "$TMPDIR/fakerepo/my-skill" + assert_failure + assert_output --partial "Basis path 'docs/deleted-adr.md' does not exist" +} + +@test "#121 pass: '(removed in )' Basis with no repo root is skipped silently" { + local skill="$TMPDIR/my-skill" + make_skill_with_source_keys "$skill" + mkdir -p "$skill/references" + cat > "$skill/references/sources.md" <'" { + rd_fixture '' + run bash "$SCRIPT" "$TMPDIR/fakerepo/my-skill" + assert_failure + assert_output --partial "Research doc field missing" + refute_output --partial "path-or-(none)" + assert_output --partial "Basis" +} + +@test "#121 parity: an inline Basis with no leading hyphen is read" { + basis_fixture '**Basis:** docs/gone.md' + run bash "$SCRIPT" "$TMPDIR/fakerepo/my-skill" + assert_failure + assert_output --partial "Basis path 'docs/gone.md' does not exist" + refute_output --partial "Basis missing" +} + +@test "#121 FAIL: 'a.md;b.md' with no space is a list, for Research doc" { + rd_fixture '- **Research doc:** docs/research/sources.md;docs/basis.md' + run bash "$SCRIPT" "$TMPDIR/fakerepo/my-skill" + assert_failure + assert_output --partial "Research doc names more than one path" +} + +@test "#121 FAIL: 'a.md;b.md' with no space is a list, for Basis" { + basis_fixture '- **Basis:** docs/basis.md;docs/other-basis.md' + run bash "$SCRIPT" "$TMPDIR/fakerepo/my-skill" + assert_failure + assert_output --partial "Basis value names more than one path" +} + +@test "#121 FAIL: an absolute Basis path is outside the repo even when it points inside the checkout" { + make_entry_skill "$TMPDIR/fakerepo" "$(printf '%s\n%s' '- **Research doc:** none' "- **Basis:** $TMPDIR/fakerepo/docs/basis.md")" + run bash "$SCRIPT" "$TMPDIR/fakerepo/my-skill" + assert_failure + assert_output --partial "outside the repository" +} + +@test "#121 FAIL: an absolute Research doc path is outside the repo even when it points inside the checkout" { + rd_fixture "- **Research doc:** $TMPDIR/fakerepo/docs/research/sources.md" + run bash "$SCRIPT" "$TMPDIR/fakerepo/my-skill" + assert_failure + assert_output --partial "outside the repository" +} + +@test "#121 parity: a '* **Basis:**' bullet spelling is read" { + make_entry_skill "$TMPDIR/fakerepo" "$(printf '%s\n%s' '- **Research doc:** none' '* **Basis:** docs/gone.md')" + run bash "$SCRIPT" "$TMPDIR/fakerepo/my-skill" + assert_failure + assert_output --partial "Basis path 'docs/gone.md' does not exist" +} + +@test "#121 pass: a comma inside a section-marker annotation is prose, not a list" { + rd_fixture '- **Research doc:** docs/research/sources.md § "Foo, bar and baz"' + run bash "$SCRIPT" "$TMPDIR/fakerepo/my-skill" + assert_success + assert_output "" +} diff --git a/plugins/kyberforge/.apm/skills/skill-author/SKILL.md b/plugins/kyberforge/.apm/skills/skill-author/SKILL.md index fc092a5..470dc4c 100644 --- a/plugins/kyberforge/.apm/skills/skill-author/SKILL.md +++ b/plugins/kyberforge/.apm/skills/skill-author/SKILL.md @@ -6,7 +6,7 @@ description: > Not read-only review -> `factory-audit`. Not agent files -> `agent-author`. allowed-tools: Bash Read Write Edit metadata: - version: "1.0.4" + version: "1.0.5" category: factory source_keys: - agentskills-home diff --git a/plugins/kyberforge/.apm/skills/skill-author/references/contract.md b/plugins/kyberforge/.apm/skills/skill-author/references/contract.md index 18f526b..53eb12c 100644 --- a/plugins/kyberforge/.apm/skills/skill-author/references/contract.md +++ b/plugins/kyberforge/.apm/skills/skill-author/references/contract.md @@ -252,6 +252,6 @@ inline that content directly into the skill (SKILL.md or a `references/` file) r to the file's path. Plugins must be self-contained and portable — the org file may not exist wherever the plugin is installed, and in this repo such files are meant to be deleted once their content is fully embedded downstream. Tag the inlined content with a `source_keys` entry using the -same `references/sources.md` schema as the create flow's Step 6, noting in the `Research doc:` -field that the source is an org convention rather than a plugin research corpus entry, so -provenance survives after the source file is gone. +same `references/sources.md` schema as the create flow's Step 6: write `Research doc: none` and +name the org convention file in a `Basis:` line, so provenance survives after the source file is +gone (annotate the Basis `(removed in )` once the file is deleted). diff --git a/plugins/kyberforge/.apm/skills/skill-author/references/create.md b/plugins/kyberforge/.apm/skills/skill-author/references/create.md index 1a00348..9dd569e 100644 --- a/plugins/kyberforge/.apm/skills/skill-author/references/create.md +++ b/plugins/kyberforge/.apm/skills/skill-author/references/create.md @@ -171,11 +171,20 @@ If a research `sources.md` is present in the conversation context: 2. For each entry, determine which skill files it contributed to (SKILL.md and any files in `references/` that drew from it). Update `Contributing files` accordingly — list skill files, not research topic files. -3. Write the updated content to `references/sources.md`. For each entry, include - `- **Research doc:** ` where `` is the relative path from the repo root to the - plugin-level research sources file this entry was drawn from (e.g. - `plugins/myplugin/docs/research/docs//sources.md`). This field is required on every - entry — it makes the provenance chain explicit and is validated by `/factory-audit`. +3. Write the updated content to `references/sources.md`. Every entry carries exactly one + `- **Research doc:** ` line. `` is the relative path from the repo root to the + **Research registry** — the plugin-level research `sources.md` whose `## H2` headings are the + source slugs (e.g. `plugins/myplugin/docs/research/docs//sources.md`) — never a topic + document, and never a list: no brace expansion, no comma- or semicolon-separated paths, no + second `Research doc:` line. A pointer to the topic document that digested the source goes in + an annotation after the path, e.g. ` (digest: )`, where it is not + checked. `/factory-audit` fails a slug missing from the registry it names. + + If the entry has no Research registry — an org convention, an ADR, a reproduction + backed by committed fixtures or tests named in `Basis:` — write `- **Research doc:** none` and name what it was drawn from with one + `- **Basis:** ` line per path. Each Basis path is checked to exist; annotate one + that has since been deleted `(removed in )` and the check is skipped. `none` with no Basis + is a FAIL. 4. Add `source_keys` to the frontmatter of `SKILL.md` (under `metadata`) listing the slugs of sources that informed it. 5. For each file in `references/` that was informed by research sources, add `source_keys` diff --git a/plugins/lint/.apm/skills/vale-config/SKILL.md b/plugins/lint/.apm/skills/vale-config/SKILL.md index 7ae5a67..caaafce 100644 --- a/plugins/lint/.apm/skills/vale-config/SKILL.md +++ b/plugins/lint/.apm/skills/vale-config/SKILL.md @@ -8,7 +8,7 @@ description: > metadata: category: lint - version: "0.1.3" + version: "0.1.4" source_keys: - context7-websites-vale-sh - house-vale-3-15-2-repro diff --git a/plugins/lint/.apm/skills/vale-config/references/configuration-reference.md b/plugins/lint/.apm/skills/vale-config/references/configuration-reference.md index b572d41..c448b71 100644 --- a/plugins/lint/.apm/skills/vale-config/references/configuration-reference.md +++ b/plugins/lint/.apm/skills/vale-config/references/configuration-reference.md @@ -82,7 +82,7 @@ Only *package* styles need fetching. A style whose YAML rule files are already c - `Vale.Avoid` — enforces the project's rejected vocabulary terms. - `Vale.Repetition` — flags repeated words (e.g. "the the"). -`Packages` (top-level, what `vale sync` downloads) and `BasedOnStyles` (per-glob, what activates) are separate keys: a style lints a file only once it is in both. Every row below reproduced against Vale 3.15.2 (slug `house-vale-3-15-2-repro`): +`Packages` (top-level, what `vale sync` downloads) and `BasedOnStyles` (per-glob, what activates) are separate keys: a style lints a file only once it is in both. Every row below is asserted against Vale 3.15.2 by `tests/test-vale-3-15-2-behaviours.sh` (slug `house-vale-3-15-2-repro`) except the `vale sync` row that adds the name to `Packages`, which needs the network and is not covered: | Configuration | Result | |---|---| @@ -98,7 +98,7 @@ Only *package* styles need fetching. A style whose YAML rule files are already c ## Frontmatter Scopes -House-verified behaviour, not documented on vale.sh — reproduced locally against Vale 3.15.2 (slug `house-vale-3-15-2-repro`). +House-verified behaviour, not documented on vale.sh — asserted against Vale 3.15.2 by `tests/test-vale-3-15-2-behaviours.sh` (slug `house-vale-3-15-2-repro`). A rule scoped to `text.frontmatter.` (e.g. `text.frontmatter.description`) matches reliably when that field's value is a single physical line, and breaks on most — not all — multi-line forms. Multi-line forms spanning 2+ lines: diff --git a/plugins/lint/.apm/skills/vale-config/references/sources.md b/plugins/lint/.apm/skills/vale-config/references/sources.md index c4a918e..940ea44 100644 --- a/plugins/lint/.apm/skills/vale-config/references/sources.md +++ b/plugins/lint/.apm/skills/vale-config/references/sources.md @@ -10,8 +10,9 @@ ## house-vale-3-15-2-repro -- **URL:** (house-verified — reproduced locally against the `vale` binary, not an external source) -- **Description:** Behaviour of Vale 3.15.2 established by running it against purpose-built fixtures in this repo, where vale.sh documents nothing: the `E100 [loadStyles]` / exit-2 failure for a `BasedOnStyles` name absent from `StylesPath`, `vale sync` reporting `Synced 0 package(s)` for a name not declared in `Packages`, the `E201` / exit-2 failure when the `StylesPath` directory does not exist, the exit-0 no-op of an empty style directory, the `E201` / exit-2 failure when a core option is written below a `[glob]` header (with `Packages` as the silent exception), and the `text.frontmatter.` scope matrix across multi-line YAML forms. -- **Research doc:** none — house-verified reproduction, not part of the plugin's research corpus (no `plugins/lint/docs/research/` topic file backs this entry) +- **URL:** (house-verified — reproduced against the `vale` binary by a committed test, not an external source) +- **Description:** Behaviour of Vale 3.15.2 asserted by the committed test (purpose-built fixtures, real `vale` run), where vale.sh documents nothing: the `E100 [loadStyles]` / exit-2 failure for a `BasedOnStyles` name absent from `StylesPath`, `vale sync` reporting `Synced 0 package(s)` for a name not declared in `Packages`, the `E201` / exit-2 failure when the `StylesPath` directory does not exist, the exit-0 no-op of an empty style directory, the `E201` / exit-2 failure when a core option is written below a `[glob]` header (with `Packages` as the silent exception), the `E100 [lintMDX]` failure of an unmapped `.mdx` without `mdx2vast`, and the `text.frontmatter.` scope matrix across multi-line YAML forms. +- **Research doc:** none +- **Basis:** tests/test-vale-3-15-2-behaviours.sh - **Contributing files:** SKILL.md, references/configuration-reference.md - **Status:** `extracted` diff --git a/plugins/lint/.apm/skills/vale-run/SKILL.md b/plugins/lint/.apm/skills/vale-run/SKILL.md index 5d90c43..e54a39f 100644 --- a/plugins/lint/.apm/skills/vale-run/SKILL.md +++ b/plugins/lint/.apm/skills/vale-run/SKILL.md @@ -6,7 +6,7 @@ description: > as in "lint the docs", "check prose style", or "why is CI failing on the docs check". Not setting up Vale config or styles -> `vale-config`. metadata: - version: "0.1.4" + version: "0.1.5" category: lint source_keys: - context7-websites-vale-sh diff --git a/plugins/lint/.apm/skills/vale-run/references/sources.md b/plugins/lint/.apm/skills/vale-run/references/sources.md index 196d43a..85592bc 100644 --- a/plugins/lint/.apm/skills/vale-run/references/sources.md +++ b/plugins/lint/.apm/skills/vale-run/references/sources.md @@ -10,8 +10,9 @@ ## house-vale-3-15-2-repro -- **URL:** (house-verified — reproduced locally against the `vale` binary, not an external source) -- **Description:** Behaviour of Vale 3.15.2 established by running it against purpose-built fixtures in this repo, where vale.sh documents nothing or documents it wrongly: `.mdx` has no built-in support and needs either `[formats] mdx = md` or an external `mdx2vast` binary (absent, the whole invocation exits 2 with `E100 [lintMDX]`), the inline-suppression form inverts between those two configurations, the `spelling` check's `ignore` paths resolve against `StylesPath` or the working directory but never against the rule file's own directory and fail silently when they resolve nowhere, `ls-config` reports styles and paths but never rules, and the `text.frontmatter.` scope matrix across multi-line YAML forms. -- **Research doc:** none — house-verified reproduction, not part of the plugin's research corpus (no `plugins/lint/docs/research/` topic file backs this entry) +- **URL:** (house-verified — reproduced against the `vale` binary by a committed test, not an external source) +- **Description:** Behaviour of Vale 3.15.2 asserted by the committed test (purpose-built fixtures, real `vale` run), where vale.sh documents nothing or documents it wrongly: an unmapped `.mdx` needs an external `mdx2vast` binary (absent, the whole invocation exits 2 with `E100 [lintMDX]`), under `[formats] mdx = md` the HTML-comment suppression form works and the JSX-comment form does not, the `spelling` check's `ignore` paths resolve against `StylesPath` or the working directory but never against the rule file's own directory and fail silently when they resolve nowhere, `ls-config` and the other `ls-*` subcommands report styles and paths but never rules, and the `text.frontmatter.` scope matrix across multi-line YAML forms. Not asserted: the native-MDX column of the suppression table, which needs `mdx2vast` installed. +- **Research doc:** none +- **Basis:** tests/test-vale-3-15-2-behaviours.sh - **Contributing files:** SKILL.md, references/troubleshooting.md - **Status:** `extracted` diff --git a/plugins/lint/.apm/skills/vale-run/references/troubleshooting.md b/plugins/lint/.apm/skills/vale-run/references/troubleshooting.md index a00a777..e6cfbd7 100644 --- a/plugins/lint/.apm/skills/vale-run/references/troubleshooting.md +++ b/plugins/lint/.apm/skills/vale-run/references/troubleshooting.md @@ -51,8 +51,7 @@ suppression syntax: | `[formats]` maps `mdx = md` (what `vale-config` recommends) | none | Markdown | `` | | no `mdx` mapping (native MDX) | `npm install -g mdx2vast` | MDX | `{/* vale off */}` | -Key the markup to that config row, never to the file extension. Verified against Vale 3.15.2, same -three fixtures under each config: +Key the markup to that config row, never to the file extension. Asserted against Vale 3.15.2 by `tests/test-vale-3-15-2-behaviours.sh` (slug `house-vale-3-15-2-repro`) for the mapped column; the native-MDX column was observed with `mdx2vast` installed and is not covered by that test (it needs the binary): | File | Mapped `mdx = md` | Native MDX (`mdx2vast` installed) | |---|---|---| @@ -120,7 +119,7 @@ ignore: **Where the file goes, and why a wrong answer is invisible.** Each entry resolves against the `StylesPath` root, or against the working directory `vale` is invoked from. It does **not** resolve against the rule file's own directory — which is the natural reading of the YAML above, since the -path sits inside the rule, and it is wrong. Verified against Vale 3.15.2 across four fresh trees, +path sits inside the rule, and it is wrong. Asserted against Vale 3.15.2 by the same test across four fresh trees, each with the same rule and the same unknown word: | Where `ignore1.txt` was placed | Result | diff --git a/scripts/check-provenance-corpus.sh b/scripts/check-provenance-corpus.sh new file mode 100755 index 0000000..da17c14 --- /dev/null +++ b/scripts/check-provenance-corpus.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Corpus-wide provenance sweep: runs factory-audit's validate-provenance.sh over +# every plugins/*/.apm/skills/*/ directory that has a references/sources.md, and +# fails on any FAIL. +# +# WHY THIS GATE EXISTS (ADR-0028, #121). Nothing else runs the validator over the +# real corpus. check-scope-walkup-sync.sh invokes it, but only against synthetic +# mktemp fixtures, and the factory-audit bats suite does the same. So a +# `Research doc:` that named the wrong file, or a slug absent from its Research +# registry, could only be found by hand-running the validator in a loop -- which +# is how 36 mismatches sat unnoticed while every gate stayed green. ADR-0028 +# promotes "the check ran and found a mismatch" from INFO to FAIL; without a +# caller across the corpus that FAIL tier would be inert. +# +# Exit codes, kept distinct on purpose: +# 0 every skill validated (INFO-only findings are printed, never swallowed) +# 1 at least one skill FAILed -- a real finding about the corpus +# 2 the gate itself could not run: validator missing, a validator exit 2 +# ("not auditable"), or NO skill with a references/sources.md found. A +# gate that discovers nothing must not read as a pass, and a skill that +# could not be audited must not read as a skill that failed the audit. +# +# The skill set is discovered by glob, not hardcoded, so a new skill is covered +# the moment it grows a references/sources.md. Runs from any cwd: REPO_ROOT defaults to the parent of this script's directory, or pass +# REPO_ROOT as arg. + +REPO_ROOT="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" +if [[ ! -d "$REPO_ROOT" ]]; then + echo "Provenance corpus check failed: REPO_ROOT '$REPO_ROOT' is not a directory." >&2 + exit 2 +fi +REPO_ROOT="$(cd "$REPO_ROOT" && pwd)" + +VALIDATOR="$REPO_ROOT/plugins/kyberforge/.apm/skills/factory-audit/scripts/validate-provenance.sh" +if [[ ! -f "$VALIDATOR" ]]; then + echo "Provenance corpus check failed: $VALIDATOR does not exist, so no skill was audited. If factory-audit's scripts moved, update this path." >&2 + exit 2 +fi + +shopt -s nullglob +sources_files=("$REPO_ROOT"/plugins/*/.apm/skills/*/references/sources.md) +shopt -u nullglob + +if [[ ${#sources_files[@]} -eq 0 ]]; then + echo "Provenance corpus check failed: found no plugins/*/.apm/skills/*/references/sources.md under $REPO_ROOT. Discovering zero skills is an error, not a pass -- the glob has gone stale or the corpus moved." >&2 + exit 2 +fi + +failing=() +errored=() +for sources in "${sources_files[@]}"; do + refs_dir="${sources%/*}" + skill_dir="${refs_dir%/*}" + rel="${skill_dir#"$REPO_ROOT"/plugins/}" + label="${rel%%/*}/${skill_dir##*/}" + + rc=0 + out="$(bash "$VALIDATOR" "$skill_dir" 2>&1)" || rc=$? + + case "$rc" in + 0) + # Exit 0 with output means INFO-only: a check that could not run, + # announced rather than skipped. Print it so it is not swallowed. + if [[ -n "$out" ]]; then + echo "== $label" + echo "$out" + fi + ;; + 1) + echo "== $label" + echo "$out" + failing+=("$label") + ;; + *) + echo "== $label (validator exit $rc)" + echo "$out" + errored+=("$label") + ;; + esac +done + +echo "" +echo "Provenance corpus: ${#sources_files[@]} skill(s) checked." + +if [[ ${#errored[@]} -gt 0 ]]; then + echo "Provenance corpus check errored (could not audit): ${errored[*]}" >&2 + if [[ ${#failing[@]} -gt 0 ]]; then + echo "Failing skills: ${failing[*]}" >&2 + fi + exit 2 +fi + +if [[ ${#failing[@]} -gt 0 ]]; then + echo "Failing skills: ${failing[*]}" >&2 + echo "Fix each FAIL above (see ADR-0028 for the Research doc / Basis grammar); INFO lines do not fail the gate." >&2 + exit 1 +fi + +echo "Provenance corpus check passed." diff --git a/tests/test-adr0020-contract.sh b/tests/test-adr0020-contract.sh index f74f79d..6b97779 100755 --- a/tests/test-adr0020-contract.sh +++ b/tests/test-adr0020-contract.sh @@ -744,6 +744,8 @@ EXPECTED = { 'apm pack --check-versions --check-clean --dry-run', ['pre-push']), 'check-scope-walkup-sync': ( 'bash scripts/check-scope-walkup-sync.sh', ['pre-push']), + 'check-provenance-corpus': ( + 'bash scripts/check-provenance-corpus.sh', ['pre-push']), 'check-skill-version-bump': ( 'bash scripts/check-skill-version-bump.sh', ['pre-push']), 'validate-marketplace': ( diff --git a/tests/test-check-provenance-corpus.sh b/tests/test-check-provenance-corpus.sh new file mode 100755 index 0000000..16b7e4e --- /dev/null +++ b/tests/test-check-provenance-corpus.sh @@ -0,0 +1,261 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SCRIPT="$REPO_ROOT/scripts/check-provenance-corpus.sh" +VALIDATOR_DIR="plugins/kyberforge/.apm/skills/factory-audit/scripts" +PASS=0 +FAIL=0 + +pass() { echo " PASS: $1"; PASS=$((PASS + 1)); } +fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); } + +FIXTURES=() +cleanup() { [[ ${#FIXTURES[@]} -eq 0 ]] || rm -rf "${FIXTURES[@]}"; } +trap cleanup EXIT + +# Per-run scratch for captured output, for the reason check-scope-walkup-sync's +# test gives: tests/run-tests.sh fans test scripts out concurrently. +RUN_TMP="$(mktemp -d)" +FIXTURES+=("$RUN_TMP") + +# A minimal REPO_ROOT: a .git entry (the validator's find_repo_root stops at +# it), a copy of the real validator at its real relative path, and one plugin +# holding a Research registry. Copying the real validator means the fixtures +# exercise the actual FAIL/INFO/exit contract rather than a stub of it. +make_repo() { + local dir + dir="$(mktemp -d)" + FIXTURES+=("$dir") + mkdir -p "$dir/.git" "$dir/$VALIDATOR_DIR" "$dir/plugins/p/docs/research/docs/t" + cp -R "$REPO_ROOT/$VALIDATOR_DIR/." "$dir/$VALIDATOR_DIR/" + cat > "$dir/plugins/p/docs/research/docs/t/sources.md" <<'EOF' +# Sources + +## known-slug + +**Status:** `extracted` +EOF + echo "$dir" +} + +# make_skill +make_skill() { + local repo="$1" name="$2" slug="$3" research="$4" + local skill="$repo/plugins/p/.apm/skills/$name" + mkdir -p "$skill/references" + cat > "$skill/SKILL.md" < "$skill/references/sources.md" < "$RUN_TMP/good.out" 2>&1; then + pass "exits 0 when every skill validates" +else + fail "exited non-zero on a clean corpus: $(cat "$RUN_TMP/good.out")" +fi + +# --- 2. A slug missing from the registry is a FAIL and is named --- +echo "" +echo "--- failing skill ---" +R="$(make_repo)" +make_skill "$R" good known-slug "$REGISTRY" +make_skill "$R" bad missing-slug "$REGISTRY" +set +e +bash "$SCRIPT" "$R" > "$RUN_TMP/bad.out" 2>&1 +rc=$? +set -e +if [[ $rc -eq 1 ]]; then + pass "exits 1 when one skill has a slug missing from its registry" +else + fail "expected exit 1, got $rc: $(cat "$RUN_TMP/bad.out")" +fi +if grep -q "bad" "$RUN_TMP/bad.out" && ! grep -qE "Failing skills:.*good" "$RUN_TMP/bad.out"; then + pass "summary line names the failing skill and not the passing one" +else + fail "summary did not name only the failing skill: $(cat "$RUN_TMP/bad.out")" +fi + +# --- 3. INFO-only passes but the INFO is printed, not swallowed --- +echo "" +echo "--- INFO-only skill ---" +R="$(make_repo)" +make_skill "$R" info-only known-slug "plugins/p/docs/research/docs/gone/sources.md" +if bash "$SCRIPT" "$R" > "$RUN_TMP/info.out" 2>&1; then + pass "exits 0 when the only findings are INFO" +else + fail "INFO-only corpus failed the gate: $(cat "$RUN_TMP/info.out")" +fi +if grep -q "INFO" "$RUN_TMP/info.out"; then + pass "INFO findings are printed" +else + fail "INFO finding was swallowed: $(cat "$RUN_TMP/info.out")" +fi + +# --- 4. Zero skills discovered is an error, not a pass --- +echo "" +echo "--- zero skills ---" +R="$(make_repo)" +set +e +bash "$SCRIPT" "$R" > "$RUN_TMP/zero.out" 2>&1 +rc=$? +set -e +if [[ $rc -eq 2 ]]; then + pass "exits 2 when no skill with references/sources.md is found" +else + fail "expected exit 2 for an empty corpus, got $rc: $(cat "$RUN_TMP/zero.out")" +fi + +# --- 5. A missing validator is a gate error (exit 2), never a pass --- +echo "" +echo "--- missing validator ---" +R="$(make_repo)" +make_skill "$R" good known-slug "$REGISTRY" +rm -rf "${R:?}/$VALIDATOR_DIR" +set +e +bash "$SCRIPT" "$R" > "$RUN_TMP/novalidator.out" 2>&1 +rc=$? +set -e +if [[ $rc -eq 2 ]]; then + pass "exits 2 when the validator is missing" +else + fail "expected exit 2 for a missing validator, got $rc: $(cat "$RUN_TMP/novalidator.out")" +fi + +# --- 6. A validator exit 2 (unauditable input) is a gate error, not a FAIL --- +echo "" +echo "--- validator exit 2 ---" +R="$(make_repo)" +make_skill "$R" good known-slug "$REGISTRY" +# Replace the entry point with a stub that reports "not auditable". +printf '#!/usr/bin/env bash\necho "stub: not auditable" >&2\nexit 2\n' \ + > "$R/$VALIDATOR_DIR/validate-provenance.sh" +set +e +bash "$SCRIPT" "$R" > "$RUN_TMP/exit2.out" 2>&1 +rc=$? +set -e +if [[ $rc -eq 2 ]]; then + pass "a validator exit 2 surfaces as gate exit 2, not as a skill FAIL" +else + fail "expected exit 2 to propagate, got $rc: $(cat "$RUN_TMP/exit2.out")" +fi + +# --- 7. The real corpus: reported, and the gate agrees with the validator --- +echo "" +echo "--- this repo's real corpus ---" +set +e +bash "$SCRIPT" "$REPO_ROOT" > "$RUN_TMP/real.out" 2>&1 +rc=$? +set -e +if [[ $rc -eq 0 ]]; then + pass "real corpus is clean (exit 0)" +else + fail "real corpus did not validate clean (exit $rc): $(cat "$RUN_TMP/real.out")" +fi + +# --- 8. Runs by absolute path from another cwd, with no argument --- +echo "" +echo "--- other cwd, no argument ---" +set +e +(cd "$RUN_TMP" && bash "$SCRIPT" > "$RUN_TMP/cwd.out" 2>&1) +rc=$? +set -e +if [[ $rc -eq 0 ]]; then + pass "derives REPO_ROOT from the script location, not the cwd" +else + fail "expected exit 0 from a foreign cwd, got $rc: $(cat "$RUN_TMP/cwd.out")" +fi + +# --- 9. A skill dir without references/sources.md is skipped, not an error --- +echo "" +echo "--- skill without sources.md ---" +R="$(make_repo)" +make_skill "$R" good known-slug "$REGISTRY" +mkdir -p "$R/plugins/p/.apm/skills/nosources" +printf -- '---\nname: nosources\ndescription: x\n---\n' > "$R/plugins/p/.apm/skills/nosources/SKILL.md" +set +e +bash "$SCRIPT" "$R" > "$RUN_TMP/skip.out" 2>&1 +rc=$? +set -e +if [[ $rc -eq 0 ]] && grep -q "1 skill(s) checked" "$RUN_TMP/skip.out" && ! grep -q "nosources" "$RUN_TMP/skip.out"; then + pass "skill without sources.md is skipped silently and not counted" +else + fail "expected exit 0, 1 skill checked, no mention (got $rc): $(cat "$RUN_TMP/skip.out")" +fi + +# --- 10. Multiple failing skills are all reported --- +echo "" +echo "--- multiple failing skills ---" +R="$(make_repo)" +make_skill "$R" good known-slug "$REGISTRY" +make_skill "$R" bad1 missing-one "$REGISTRY" +make_skill "$R" bad2 missing-two "$REGISTRY" +set +e +bash "$SCRIPT" "$R" > "$RUN_TMP/multi.out" 2>&1 +rc=$? +set -e +if [[ $rc -eq 1 ]] && grep -qE "Failing skills:.*bad1" "$RUN_TMP/multi.out" \ + && grep -qE "Failing skills:.*bad2" "$RUN_TMP/multi.out" \ + && ! grep -qE "Failing skills:.*good" "$RUN_TMP/multi.out"; then + pass "exits 1 and names every failing skill" +else + fail "expected exit 1 naming bad1 and bad2 (got $rc): $(cat "$RUN_TMP/multi.out")" +fi + +# --- 11. An errored skill alongside a failing one: exit 2 wins, both named --- +echo "" +echo "--- errored + failing precedence ---" +R="$(make_repo)" +make_skill "$R" failing known-slug "$REGISTRY" +make_skill "$R" broken known-slug "$REGISTRY" +# Stub validator: FAIL for 'failing', "not auditable" for 'broken'. +cat > "$R/$VALIDATOR_DIR/validate-provenance.sh" <<'EOF' +#!/usr/bin/env bash +case "$1" in + */failing) echo "FAIL: stub"; exit 1 ;; + */broken) echo "stub: not auditable" >&2; exit 2 ;; +esac +exit 0 +EOF +set +e +bash "$SCRIPT" "$R" > "$RUN_TMP/prec.out" 2>&1 +rc=$? +set -e +if [[ $rc -eq 2 ]] && grep -q "errored (could not audit): .*broken" "$RUN_TMP/prec.out" \ + && grep -q "Failing skills: .*failing" "$RUN_TMP/prec.out"; then + pass "exit 2 takes precedence over exit 1, and both are reported" +else + fail "expected exit 2 naming both (got $rc): $(cat "$RUN_TMP/prec.out")" +fi + +echo "" +echo "Results: $PASS passed, $FAIL failed" +[[ $FAIL -eq 0 ]] diff --git a/tests/test-vale-3-15-2-behaviours.sh b/tests/test-vale-3-15-2-behaviours.sh new file mode 100755 index 0000000..c78ac5b --- /dev/null +++ b/tests/test-vale-3-15-2-behaviours.sh @@ -0,0 +1,220 @@ +#!/usr/bin/env bash +# Reproduction fixtures for the Vale 3.15.2 behaviours the lint plugin's vale-config and vale-run +# skills state as house-verified (provenance key house-vale-3-15-2-repro). Each case builds a +# purpose-built fixture in a temp dir, runs the real `vale` binary and asserts exit code plus +# output. A behaviour that changes in a later Vale release fails here, which is the point: the +# skill text is only backed while this test passes. +# +# Not reproducible here (mdx2vast is not installed in CI): the native-MDX halves of the mdx +# suppression table. Only the mdx2vast-absent E100 and the `[formats] mdx = md` column are asserted. +set -uo pipefail + +if ! command -v vale &>/dev/null; then + echo "SKIP: vale is not installed" + exit 77 +fi +EXPECTED="3.15.2" +GOT="$(vale --version | awk '{print $3}')" +if [[ "$GOT" != "$EXPECTED" ]]; then + echo "SKIP: behaviours are pinned to Vale $EXPECTED, found $GOT" + exit 77 +fi + +PASS=0 +FAIL=0 +pass() { echo " PASS: $1"; PASS=$((PASS + 1)); } +fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); } + +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +OUT="" RC=0 +# run : run vale from , capture combined output and exit code. +run() { + local dir="$1"; shift + OUT="$(cd "$dir" && vale --no-wrap "$@" 2>&1)"; RC=$? + OUT="$(sed $'s/\x1b\\[[0-9;]*m//g' <<<"$OUT")" +} +# expect