Compare commits
18 Commits
da95fa2a9e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f30fbacf14 | |||
| f22836ff7e | |||
| 4357da5b4d | |||
| b6a5915520 | |||
| 025ad4a5af | |||
| d654dca056 | |||
| c5f754d3ad | |||
| 3ea057794c | |||
| 97cd22edda | |||
| 45d8f19e56 | |||
| 58a3f402a6 | |||
| c008da1876 | |||
| 2c4b6d2615 | |||
| 2bde9a6a82 | |||
| b62513d30d | |||
| a1f9fa9091 | |||
| 740f631d1d | |||
| 5a52949c57 |
@@ -1,59 +1,59 @@
|
||||
{
|
||||
"name": "holocron",
|
||||
"description": "AI development skills for Claude Code, and for GitHub Copilot through apm — factory, design, implement, review, and cross-cutting workflows.",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"owner": {
|
||||
"name": "Defame1297",
|
||||
"email": "defame1297@rkdr.net",
|
||||
"url": "https://git.dev.rkdr.net/Defame1297/"
|
||||
"url": "https://git.rkdr.net/Defame1297/"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "kyberforge",
|
||||
"description": "Skills and agents for creating, maintaining, and managing an apm plugin marketplace for Claude Code and GitHub Copilot.",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"category": "Developer Tools",
|
||||
"source": "./plugins/kyberforge"
|
||||
},
|
||||
{
|
||||
"name": "bin",
|
||||
"description": "Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.",
|
||||
"version": "1.1.8",
|
||||
"version": "1.1.9",
|
||||
"category": "Utilities",
|
||||
"source": "./plugins/bin"
|
||||
},
|
||||
{
|
||||
"name": "git",
|
||||
"description": "Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it.",
|
||||
"version": "1.3.8",
|
||||
"version": "1.3.9",
|
||||
"category": "Version Control",
|
||||
"source": "./plugins/git"
|
||||
},
|
||||
{
|
||||
"name": "gitea",
|
||||
"description": "Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone.",
|
||||
"version": "1.3.9",
|
||||
"version": "1.3.10",
|
||||
"category": "Version Control",
|
||||
"source": "./plugins/gitea"
|
||||
},
|
||||
{
|
||||
"name": "onedev",
|
||||
"description": "Skills and agents for working with a OneDev forge through the TOD CLI — the forge's own objects, as distinct from the local git clone.",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"category": "Version Control",
|
||||
"source": "./plugins/onedev"
|
||||
},
|
||||
{
|
||||
"name": "core",
|
||||
"description": "Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it.",
|
||||
"version": "1.1.3",
|
||||
"version": "1.1.4",
|
||||
"category": "Productivity",
|
||||
"source": "./plugins/core"
|
||||
},
|
||||
{
|
||||
"name": "lint",
|
||||
"description": "Skills and agents for configuring and running linters.",
|
||||
"version": "1.1.8",
|
||||
"version": "1.1.9",
|
||||
"category": "Developer Tools",
|
||||
"source": "./plugins/lint"
|
||||
}
|
||||
|
||||
2
.gitmodules
vendored
2
.gitmodules
vendored
@@ -12,4 +12,4 @@
|
||||
ignore = dirty
|
||||
[submodule "docs/wiki"]
|
||||
path = docs/wiki
|
||||
url = git@git.dev.rkdr.net:Defame1297/holocron.wiki.git
|
||||
url = git@git.rkdr.net:Defame1297/holocron.wiki.git
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
1254
apm.lock.yaml
1254
apm.lock.yaml
File diff suppressed because it is too large
Load Diff
22
apm.yml
22
apm.yml
@@ -1,5 +1,5 @@
|
||||
name: holocron
|
||||
version: 0.5.0
|
||||
version: 0.5.1
|
||||
description: AI development skills for Claude Code, and for GitHub Copilot through apm — factory, design, implement, review, and cross-cutting workflows.
|
||||
license: MIT
|
||||
|
||||
@@ -16,17 +16,17 @@ targets:
|
||||
- claude
|
||||
dependencies:
|
||||
apm:
|
||||
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
|
||||
- git: git@git.rkdr.net:Defame1297/holocron.git
|
||||
path: plugins/bin
|
||||
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
|
||||
- git: git@git.rkdr.net:Defame1297/holocron.git
|
||||
path: plugins/core
|
||||
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
|
||||
- git: git@git.rkdr.net:Defame1297/holocron.git
|
||||
path: plugins/git
|
||||
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
|
||||
- git: git@git.rkdr.net:Defame1297/holocron.git
|
||||
path: plugins/gitea
|
||||
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
|
||||
- git: git@git.rkdr.net:Defame1297/holocron.git
|
||||
path: plugins/kyberforge
|
||||
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
|
||||
- git: git@git.rkdr.net:Defame1297/holocron.git
|
||||
path: plugins/lint
|
||||
# TOD's skills arrive transitively through this wrapper rather than as a
|
||||
# direct entry, so the marketplace and this repo consume onedev by the same
|
||||
@@ -38,7 +38,7 @@ dependencies:
|
||||
# `apm install` fails, which includes the copy kyberforge's SessionStart
|
||||
# hook runs on launch. Accepted deliberately: this branch is merging
|
||||
# immediately.
|
||||
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
|
||||
- git: git@git.rkdr.net:Defame1297/holocron.git
|
||||
path: plugins/onedev
|
||||
mcp: []
|
||||
|
||||
@@ -61,7 +61,7 @@ dependencies:
|
||||
# an apm mechanic.
|
||||
executables:
|
||||
allow:
|
||||
kyberforge#2.0.0:
|
||||
kyberforge#2.0.1:
|
||||
hooks: true
|
||||
bin: true
|
||||
|
||||
@@ -71,11 +71,11 @@ marketplace:
|
||||
# top-level apm.yml description:/version: above are NOT inherited into the
|
||||
# compiled output despite being used elsewhere (e.g. by `apm audit`).
|
||||
description: AI development skills for Claude Code, and for GitHub Copilot through apm — factory, design, implement, review, and cross-cutting workflows.
|
||||
version: 0.5.0
|
||||
version: 0.5.1
|
||||
owner:
|
||||
name: Defame1297
|
||||
email: defame1297@rkdr.net
|
||||
url: https://git.dev.rkdr.net/Defame1297/
|
||||
url: https://git.rkdr.net/Defame1297/
|
||||
|
||||
# Default tag pattern used to resolve version ranges for each package.
|
||||
build:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -414,6 +414,56 @@ and rises to a blocking ERROR the moment a resolving sibling joins it. The reaso
|
||||
the point of enforcement in `_add()`'s docstring in `scripts/skill-size-check.sh` and its two
|
||||
mirrored copies, and the verdict table in `docs/spec/gates.md` states the corrected shape.
|
||||
|
||||
## Amendment (2026-09-22): body-level routing targets are resolved too
|
||||
|
||||
The Decision section's routing-target resolver (`boundary_targets()` / `unresolved_targets()`) reads
|
||||
the **description** only. A target named in the **body** — a dispatch table row, a "run X" step, both
|
||||
routine in a 900-word procedure — was checked by nothing. Two real instances shipped before either
|
||||
was caught: `bin/write-docs` routed twice to a deleted `to-prd` skill, and `bin/triage` told an agent
|
||||
to run a nonexistent `/setup-matt-pocock-skills`. Both were found by reading, not by a gate, during
|
||||
the #99 retrofit and its follow-up audit; both were fixed in `03abcff`. **The fix this amendment
|
||||
records is the gate, not those two edits** (issue #124).
|
||||
|
||||
The body gate is a **separate, narrower** extractor (`body_targets()` /
|
||||
`unresolved_body_targets()`), not the description resolver reused at wider scope. The description
|
||||
resolver's sentence-level heuristics — `BOUNDARY_MARKER`, the follower test, in-sentence
|
||||
corroboration — are tuned for a one-to-three-sentence routing clause and misfire on dispatch-table
|
||||
and procedure prose in both directions: under-firing on a table row that carries no "do not" /
|
||||
"instead", over-firing on a procedure step naming a file, a CLI verb or a config key exactly the way
|
||||
a route names a skill. Retuning those heuristics for the body genre was considered and rejected as
|
||||
the harder half of the problem, with a materially worse cost of getting it wrong (a body is loaded
|
||||
on every invocation, so a false-positive-prone body gate is felt far more often than a
|
||||
false-positive-prone description gate).
|
||||
|
||||
So the body gate reads **only** explicit route notation — `/name` and backticked-or-slash-prefixed
|
||||
`-> name` / `→ name` — already the description gate's own unconditionally-blocking tier, and nothing
|
||||
softer: no SUGGESTION tier, no bare-word forms, no corroboration. Two further restrictions, both
|
||||
earned by a real corpus false positive rather than assumed up front:
|
||||
|
||||
- **the target must be hyphenated**, even in notation. `` `/fork` `` (`forge/SKILL.md`, citing
|
||||
Claude Code's own `/fork` subagent command) and `` `/name` `` (`skill-author/SKILL.md`, a
|
||||
placeholder for the skill's own name) are real corpus citations of a tool or a placeholder, not
|
||||
routes, and both hard-FAILed with no escape hatch before this restriction. This is the same
|
||||
"single-word targets are ordinary English" trade the Decision section already makes for the bare
|
||||
form, extended to notation because the body genre has no boundary-sentence signal to fall back on;
|
||||
- **a bare hyphenated word after any arrow is not notation.** The description gate's own bare-arrow
|
||||
sweep (`NOTATION_ARROW`) reads ordinary process-chain prose as a route: `caveman`'s "Inline obj
|
||||
prop -> new ref -> re-render." dangled to `re-render` under it. The body gate uses `ARROW_MARKED`
|
||||
instead, which requires the target to be backticked or slash-prefixed — true of the one real
|
||||
historical target (`` -> `to-prd` ``, confirmed against `03abcff`'s diff), so this costs no real
|
||||
coverage;
|
||||
- a target immediately preceded by `<` is a closing tag (`</what-to-do>`, `<supporting-info>` — this
|
||||
repo's own `grill-with-docs/SKILL.md` uses these as prompt section delimiters), not `/name`
|
||||
notation, and is discarded on that basis alone.
|
||||
|
||||
Both consumers — `scripts/skill-size-check.sh` and `factory-audit/scripts/lib-checks-skill.sh` —
|
||||
call the shared functions independently over the same `known_targets()` universe the description
|
||||
check already computed, so a body target folds into the existing "DID NOT RUN" INFO tier rather than
|
||||
adding a second one. `tests/test-adr0020-targets.sh` pins the two live true positives, all three
|
||||
guards above, and the fenced-code-block mask; the corpus-wide dangling assertion now covers body
|
||||
targets the same way it already covered description ones. `docs/spec/gates.md`'s "Body-level routing
|
||||
targets" section states the enforced shape in full.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Editing any non-compliant skill now requires retrofitting it first.** At decision time, 30 of 39
|
||||
|
||||
143
docs/adr/0028-research-doc-names-the-research-registry.md
Normal file
143
docs/adr/0028-research-doc-names-the-research-registry.md
Normal file
@@ -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 <sha>)` 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:** <path>` 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 <sha>)`. 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.
|
||||
@@ -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**
|
||||
|
||||
@@ -359,6 +360,68 @@ at a real sentence end. **Read the second bullet forward as well as back:** a ba
|
||||
after a dotted filename is now extracted, resolved, and a blocking ERROR when it dangles, where the
|
||||
same clause used to pass unchecked in silence.
|
||||
|
||||
### Body-level routing targets (issue #124)
|
||||
|
||||
Everything above resolves targets named in the **description** — the one field `boundary_targets()`
|
||||
and `unresolved_targets()` read. Until issue #124, a target named in the **body** — a dispatch table
|
||||
or a "run X" step, both routine in a 900-word procedure — was checked by nothing: `bin/write-docs`
|
||||
routed twice to a deleted `to-prd` skill and `bin/triage` told an agent to run a nonexistent
|
||||
`/setup-matt-pocock-skills`, and both were found by reading, not by any gate (fixed in `03abcff`;
|
||||
the gate itself is the ask this section documents).
|
||||
|
||||
`body_targets()` / `unresolved_body_targets()` (`lib-boundary-resolver.sh`) are a **separate,
|
||||
narrower** extractor, not a reuse of the description one at wider scope. A body is dispatch-table
|
||||
and procedure prose, not a one-to-three-sentence routing clause, so `BOUNDARY_MARKER`, the follower
|
||||
test and in-sentence corroboration all misfire on it in both directions — under-firing on a table
|
||||
row that carries no "do not"/"instead", over-firing on a procedure step that names a file, a CLI verb
|
||||
or a config key exactly the way a route names a skill. So the body gate reads only **notation**,
|
||||
already the description gate's own "always blocks" tier, and nothing softer:
|
||||
|
||||
| Form | Pattern | Requires |
|
||||
|---|---|---|
|
||||
| `/name` | `NOTATION_SLASH` | a hyphen in `name`; not preceded by `<` |
|
||||
| `-> name` / `→ name` | `ARROW_MARKED` | the name **backticked or slash-prefixed** — `NOTATION_ARROW`'s bare form is not used here |
|
||||
|
||||
Both constraints exist because the corpus, not intuition, said so — each is a real false positive
|
||||
this gate produced once and was narrowed to remove:
|
||||
|
||||
- **No SUGGESTION tier, no continuation, one arrow per target.** Both forms are notation, and
|
||||
notation is unconditionally blocking — there is no ambiguous prose reading left to soften, so
|
||||
there is nothing to report at a softer tier. `CONT_MARKED`/`CONT_ANY` are not run either, so
|
||||
`-> \`a\` or \`b\`` resolves only `a`, same as the one-arrow-one-target convention **#107** already
|
||||
states for descriptions — enforced here by construction instead of by a second SUGGESTION.
|
||||
- **A bare hyphenated word after any arrow is not notation here.** `NOTATION_ARROW` (used for the
|
||||
description gate's own `Not X -> name` sweep) matches a bare `-> name` unconditionally, and a body
|
||||
is full of ordinary arrow prose that is not a route: `caveman`'s own `Inline obj prop -> new ref ->
|
||||
re-render.` read as a dangling route to `re-render` under that pattern. `ARROW_MARKED` requires the
|
||||
target to be backticked or slash-prefixed, which the one real historical target (`` -> `to-prd` ``,
|
||||
per `03abcff`'s diff) already was, so the narrowing costs no real coverage.
|
||||
- **A single-word target is discarded, even in notation.** `` `/fork` `` (`forge/SKILL.md`,
|
||||
contrasting `context: fork` with Claude Code's own `/fork` subagent command) and `` `/name` ``
|
||||
(`skill-author/SKILL.md`, "the user types `/name`" — a placeholder for the skill's *own* name, not
|
||||
a route) are both real corpus citations of a tool or a placeholder, not routes, and both hard-FAILed
|
||||
with no escape hatch before the hyphen requirement was added. This is a real, accepted recall loss:
|
||||
a body dispatch entry to a genuinely single-word skill (`forge`, `research`, `triage`, `tdd`,
|
||||
`prototype`) cannot be checked through this extractor. Same trade the description gate already
|
||||
makes for the *bare* form (the known gap above), extended here to notation as well because the body
|
||||
genre has no boundary-sentence signal to lean on instead.
|
||||
- **A name immediately preceded by `<` is a closing tag, not a route.** `grill-with-docs/SKILL.md`
|
||||
uses XML-style prompt delimiters (`<what-to-do>...</what-to-do>`, `<supporting-info>...`), and
|
||||
`</what-to-do>` is indistinguishable from `/what-to-do` notation by every other rule above. No route
|
||||
is ever written directly after `<` in this corpus, so the guard costs nothing else.
|
||||
|
||||
Fenced code blocks are masked first (`mask_fenced()`, the same masking `gotcha_stats()` and the
|
||||
references/-pointer check already use): an illustrative ` ```/some-skill``` ` in `skill-author` or
|
||||
`factory-audit` — which document this very notation — is not a live dispatch entry.
|
||||
|
||||
Both consumers agree by construction: `scripts/skill-size-check.sh` and
|
||||
`factory-audit/scripts/lib-checks-skill.sh` each call `body_targets()`/`unresolved_body_targets()`
|
||||
independently, over the same `known_targets()` universe the description check already computed, so
|
||||
the "DID NOT RUN" INFO tier covers both description and body targets in one message rather than
|
||||
firing twice. `tests/test-adr0020-targets.sh`'s "body-level routing targets (issue #124)" section
|
||||
pins both the two live true positives and every guard above; the corpus-wide dangling assertion
|
||||
(`EXPECTED_DANGLING`) covers body targets the same way it already covered description ones.
|
||||
|
||||
### SUGGESTION-only checks
|
||||
|
||||
Deterministic to measure, judgment to act on:
|
||||
@@ -576,6 +639,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 <dir>`.
|
||||
|
||||
## 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
|
||||
|
||||
@@ -9,7 +9,7 @@ apm is the only supported install path (ADR-0024). Declare this package in the c
|
||||
```yaml
|
||||
dependencies:
|
||||
apm:
|
||||
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
|
||||
- git: git@git.rkdr.net:Defame1297/holocron.git
|
||||
path: plugins/bin
|
||||
```
|
||||
|
||||
@@ -19,7 +19,7 @@ Then:
|
||||
apm install
|
||||
```
|
||||
|
||||
The entry above is unpinned and tracks the remote's default branch — add `ref: <tag>` to pin a release. Registering the catalogue instead (`apm marketplace add git@git.dev.rkdr.net:Defame1297/holocron.git --name holocron`) gets you the `bin@holocron` short name, but writes to `~/.apm/marketplaces.json` at user scope; the git+path object needs nothing beyond the manifest.
|
||||
The entry above is unpinned and tracks the remote's default branch — add `ref: <tag>` to pin a release. Registering the catalogue instead (`apm marketplace add git@git.rkdr.net:Defame1297/holocron.git --name holocron`) gets you the `bin@holocron` short name, but writes to `~/.apm/marketplaces.json` at user scope; the git+path object needs nothing beyond the manifest.
|
||||
|
||||
**Native plugin installs do not work.** This package ships no per-plugin manifest and no flat content directories, so a host that installs it natively gets zero skills — and Claude Code raises no error while doing it (ADR-0024).
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
name: bin
|
||||
version: 1.1.8
|
||||
version: 1.1.9
|
||||
description: Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.
|
||||
author:
|
||||
name: Defame1297
|
||||
email: defame1297@rkdr.net
|
||||
url: https://git.dev.rkdr.net/Defame1297/
|
||||
url: https://git.rkdr.net/Defame1297/
|
||||
license: MIT
|
||||
homepage: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/bin
|
||||
repository: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/bin
|
||||
homepage: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/bin
|
||||
repository: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/bin
|
||||
keywords:
|
||||
- utility
|
||||
- diagnostics
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -9,7 +9,7 @@ apm is the only supported install path (ADR-0024). Declare this package in the c
|
||||
```yaml
|
||||
dependencies:
|
||||
apm:
|
||||
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
|
||||
- git: git@git.rkdr.net:Defame1297/holocron.git
|
||||
path: plugins/core
|
||||
```
|
||||
|
||||
@@ -19,7 +19,7 @@ Then:
|
||||
apm install
|
||||
```
|
||||
|
||||
The entry above is unpinned and tracks the remote's default branch — add `ref: <tag>` to pin a release. Registering the catalogue instead (`apm marketplace add git@git.dev.rkdr.net:Defame1297/holocron.git --name holocron`) gets you the `core@holocron` short name, but writes to `~/.apm/marketplaces.json` at user scope; the git+path object needs nothing beyond the manifest.
|
||||
The entry above is unpinned and tracks the remote's default branch — add `ref: <tag>` to pin a release. Registering the catalogue instead (`apm marketplace add git@git.rkdr.net:Defame1297/holocron.git --name holocron`) gets you the `core@holocron` short name, but writes to `~/.apm/marketplaces.json` at user scope; the git+path object needs nothing beyond the manifest.
|
||||
|
||||
**Native plugin installs do not work.** This package ships no per-plugin manifest and no flat content directories, so a host that installs it natively gets zero skills — and Claude Code raises no error while doing it (ADR-0024).
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
name: core
|
||||
version: 1.1.3
|
||||
version: 1.1.4
|
||||
description: Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it.
|
||||
author:
|
||||
name: Defame1297
|
||||
email: defame1297@rkdr.net
|
||||
url: https://git.dev.rkdr.net/Defame1297/
|
||||
url: https://git.rkdr.net/Defame1297/
|
||||
license: MIT
|
||||
homepage: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/core
|
||||
repository: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/core
|
||||
homepage: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/core
|
||||
repository: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/core
|
||||
keywords:
|
||||
- agents-md
|
||||
- documentation
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -9,7 +9,7 @@ apm is the only supported install path (ADR-0024). Declare this package in the c
|
||||
```yaml
|
||||
dependencies:
|
||||
apm:
|
||||
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
|
||||
- git: git@git.rkdr.net:Defame1297/holocron.git
|
||||
path: plugins/git
|
||||
```
|
||||
|
||||
@@ -19,7 +19,7 @@ Then:
|
||||
apm install
|
||||
```
|
||||
|
||||
The entry above is unpinned and tracks the remote's default branch — add `ref: <tag>` to pin a release. Registering the catalogue instead (`apm marketplace add git@git.dev.rkdr.net:Defame1297/holocron.git --name holocron`) gets you the `git@holocron` short name, but writes to `~/.apm/marketplaces.json` at user scope; the git+path object needs nothing beyond the manifest.
|
||||
The entry above is unpinned and tracks the remote's default branch — add `ref: <tag>` to pin a release. Registering the catalogue instead (`apm marketplace add git@git.rkdr.net:Defame1297/holocron.git --name holocron`) gets you the `git@holocron` short name, but writes to `~/.apm/marketplaces.json` at user scope; the git+path object needs nothing beyond the manifest.
|
||||
|
||||
**Native plugin installs do not work.** This package ships no per-plugin manifest and no flat content directories, so a host that installs it natively gets zero skills and zero agents — and Claude Code raises no error while doing it (ADR-0024).
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
name: git
|
||||
version: 1.3.8
|
||||
version: 1.3.9
|
||||
description: Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it.
|
||||
author:
|
||||
name: Defame1297
|
||||
email: defame1297@rkdr.net
|
||||
url: https://git.dev.rkdr.net/Defame1297/
|
||||
url: https://git.rkdr.net/Defame1297/
|
||||
license: MIT
|
||||
homepage: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/git
|
||||
repository: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/git
|
||||
homepage: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/git
|
||||
repository: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/git
|
||||
keywords:
|
||||
- git
|
||||
- vcs
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
name: gitea
|
||||
version: 1.3.9
|
||||
version: 1.3.10
|
||||
description: Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone.
|
||||
author:
|
||||
name: Defame1297
|
||||
email: defame1297@rkdr.net
|
||||
url: https://git.dev.rkdr.net/Defame1297/
|
||||
url: https://git.rkdr.net/Defame1297/
|
||||
license: MIT
|
||||
homepage: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/gitea
|
||||
repository: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/gitea
|
||||
homepage: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/gitea
|
||||
repository: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/gitea
|
||||
keywords:
|
||||
- gitea
|
||||
- issues
|
||||
|
||||
@@ -7,7 +7,7 @@ description: >
|
||||
fixes -> agent-author.
|
||||
allowed-tools: Bash Read
|
||||
metadata:
|
||||
version: "1.0.3"
|
||||
version: "1.0.5"
|
||||
category: factory
|
||||
source_keys:
|
||||
- agentskills-home
|
||||
|
||||
@@ -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 <sha>)`.
|
||||
- **`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.
|
||||
|
||||
@@ -898,6 +898,103 @@ def unresolved_targets(description, known):
|
||||
reported.add(name)
|
||||
return sorted(blocking), sorted(reported - blocking)
|
||||
|
||||
|
||||
# --- Body-level routing targets (issue #124) -------------------------------
|
||||
# boundary_targets()/unresolved_targets() above are tuned for a description:
|
||||
# one to three sentences, where BOUNDARY_MARKER, the follower test and
|
||||
# in-sentence corroboration all exist to tell a routing sentence apart from
|
||||
# ordinary prose about a hyphenated tool. A SKILL.md body is a different
|
||||
# genre — up to 900 words of procedure and dispatch tables — where those same
|
||||
# heuristics would misfire in both directions: a dispatch table rarely reads
|
||||
# as a "boundary sentence" (under-fire), and a procedure step naming a file, a
|
||||
# CLI verb or a config key looks exactly like a route (over-fire). Retuning
|
||||
# the sentence-level heuristics for that genre is the hard half of this gate
|
||||
# and is deliberately NOT attempted here — see the issue for why.
|
||||
#
|
||||
# So the body extractor takes the narrow route instead: only two EXPLICIT
|
||||
# ROUTE NOTATION forms count, and each is measured against the real corpus
|
||||
# (39 SKILL.md bodies) rather than assumed correct from the description gate's
|
||||
# behaviour — a body is dense with prose that LOOKS like this notation and
|
||||
# genuinely is not, in ways a one-to-three-sentence description never is:
|
||||
#
|
||||
# * ARROW_MARKED — `-> name` / `→ name` where the target is BACKTICKED or
|
||||
# slash-prefixed (MARKED_TARGET). NOT NOTATION_ARROW, which matches a bare
|
||||
# hyphenated word after any arrow: the corpus's own process-chain prose
|
||||
# ("Inline obj prop -> new ref -> re-render.", caveman/SKILL.md) reads as
|
||||
# a route under that pattern and does not under this one, because a
|
||||
# process chain is never itself backticked or slash-prefixed. The one
|
||||
# live true positive this was filed over, write-docs' "-> `to-prd`", IS
|
||||
# backticked (03abcff's diff shows the original), so ARROW_MARKED still
|
||||
# catches it losslessly.
|
||||
# * NOTATION_SLASH — free-standing `/name`, unconditionally, the same
|
||||
# pattern the description gate sweeps with. Two guards narrow it for body
|
||||
# text specifically, each one measured against a real corpus false
|
||||
# positive rather than hypothesised:
|
||||
# - a name with NO hyphen is discarded. A real dispatch entry in this
|
||||
# corpus always names a multi-word skill (`to-prd`,
|
||||
# `setup-matt-pocock-skills`); a single bare or backticked word after
|
||||
# a `/` is prose citing a CLI command, a Claude Code built-in or a
|
||||
# placeholder — `` `/fork` `` (forge/SKILL.md, contrasting
|
||||
# `context: fork` with Claude Code's own /fork subagent command) and
|
||||
# `` `/name` `` (skill-author/SKILL.md, "the user types `/name`" —
|
||||
# `name` is a placeholder for the skill's OWN name, not a route) are
|
||||
# both real corpus hits this guard removes. This is a real recall
|
||||
# loss — `/forge`, `/triage` and other single-word skill names are
|
||||
# unreachable through this extractor — accepted deliberately, the
|
||||
# same "start narrow" trade the issue itself recommends.
|
||||
# - a name immediately preceded by `<` is discarded. An XML/HTML-style
|
||||
# closing tag used as a prompt section delimiter — `</what-to-do>`,
|
||||
# `</supporting-info>` (grill-with-docs/SKILL.md) — is indistinguishable
|
||||
# from `/what-to-do` notation by every other rule in this pattern; no
|
||||
# route is ever written directly after `<` in this corpus, so the
|
||||
# guard costs nothing else.
|
||||
#
|
||||
# Every surviving hit is unconditionally blocking: both forms are explicit
|
||||
# notation with the ambiguous single-word and closing-tag readings already
|
||||
# removed, so there is no SUGGESTION tier here — that tier exists to soften
|
||||
# an ambiguous prose form, and none is admitted at this point.
|
||||
#
|
||||
# No conjunction continuation (CONT_*) either: `-> \`to-prd\` or \`grill-me\``
|
||||
# resolves only `to-prd`, the same one-arrow-one-target convention
|
||||
# multi_target_arrow_clauses() already enforces on descriptions (issue #107),
|
||||
# applied here by construction instead of by a second SUGGESTION.
|
||||
def body_targets(body):
|
||||
"""Every /name or -> `name` routing target named in a SKILL.md body.
|
||||
|
||||
Fenced code blocks are masked first, the same way gotcha_stats() and
|
||||
missing_reference_pointers() mask them: a ```-fenced example quoting
|
||||
`/some-skill` or `-> \`some-skill\`` as illustration is not a live
|
||||
dispatch entry, and skill-author/factory-audit — which document this
|
||||
very notation — are exactly the skills most likely to carry one.
|
||||
"""
|
||||
masked = mask_fenced(body)
|
||||
names = set()
|
||||
for match in NOTATION_SLASH.finditer(masked):
|
||||
if match.start() > 0 and masked[match.start() - 1] == '<':
|
||||
continue # </closing-tag>, not /route-notation
|
||||
name = match.group(1)
|
||||
if '-' in name:
|
||||
names.add(name)
|
||||
for match in ARROW_MARKED.finditer(masked):
|
||||
name, _, _ = _first(match)
|
||||
if name and '-' in name:
|
||||
names.add(name)
|
||||
return sorted(names)
|
||||
|
||||
|
||||
def unresolved_body_targets(body, known):
|
||||
"""Body routing targets (notation only) that resolve to nothing.
|
||||
|
||||
Unlike unresolved_targets(), this has one outcome, not two: every name
|
||||
body_targets() finds is already route notation, and notation always
|
||||
blocks. `known` is the resolved universe from known_targets(); passing an
|
||||
empty set is not meaningful — callers check for that first and decline
|
||||
out loud instead, exactly as they do for the description gate.
|
||||
"""
|
||||
return sorted(name for name in body_targets(body)
|
||||
if normalize_target(name) not in known)
|
||||
|
||||
|
||||
# --- Frontmatter ----------------------------------------------------------
|
||||
# Tolerant on the way in, HARD-FAILING on the way out. A UTF-8 BOM, a leading
|
||||
# blank line, trailing whitespace after either `---`, or CRLF line endings all
|
||||
|
||||
@@ -443,39 +443,56 @@ elif desc:
|
||||
# derived from this script's own path, and — when an authoring root exists — it
|
||||
# never reads a deployed .claude/ tree, so a fresh clone and a machine that has
|
||||
# run `apm install` return the same verdict. See the shared resolver's header.
|
||||
if desc:
|
||||
routing_targets = boundary_targets(desc)
|
||||
known = known_targets(skill_dir) if routing_targets else set()
|
||||
if routing_targets and not known:
|
||||
routing_targets = boundary_targets(desc) if desc else []
|
||||
# Body-level targets (issue #124): notation only (`/name`, `-> name`), so
|
||||
# every hit is unconditionally blocking — see the shared resolver's
|
||||
# body_targets() header for why the description gate's SUGGESTION tier has
|
||||
# no counterpart here. Read regardless of `desc`: a body dispatch table can
|
||||
# carry a broken route even when the description carries none.
|
||||
body_routing_targets = body_targets(body)
|
||||
if routing_targets or body_routing_targets:
|
||||
known = known_targets(skill_dir)
|
||||
if not known:
|
||||
unchecked = sorted(set(routing_targets) | set(body_routing_targets))
|
||||
info(f"boundary-target resolution DID NOT RUN — no skill universe could be "
|
||||
f"determined for this path (no authoring root above it, no apm package "
|
||||
f"root, no declared apm dependencies, no deployed .claude/ or .agents/ "
|
||||
f"tree). Unchecked target(s): {', '.join(routing_targets)}")
|
||||
elif routing_targets:
|
||||
# blocking vs reported: a target only earns a FAIL when it is written in
|
||||
# route notation or its own sentence corroborates it by naming another
|
||||
# target that resolves. See the shared resolver's CORROBORATION note.
|
||||
unresolved, soft = unresolved_targets(desc, known)
|
||||
for target in unresolved:
|
||||
fail(f"description routes to '{target}', which resolves to no skill or agent "
|
||||
f"in this monorepo, in this package, or in a package it declares in "
|
||||
f"apm.yml dependencies.apm — a boundary clause naming a non-existent "
|
||||
f"target sends the router nowhere")
|
||||
for target in soft:
|
||||
suggest(f"description routes to '{target}', which resolves to no skill or agent "
|
||||
f"in this monorepo, in this package, or in a package it declares in "
|
||||
f"apm.yml dependencies.apm — SUGGESTION rather than FAIL because nothing "
|
||||
f"else in that sentence resolves, so it is equally likely to be a tool, a "
|
||||
f"file format or an English compound. If it IS a route, write it as "
|
||||
f"`/{target}` or `-> {target}` and it will be checked properly")
|
||||
if not unresolved:
|
||||
# Counts the targets that ACTUALLY resolve, not every target found:
|
||||
# a confirm-only target (one used attributively — see the resolver's
|
||||
# ATTRIBUTIVE USE note) is exempt from the failure above, so
|
||||
# reporting it as resolved would be a false claim.
|
||||
resolved = [t for t in routing_targets if normalize_target(t) in known]
|
||||
ok(f"{len(resolved)} of {len(routing_targets)} boundary target(s) resolve: "
|
||||
f"{', '.join(resolved) if resolved else '(none)'}")
|
||||
f"tree). Unchecked target(s): {', '.join(unchecked)}")
|
||||
else:
|
||||
if routing_targets:
|
||||
# blocking vs reported: a target only earns a FAIL when it is written in
|
||||
# route notation or its own sentence corroborates it by naming another
|
||||
# target that resolves. See the shared resolver's CORROBORATION note.
|
||||
unresolved, soft = unresolved_targets(desc, known)
|
||||
for target in unresolved:
|
||||
fail(f"description routes to '{target}', which resolves to no skill or agent "
|
||||
f"in this monorepo, in this package, or in a package it declares in "
|
||||
f"apm.yml dependencies.apm — a boundary clause naming a non-existent "
|
||||
f"target sends the router nowhere")
|
||||
for target in soft:
|
||||
suggest(f"description routes to '{target}', which resolves to no skill or agent "
|
||||
f"in this monorepo, in this package, or in a package it declares in "
|
||||
f"apm.yml dependencies.apm — SUGGESTION rather than FAIL because nothing "
|
||||
f"else in that sentence resolves, so it is equally likely to be a tool, a "
|
||||
f"file format or an English compound. If it IS a route, write it as "
|
||||
f"`/{target}` or `-> {target}` and it will be checked properly")
|
||||
if not unresolved:
|
||||
# Counts the targets that ACTUALLY resolve, not every target found:
|
||||
# a confirm-only target (one used attributively — see the resolver's
|
||||
# ATTRIBUTIVE USE note) is exempt from the failure above, so
|
||||
# reporting it as resolved would be a false claim.
|
||||
resolved = [t for t in routing_targets if normalize_target(t) in known]
|
||||
ok(f"{len(resolved)} of {len(routing_targets)} boundary target(s) resolve: "
|
||||
f"{', '.join(resolved) if resolved else '(none)'}")
|
||||
unresolved_body = unresolved_body_targets(body, known)
|
||||
for target in unresolved_body:
|
||||
fail(f"body routes to '{target}' (`/{target}` or `-> {target}` notation), which "
|
||||
f"resolves to no skill or agent in this monorepo, in this package, or in a "
|
||||
f"package it declares in apm.yml dependencies.apm — a dispatch table or "
|
||||
f"\"run X\" step naming a non-existent target sends the agent nowhere")
|
||||
if body_routing_targets and not unresolved_body:
|
||||
ok(f"{len(body_routing_targets)} of {len(body_routing_targets)} body routing "
|
||||
f"target(s) resolve: {', '.join(body_routing_targets)}")
|
||||
|
||||
# Body unfilled placeholders
|
||||
fill_matches = PLACEHOLDER_RE.findall(body)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <sha>)' 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 <sha>) 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_name>:**' 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:** <path-or-(none)>' to the '## {slug}' entry in references/sources.md."
|
||||
f"Add '- **Research doc:** <path to the plugin's research sources.md>' to the '## {slug}' entry in references/sources.md, "
|
||||
f"or '- **Research doc:** none' plus a '- **Basis:** <repo path>' 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:** <repo path>' 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:** <repo path>' 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:** <repo path>' line per path."
|
||||
)
|
||||
elif BASIS_REMOVED_RE.search(basis):
|
||||
# A path the entry HISTORICALLY rested on, annotated
|
||||
# '(removed in <sha>)' 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. '<registry path> (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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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 <sha>)` once the file is deleted).
|
||||
|
||||
@@ -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:** <path>` where `<path>` 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/<topic>/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:** <path>` line. `<path>` 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/<topic>/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. `<registry path> (digest: <full plugins/... path of the topic doc>)`, 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:** <repo path>` line per path. Each Basis path is checked to exist; annotate one
|
||||
that has since been deleted `(removed in <sha>)` 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`
|
||||
|
||||
@@ -9,7 +9,7 @@ apm is the only supported install path (ADR-0024). Declare this package in the c
|
||||
```yaml
|
||||
dependencies:
|
||||
apm:
|
||||
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
|
||||
- git: git@git.rkdr.net:Defame1297/holocron.git
|
||||
path: plugins/kyberforge
|
||||
```
|
||||
|
||||
@@ -19,7 +19,7 @@ Then:
|
||||
apm install
|
||||
```
|
||||
|
||||
The entry above is unpinned and tracks the remote's default branch — add `ref: <tag>` to pin a release. Registering the catalogue instead (`apm marketplace add git@git.dev.rkdr.net:Defame1297/holocron.git --name holocron`) gets you the `kyberforge@holocron` short name, but writes to `~/.apm/marketplaces.json` at user scope; the git+path object needs nothing beyond the manifest.
|
||||
The entry above is unpinned and tracks the remote's default branch — add `ref: <tag>` to pin a release. Registering the catalogue instead (`apm marketplace add git@git.rkdr.net:Defame1297/holocron.git --name holocron`) gets you the `kyberforge@holocron` short name, but writes to `~/.apm/marketplaces.json` at user scope; the git+path object needs nothing beyond the manifest.
|
||||
|
||||
**Native plugin installs do not work.** This package ships no per-plugin manifest and no flat content directories, so a host that installs it natively gets zero skills, agents and hooks — and Claude Code raises no error while doing it (ADR-0024).
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
name: kyberforge
|
||||
version: 2.0.0
|
||||
version: 2.0.1
|
||||
description: Skills and agents for creating, maintaining, and managing an apm plugin marketplace for Claude Code and GitHub Copilot.
|
||||
author:
|
||||
name: Defame1297
|
||||
email: defame1297@rkdr.net
|
||||
url: https://git.dev.rkdr.net/Defame1297/
|
||||
url: https://git.rkdr.net/Defame1297/
|
||||
license: MIT
|
||||
homepage: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/kyberforge
|
||||
repository: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/kyberforge
|
||||
homepage: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/kyberforge
|
||||
repository: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/kyberforge
|
||||
keywords:
|
||||
- marketplace
|
||||
- plugin
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.<key>` (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:
|
||||
|
||||
|
||||
@@ -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.<key>` 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.<key>` 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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.<key>` 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.<key>` 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`
|
||||
|
||||
@@ -51,8 +51,7 @@ suppression syntax:
|
||||
| `[formats]` maps `mdx = md` (what `vale-config` recommends) | none | Markdown | `<!-- vale off -->` |
|
||||
| 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 |
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
name: lint
|
||||
version: 1.1.8
|
||||
version: 1.1.9
|
||||
description: Skills and agents for configuring and running linters.
|
||||
author:
|
||||
name: Defame1297
|
||||
email: defame1297@rkdr.net
|
||||
url: https://git.dev.rkdr.net/Defame1297/
|
||||
url: https://git.rkdr.net/Defame1297/
|
||||
license: MIT
|
||||
homepage: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/lint
|
||||
repository: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/lint
|
||||
homepage: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/lint
|
||||
repository: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/lint
|
||||
keywords:
|
||||
- lint
|
||||
- style
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
name: onedev
|
||||
version: 0.1.0
|
||||
version: 0.1.1
|
||||
description: Skills and agents for working with a OneDev forge through the TOD CLI — the forge's own objects, as distinct from the local git clone.
|
||||
author:
|
||||
name: Defame1297
|
||||
email: defame1297@rkdr.net
|
||||
url: https://git.dev.rkdr.net/Defame1297/
|
||||
url: https://git.rkdr.net/Defame1297/
|
||||
license: MIT
|
||||
homepage: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/onedev
|
||||
repository: https://git.dev.rkdr.net/Defame1297/holocron/src/branch/main/plugins/onedev
|
||||
homepage: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/onedev
|
||||
repository: https://git.rkdr.net/Defame1297/holocron/src/branch/main/plugins/onedev
|
||||
keywords:
|
||||
- onedev
|
||||
- tod
|
||||
|
||||
101
scripts/check-provenance-corpus.sh
Executable file
101
scripts/check-provenance-corpus.sh
Executable file
@@ -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."
|
||||
@@ -444,30 +444,42 @@ for path in files:
|
||||
"\"Not X -> %s. Not Y -> %s.\"" % (path, first, second, first, second))
|
||||
|
||||
targets = boundary_targets(desc)
|
||||
if targets:
|
||||
# Body-level targets (issue #124): notation only (`/name`, `-> name`), so
|
||||
# every hit is unconditionally blocking — see body_targets()'s header for
|
||||
# why the description gate's SUGGESTION tier has no counterpart here.
|
||||
body_route_names = body_targets(body)
|
||||
if targets or body_route_names:
|
||||
known = known_targets(skill_dir)
|
||||
if known:
|
||||
blocking, reported = unresolved_targets(desc, known)
|
||||
for target in blocking:
|
||||
error("%s: description routes to '%s', which does not resolve to a skill "
|
||||
"or agent in this monorepo, in this package, or in a package it "
|
||||
"declares in apm.yml dependencies.apm (ADR-0020). A boundary clause "
|
||||
"that names a non-existent target sends the router nowhere."
|
||||
% (path, target))
|
||||
for target in reported:
|
||||
suggest("%s: description routes to '%s', which does not resolve to a skill "
|
||||
"or agent in this monorepo, in this package, or in a package it "
|
||||
"declares in apm.yml dependencies.apm (ADR-0020). SUGGESTION rather "
|
||||
"than a hard failure because nothing else in the sentence resolves, "
|
||||
"so this is equally likely to be a tool, a file format or an English "
|
||||
"compound. If it IS a route, write it as `/%s` or `-> %s` and it will "
|
||||
"be checked properly." % (path, target, target, target))
|
||||
if targets:
|
||||
blocking, reported = unresolved_targets(desc, known)
|
||||
for target in blocking:
|
||||
error("%s: description routes to '%s', which does not resolve to a skill "
|
||||
"or agent in this monorepo, in this package, or in a package it "
|
||||
"declares in apm.yml dependencies.apm (ADR-0020). A boundary clause "
|
||||
"that names a non-existent target sends the router nowhere."
|
||||
% (path, target))
|
||||
for target in reported:
|
||||
suggest("%s: description routes to '%s', which does not resolve to a skill "
|
||||
"or agent in this monorepo, in this package, or in a package it "
|
||||
"declares in apm.yml dependencies.apm (ADR-0020). SUGGESTION rather "
|
||||
"than a hard failure because nothing else in the sentence resolves, "
|
||||
"so this is equally likely to be a tool, a file format or an English "
|
||||
"compound. If it IS a route, write it as `/%s` or `-> %s` and it will "
|
||||
"be checked properly." % (path, target, target, target))
|
||||
for target in unresolved_body_targets(body, known):
|
||||
error("%s: body routes to '%s' (`/%s` or `-> %s` notation), which does not "
|
||||
"resolve to a skill or agent in this monorepo, in this package, or in a "
|
||||
"package it declares in apm.yml dependencies.apm (ADR-0020). A dispatch "
|
||||
"table or \"run X\" step naming a non-existent target sends the agent "
|
||||
"nowhere." % (path, target, target, target))
|
||||
else:
|
||||
unchecked = sorted(set(targets) | set(body_route_names))
|
||||
info("%s: boundary-target resolution DID NOT RUN — no skill universe "
|
||||
"could be determined for this path (no authoring root above it, no "
|
||||
"apm package root, no declared apm dependencies, no deployed "
|
||||
".claude/ or .agents/ tree). Unchecked target(s): %s"
|
||||
% (path, ", ".join(targets)))
|
||||
% (path, ", ".join(unchecked)))
|
||||
|
||||
sys.exit(1 if failed else 0)
|
||||
SSC_CHECKS_PY
|
||||
|
||||
@@ -46,6 +46,17 @@ fi
|
||||
# which is ADR-0024 consequence 2 arriving here. Keeping the exclusion now is
|
||||
# what stops that landing as a mystery double-run on the merge that enables it.
|
||||
#
|
||||
# build/ is excluded for the same reason again, one layer further out: `apm
|
||||
# pack` stages a full copy of a package's tree (including its skills' tests/
|
||||
# directories) under build/<package>-<version>/ before archiving it. Those
|
||||
# staged .bats files carry the same six-levels-up REPO_ROOT walk-up as any
|
||||
# other copy, which resolves past this repo's actual root and fails on a
|
||||
# missing bats-support helper -- the same failure mode apm_modules/ and
|
||||
# .claude/skills/ above already guard against, just from a different apm
|
||||
# subcommand. build/ is gitignored and regenerated on demand, so nothing here
|
||||
# depends on its contents; the exclusion only stops a stray local `apm pack`
|
||||
# output from being discovered and double-run.
|
||||
#
|
||||
# The walk runs from inside REPO_ROOT so the exclusions match paths RELATIVE to
|
||||
# it, the same universe the `git ls-files` grep below sees. Matched against
|
||||
# absolute paths, `*/.claude/worktrees/*` excluded every file whenever the
|
||||
@@ -61,6 +72,7 @@ done < <(
|
||||
-not -path "*/.claude/worktrees/*" \
|
||||
-not -path "*/apm_modules/*" \
|
||||
-not -path "*/.claude/skills/*" \
|
||||
-not -path "*/build/*" \
|
||||
| sort
|
||||
)
|
||||
|
||||
@@ -99,7 +111,7 @@ if [[ -n "$GIT_TOPLEVEL" && "$GIT_TOPLEVEL" == "$REPO_ROOT" ]]; then
|
||||
[[ -n "$f" ]] && EXPECTED_FILES+=("$REPO_ROOT/$f")
|
||||
done < <(
|
||||
git -C "$REPO_ROOT" ls-files -- '*.bats' \
|
||||
| grep -Ev '(^|/)tests/bats/|(^|/)test_helper/|(^|/)\.claude/worktrees/|(^|/)apm_modules/|(^|/)\.claude/skills/' \
|
||||
| grep -Ev '(^|/)tests/bats/|(^|/)test_helper/|(^|/)\.claude/worktrees/|(^|/)apm_modules/|(^|/)\.claude/skills/|(^|/)build/' \
|
||||
| sort || true
|
||||
)
|
||||
else
|
||||
|
||||
@@ -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': (
|
||||
|
||||
@@ -961,6 +961,117 @@ else
|
||||
fail "an attributive target naming a REAL skill produced output (exit $ATTR_RC): $ATTR_OUT"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Body-level routing targets (issue #124)
|
||||
# ---------------------------------------------------------------------------
|
||||
# boundary_targets()/unresolved_targets() are the DESCRIPTION gate, exercised
|
||||
# above. body_targets()/unresolved_body_targets() are the separate, narrower
|
||||
# extractor added for issue #124: a SKILL.md body is dispatch-table and
|
||||
# procedure prose, not a one-to-three-sentence routing clause, so the body
|
||||
# extractor takes only /name and -> `name` NOTATION (never the bare-prose
|
||||
# forms the description gate also reads), and even within notation, a target
|
||||
# must be hyphenated and must not be a `<tag` immediately before the `/`.
|
||||
# Every fixture is built inside a real plugin tree (BODY_ROOT), same as
|
||||
# section 2 above, so the resolver actually runs instead of declining.
|
||||
echo ""
|
||||
echo "--- body-level routing targets (issue #124) ---"
|
||||
BODY_ROOT="$TMPDIR_T/body"
|
||||
write_skill "$BODY_ROOT/plugins/p/.apm/skills/sibling-skill" sibling-skill \
|
||||
"Use when doing the other thing. Do not use for anything else."
|
||||
|
||||
# write_skill_body <skill-dir> <name> <body>
|
||||
write_skill_body() {
|
||||
mkdir -p "$1"
|
||||
{
|
||||
echo "---"
|
||||
echo "name: $2"
|
||||
echo "description: Use when doing the thing. Do not use for anything else."
|
||||
echo "metadata:"
|
||||
echo " version: \"1.0.0\""
|
||||
echo "---"
|
||||
echo ""
|
||||
printf '%s\n' "$3"
|
||||
} > "$1/SKILL.md"
|
||||
}
|
||||
|
||||
# body_case <slug> <expect: silent|errors> <needle> <body>
|
||||
body_case() {
|
||||
local slug="$1" mode="$2" needle="$3" body="$4" out status=0
|
||||
write_skill_body "$BODY_ROOT/plugins/p/.apm/skills/$slug" "$slug" "$body"
|
||||
set +e
|
||||
out="$(bash "$HOOK" "$BODY_ROOT/plugins/p/.apm/skills/$slug/SKILL.md" 2>&1)"
|
||||
status=$?
|
||||
set -e
|
||||
if [[ "$out" == *"DID NOT RUN"* ]]; then
|
||||
fail "body \"$body\" — the resolver declined, so this case asserts nothing about extraction: $out"
|
||||
return
|
||||
fi
|
||||
case "$mode" in
|
||||
silent)
|
||||
if [[ $status -eq 0 && -z "$out" ]]; then
|
||||
pass "not a dangling body target: \"$body\""
|
||||
else
|
||||
fail "body \"$body\" (exit $status, output: ${out:-<empty>})"
|
||||
fi
|
||||
;;
|
||||
errors)
|
||||
if [[ $status -ne 0 && "$out" == *"$needle"* ]]; then
|
||||
pass "dangling body target caught: \"$body\""
|
||||
else
|
||||
fail "body \"$body\" should have ERRORed with $needle (exit $status, output: ${out:-<empty>})"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# The two live true positives the issue was filed over, at fixture scale:
|
||||
# a bare/backticked `/name` and a backticked `-> \`name\``.
|
||||
body_case body-slash-dangling errors "body routes to 'no-such-body-skill'" \
|
||||
"Run \`/no-such-body-skill\` if the config is missing."
|
||||
body_case body-slash-resolves silent "" \
|
||||
"Run \`/sibling-skill\` if the config is missing."
|
||||
body_case body-arrow-dangling errors "body routes to 'no-such-arrow-body'" \
|
||||
"- User wants X -> \`no-such-arrow-body\`"
|
||||
body_case body-arrow-resolves silent "" \
|
||||
"- User wants X -> \`sibling-skill\`"
|
||||
|
||||
# No conjunction continuation: only the FIRST target after an arrow is ever
|
||||
# read, so a dangling SECOND name is silently uncounted rather than reported
|
||||
# — the same one-arrow-one-target convention issue #107 enforces on
|
||||
# descriptions (there, at SUGGESTION tier; here, by construction, since the
|
||||
# body gate has no SUGGESTION tier at all).
|
||||
body_case body-arrow-no-continuation silent "" \
|
||||
"- User wants X -> \`sibling-skill\` or \`no-such-uncounted-target\`"
|
||||
|
||||
# The single-word guard: a real corpus false positive removed by requiring a
|
||||
# hyphen. `` `/fork` `` (forge/SKILL.md) and `` `/name` `` (skill-author/SKILL.md)
|
||||
# are both single-word citations of a tool or a placeholder, not routes, and
|
||||
# both would otherwise have hard-FAILed with no escape hatch.
|
||||
body_case body-slash-single-word-guard silent "" \
|
||||
"See \`/fork\` for how the two differ."
|
||||
|
||||
# The closing-tag guard: an XML/HTML-style section delimiter used as a prompt
|
||||
# marker (grill-with-docs/SKILL.md's <what-to-do>...</what-to-do>) is
|
||||
# indistinguishable from /route notation by every other rule in the pattern —
|
||||
# a `<` immediately before the `/` is the one signal that tells them apart.
|
||||
body_case body-closing-tag-guard silent "" \
|
||||
$'<what-to-do>\nDo the thing.\n</what-to-do>'
|
||||
|
||||
# The bare-arrow guard: NOTATION_ARROW (bare hyphenated word after any arrow)
|
||||
# is deliberately NOT used here, only ARROW_MARKED (backticked or
|
||||
# slash-prefixed). caveman/SKILL.md's own process chain, "Inline obj prop ->
|
||||
# new ref -> re-render.", is real corpus prose this guard exists for — an
|
||||
# unbacked, unresolvable name after an arrow must stay silent, not become a
|
||||
# hard-blocking dangling-target FAIL with no suppression mechanism.
|
||||
body_case body-arrow-bare-not-notation silent "" \
|
||||
"Reproduce -> minimise -> no-such-bare-chain-target."
|
||||
|
||||
# Fenced code blocks are masked, same as gotcha_stats() and
|
||||
# missing_reference_pointers() mask them: an illustrative example is not a
|
||||
# live dispatch entry.
|
||||
body_case body-fenced-example silent "" \
|
||||
$'```\nRun /no-such-fenced-skill instead.\n```'
|
||||
|
||||
echo ""
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ $FAIL -eq 0 ]]
|
||||
|
||||
261
tests/test-check-provenance-corpus.sh
Executable file
261
tests/test-check-provenance-corpus.sh
Executable file
@@ -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 <repo> <name> <slug> <research-doc-value>
|
||||
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" <<EOF
|
||||
---
|
||||
name: $name
|
||||
description: A valid skill description.
|
||||
metadata:
|
||||
source_keys:
|
||||
- $slug
|
||||
---
|
||||
|
||||
## Step 1
|
||||
|
||||
Do the thing.
|
||||
EOF
|
||||
cat > "$skill/references/sources.md" <<EOF
|
||||
# Sources
|
||||
|
||||
## $slug
|
||||
|
||||
- **URL:** https://example.com/$slug
|
||||
- **Description:** A test source.
|
||||
- **Contributing files:** SKILL.md
|
||||
- **Research doc:** $research
|
||||
- **Status:** \`extracted\`
|
||||
EOF
|
||||
}
|
||||
|
||||
REGISTRY="plugins/p/docs/research/docs/t/sources.md"
|
||||
|
||||
# --- 1. A skill whose slug resolves in the registry passes, quietly ---
|
||||
echo ""
|
||||
echo "--- passing skill ---"
|
||||
R="$(make_repo)"
|
||||
make_skill "$R" good known-slug "$REGISTRY"
|
||||
if bash "$SCRIPT" "$R" > "$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 ]]
|
||||
@@ -476,6 +476,40 @@ else
|
||||
fail "the plan-shortfall run failed with the wrong count: $FAKE_OUT"
|
||||
fi
|
||||
|
||||
# --- 12. A build/ directory (apm pack's staging output) is excluded, the same
|
||||
# way apm_modules/ and .claude/skills/ above are. A stray local `apm pack` run
|
||||
# leaves build/<pkg>-<version>/ on disk holding a full copy of every packaged
|
||||
# skill's tests/ directory, gitignored and regenerable, but discoverable by a
|
||||
# bare `find` all the same. Those staged .bats files carry the same
|
||||
# several-levels-up REPO_ROOT walk-up as any other copy, which overshoots this
|
||||
# fixture's root, so an unexcluded build/ turns into the same
|
||||
# bats-support-not-found failure apm_modules/ and .claude/skills/ already guard
|
||||
# against -- this was caught live with 423 duplicate failures against a real
|
||||
# checkout holding a stray build/holocron-*/ from an earlier `apm pack`.
|
||||
echo ""
|
||||
echo "--- a build/ directory holding staged .bats copies is excluded ---"
|
||||
DIR12="$(make_fake_repo)"
|
||||
FIXTURES+=("$DIR12")
|
||||
seed_bats_files "$DIR12"
|
||||
mkdir -p "$DIR12/build/some-pkg-1.0.0/tests"
|
||||
printf '@test "staged" { false; }\n' > "$DIR12/build/some-pkg-1.0.0/tests/staged.bats"
|
||||
install_stub_bats "$DIR12" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
echo "1..1"
|
||||
echo "ok 1 first"
|
||||
exit 0
|
||||
EOF
|
||||
run_fake "$DIR12"
|
||||
if [[ $FAKE_RC -ne 0 ]]; then
|
||||
fail "a tree holding a build/ directory failed the run: $FAKE_OUT"
|
||||
elif grep -q "build/some-pkg-1.0.0" <<< "$FAKE_OUT"; then
|
||||
fail "a .bats file staged under build/ was discovered and run: $FAKE_OUT"
|
||||
elif grep -q "^2 tests, 0 failures$" <<< "$FAKE_OUT"; then
|
||||
pass "a build/ directory's staged .bats copies are excluded from discovery"
|
||||
else
|
||||
fail "the build/-exclusion run passed with an unexpected count: $FAKE_OUT"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ $FAIL -eq 0 ]]
|
||||
|
||||
220
tests/test-vale-3-15-2-behaviours.sh
Executable file
220
tests/test-vale-3-15-2-behaviours.sh
Executable file
@@ -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 <dir> <vale args...>: run vale from <dir>, 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 <label> <rc> <grep-fixed-pattern-or-empty>
|
||||
expect() {
|
||||
local label="$1" rc="$2" pat="${3:-}"
|
||||
if [[ "$RC" -ne "$rc" ]]; then
|
||||
fail "$label: exit $RC, want $rc"; echo "$OUT" | sed 's/^/ /'; return
|
||||
fi
|
||||
if [[ -n "$pat" ]] && ! grep -qF -- "$pat" <<<"$OUT"; then
|
||||
fail "$label: output lacks '$pat'"; echo "$OUT" | sed 's/^/ /'; return
|
||||
fi
|
||||
pass "$label"
|
||||
}
|
||||
|
||||
# tree <name>: fresh dir with styles/ and a one-line doc containing a repeated word.
|
||||
tree() {
|
||||
local d="$WORK/$1"; mkdir -p "$d/styles"
|
||||
printf 'This is the the sample.\n' >"$d/doc.md"
|
||||
echo "$d"
|
||||
}
|
||||
rule() { # rule <dir> <style>: a one-rule custom style flagging the word "foo"
|
||||
mkdir -p "$1/styles/$2"
|
||||
cat >"$1/styles/$2/Foo.yml" <<'Y'
|
||||
extends: existence
|
||||
message: "found '%s'"
|
||||
level: error
|
||||
tokens:
|
||||
- foo
|
||||
Y
|
||||
}
|
||||
|
||||
echo "1. BasedOnStyles names a style absent from StylesPath"
|
||||
d="$(tree c1)"
|
||||
printf 'StylesPath = styles\n[*.md]\nBasedOnStyles = Nope\n' >"$d/.vale.ini"
|
||||
run "$d" doc.md
|
||||
expect "E100 loadStyles, exit 2" 2 "E100 [loadStyles]"
|
||||
grep -qF "style 'Nope' does not exist on StylesPath" <<<"$OUT" && pass "message names the style" || fail "message names the style"
|
||||
|
||||
echo "2. vale sync for a name in BasedOnStyles but not Packages"
|
||||
d="$(tree c2)"
|
||||
printf 'StylesPath = styles\n[*.md]\nBasedOnStyles = Nope\n' >"$d/.vale.ini"
|
||||
run "$d" sync
|
||||
expect "Synced 0 package(s), exit 0" 0 "Synced 0 package(s)"
|
||||
[[ -z "$(ls -A "$d/styles")" ]] && pass "nothing downloaded" || fail "nothing downloaded"
|
||||
run "$d" doc.md
|
||||
expect "next lint repeats E100" 2 "E100 [loadStyles]"
|
||||
|
||||
echo "3. StylesPath directory absent, only built-in Vale active"
|
||||
d="$WORK/c3"; mkdir -p "$d"; printf 'x\n' >"$d/doc.md"
|
||||
printf 'StylesPath = nostyles\n[*.md]\nBasedOnStyles = Vale\n' >"$d/.vale.ini"
|
||||
run "$d" doc.md
|
||||
expect "E201, exit 2" 2 "E201 Invalid value"
|
||||
grep -q "does not exist" <<<"$OUT" && pass "path-does-not-exist message" || fail "path-does-not-exist message"
|
||||
|
||||
echo "4. Empty style directory loads and lints nothing"
|
||||
d="$(tree c4)"; mkdir "$d/styles/Empty"
|
||||
printf 'StylesPath = styles\n[*.md]\nBasedOnStyles = Empty\n' >"$d/.vale.ini"
|
||||
run "$d" doc.md
|
||||
expect "0 findings, exit 0" 0 "0 errors, 0 warnings and 0 suggestions"
|
||||
|
||||
echo "4b. Built-in Vale and committed YAML lint with no Packages entry"
|
||||
d="$(tree c4b)"; rule "$d" Mine; printf 'a foo b\n' >>"$d/doc.md"
|
||||
printf 'StylesPath = styles\n[*.md]\nBasedOnStyles = Vale, Mine\n' >"$d/.vale.ini"
|
||||
run "$d" doc.md
|
||||
expect "built-in Vale.Repetition fires, exit 1" 1 "Vale.Repetition"
|
||||
grep -qF "Mine.Foo" <<<"$OUT" && pass "committed style fires" || fail "committed style fires"
|
||||
|
||||
echo "4c. Style in Packages-only (not BasedOnStyles) lints nothing"
|
||||
d="$(tree c4c)"; rule "$d" Mine; printf 'a foo b\n' >>"$d/doc.md"
|
||||
printf 'StylesPath = styles\nPackages = Mine\n[*.md]\nBasedOnStyles =\n' >"$d/.vale.ini"
|
||||
run "$d" doc.md
|
||||
expect "0 findings, exit 0" 0 "0 errors, 0 warnings and 0 suggestions"
|
||||
|
||||
echo "5. Core option below a [glob] header"
|
||||
d="$(tree c5)"
|
||||
printf '[*.md]\nBasedOnStyles = Vale\nStylesPath = styles\n' >"$d/.vale.ini"
|
||||
run "$d" doc.md
|
||||
expect "E201 core option, exit 2" 2 "is a core option"
|
||||
d="$(tree c5b)"
|
||||
printf 'StylesPath = styles\n[*.md]\nBasedOnStyles = Vale\nMinAlertLevel = error\n' >"$d/.vale.ini"
|
||||
run "$d" doc.md
|
||||
expect "MinAlertLevel below glob also E201" 2 "is a core option"
|
||||
d="$(tree c5c)"
|
||||
printf 'StylesPath = styles\n[*.md]\nBasedOnStyles = Vale\nPackages = Foo\n' >"$d/.vale.ini"
|
||||
run "$d" doc.md
|
||||
expect "Packages below glob: no error (exit 1 from the repetition finding)" 1 "Vale.Repetition"
|
||||
run "$d" ls-config
|
||||
grep -q '"Packages": false' <<<"$OUT" && pass "Packages parsed as per-glob rule toggle" || fail "Packages parsed as per-glob rule toggle"
|
||||
run "$d" sync
|
||||
expect "sync reports Synced 0 package(s)" 0 "Synced 0 package(s)"
|
||||
|
||||
echo "6. text.frontmatter.<key> scope across YAML forms"
|
||||
d="$WORK/c6"; mkdir -p "$d/styles/FM"
|
||||
cat >"$d/styles/FM/Foo.yml" <<'Y'
|
||||
extends: existence
|
||||
message: "found '%s'"
|
||||
level: error
|
||||
scope: text.frontmatter.description
|
||||
tokens:
|
||||
- foo
|
||||
Y
|
||||
printf 'StylesPath = styles\n[*.md]\nBasedOnStyles = FM\n' >"$d/.vale.ini"
|
||||
fm() { # fm <name> <frontmatter lines...>
|
||||
local n="$1"; shift
|
||||
{ echo '---'; printf '%s\n' "$@"; echo '---'; echo; echo 'Body.'; } >"$d/$n.md"
|
||||
}
|
||||
fm single 'description: has foo here'
|
||||
fm literal 'description: |' ' line one' ' has foo here'
|
||||
fm folded 'description: >' ' line one' ' has foo here'
|
||||
fm plain 'description: line one' ' has foo here'
|
||||
fm squote "description: 'line one" " has foo here'"
|
||||
fm dquote 'description: "line one' ' has foo here"'
|
||||
run "$d" single.md; expect "single line lints" 1 "Foo"
|
||||
run "$d" literal.md; expect "| literal lints" 1 "Foo"
|
||||
run "$d" folded.md; expect "> folded silent" 0 "0 errors"
|
||||
run "$d" plain.md; expect "plain continuation silent" 0 "0 errors"
|
||||
run "$d" squote.md; expect "single-quoted multi-line silent" 0 "0 errors"
|
||||
run "$d" dquote.md; expect "double-quoted multi-line silent" 0 "0 errors"
|
||||
|
||||
echo "7. .mdx without mapping and without mdx2vast"
|
||||
d="$WORK/c7"; mkdir -p "$d/styles"
|
||||
printf 'This is the the sample.\n' >"$d/doc.md"; cp "$d/doc.md" "$d/doc.mdx"
|
||||
printf 'StylesPath = styles\n[*.{md,mdx}]\nBasedOnStyles = Vale\n' >"$d/.vale.ini"
|
||||
if command -v mdx2vast &>/dev/null; then
|
||||
echo " SKIP: mdx2vast is installed; the absent-binary case cannot run"
|
||||
else
|
||||
run "$d" .
|
||||
expect "whole invocation dies with E100 lintMDX, exit 2" 2 "E100 [lintMDX]"
|
||||
grep -qF "mdx2vast not found" <<<"$OUT" && pass "mdx2vast not found" || fail "mdx2vast not found"
|
||||
grep -q "doc.md" <<<"$OUT" && fail ".md alongside produced no output" || pass ".md alongside produced no output"
|
||||
fi
|
||||
|
||||
echo "8. mdx mapped onto md: suppression form"
|
||||
d="$WORK/c8"; mkdir -p "$d/styles"
|
||||
printf 'StylesPath = styles\n[formats]\nmdx = md\n[*.{md,mdx}]\nBasedOnStyles = Vale\n' >"$d/.vale.ini"
|
||||
printf 'This is the the sample.\n' >"$d/ctl.mdx"
|
||||
printf '<!-- vale off -->\nThis is the the sample.\n<!-- vale on -->\n' >"$d/html.mdx"
|
||||
printf '{/* vale off */}\nThis is the the sample.\n{/* vale on */}\n' >"$d/jsx.mdx"
|
||||
run "$d" ctl.mdx; expect "control alerts" 1 "Vale.Repetition"
|
||||
run "$d" html.mdx; expect "HTML comment suppresses" 0 "0 errors"
|
||||
run "$d" jsx.mdx; expect "JSX comment does not suppress" 1 "Vale.Repetition"
|
||||
|
||||
echo "9. spelling ignore path resolution"
|
||||
mk_spell() { # mk_spell <name> ; leaves rule with ignore1.txt, no ignore file placed
|
||||
local d; d="$WORK/$1"; mkdir -p "$d/styles/MyStyle" "$d/proj"
|
||||
cat >"$d/styles/MyStyle/Spell.yml" <<'Y'
|
||||
extends: spelling
|
||||
message: "Did you really mean '%s'?"
|
||||
level: error
|
||||
ignore:
|
||||
- ignore1.txt
|
||||
Y
|
||||
printf 'The zzqwidget is here.\n' >"$d/proj/doc.md"
|
||||
printf 'StylesPath = ../styles\n[*.md]\nBasedOnStyles = MyStyle\n' >"$d/proj/.vale.ini"
|
||||
echo "$d"
|
||||
}
|
||||
d="$(mk_spell s1)"; printf 'zzqwidget\n' >"$d/styles/ignore1.txt"
|
||||
run "$d/proj" doc.md; expect "ignore file at StylesPath root works" 0 "0 errors"
|
||||
d="$(mk_spell s2)"; printf 'zzqwidget\n' >"$d/proj/ignore1.txt"
|
||||
run "$d/proj" doc.md; expect "ignore file in working directory works" 0 "0 errors"
|
||||
run "$d" --config=proj/.vale.ini proj/doc.md; expect "working-dir copy fails from another directory" 1 "zzqwidget"
|
||||
d="$(mk_spell s3)"; printf 'zzqwidget\n' >"$d/styles/MyStyle/ignore1.txt"
|
||||
run "$d/proj" doc.md; expect "ignore file beside the rule is not read" 1 "zzqwidget"
|
||||
d="$(mk_spell s4)"
|
||||
run "$d/proj" doc.md; expect "absent ignore file fails silently" 1 "zzqwidget"
|
||||
grep -qi "ignore1" <<<"$OUT" && fail "no diagnostic emitted for missing ignore file" || pass "no diagnostic emitted for missing ignore file"
|
||||
|
||||
echo "10. ls-config reports styles and paths, never rules"
|
||||
d="$(tree c10)"; rule "$d" MyStyle
|
||||
printf 'StylesPath = styles\n[*.md]\nBasedOnStyles = MyStyle\n' >"$d/.vale.ini"
|
||||
printf 'a foo b\n' >"$d/doc.md"
|
||||
run "$d" doc.md; expect "rule fires" 1 "MyStyle.Foo"
|
||||
run "$d" ls-config
|
||||
grep -qF '"MyStyle"' <<<"$OUT" && pass "ls-config names the style" || fail "ls-config names the style"
|
||||
grep -qF 'Foo' <<<"$OUT" && fail "ls-config must not name the rule" || pass "ls-config does not name the rule"
|
||||
for sub in ls-dirs ls-vars ls-metrics; do
|
||||
run "$d" "$sub"
|
||||
grep -qF 'Foo' <<<"$OUT" && fail "$sub must not name the rule" || pass "$sub does not name the rule"
|
||||
done
|
||||
run "$d" ls-config
|
||||
grep -qF '"Checks": null' <<<"$OUT" && pass "Checks: null" || fail "Checks: null"
|
||||
|
||||
echo
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
[[ "$FAIL" -eq 0 ]]
|
||||
Reference in New Issue
Block a user