diff --git a/docs/adr/0018-treat-the-plugin-as-the-encapsulation-boundary-for-skill-citation.md b/docs/adr/0018-treat-the-plugin-as-the-encapsulation-boundary-for-skill-citation.md index a90947fce..de9db2564 100644 --- a/docs/adr/0018-treat-the-plugin-as-the-encapsulation-boundary-for-skill-citation.md +++ b/docs/adr/0018-treat-the-plugin-as-the-encapsulation-boundary-for-skill-citation.md @@ -226,3 +226,97 @@ in full. adds a `.claude/skills/` tree, since both premises of the distribution-unit argument would fail. It does not re-open on a request to relax cross-plugin citation: that is the case the contract is about and the evidence here does not touch it. + +## Amendment (2026-08-28) — the test clause 2 was applied under, and two surfaces it never named + +A remediation sweep applied clause 2 across `docs/**`, fixed 16 citations, kept 23, and stated its +dividing test only in the pull request that carried it. **The test is nowhere in this record.** It +has already cost something measurable: one commit ruled a reach into a skill's private `scripts/lib/` +out of bounds in one convention doc and left the byte-identical shape standing in another, three +files apart, on the same day. An unwritten test cannot be applied consistently, and was not. Three +things are recorded here so the next pass reads the rule instead of re-deriving it. + +### The dividing test: fix an address, keep evidence + +Clause 2 cannot mean every occurrence of a cross-plugin path, because **this document breaks that +reading on its own first page.** It path-cites another plugin's skill privates five times, three of +them the same `context/` file, which is private under any reading of the contract: +`plugins/docs-hygiene/skills/audit-encapsulation/context/public-surface-contract.md` at lines 19, 44 +and 108, that skill's `scripts/detect.sh` at 209, and +`plugins/source-control/skills/worktree/SKILL.md` at 222. Either this record violates itself in the +act of stating the rule, or clause 2 has always been scoped to what a citation *does* rather than to +the characters it contains. Only the second reading is coherent, and it is the reading every applied +fix in fact used. + +**FIX a citation that is the doc's address for an obligation.** The reader is being sent there to +get the rule the sentence declares itself bound by, and the path is missing for exactly the audience +the sentence binds: a contract fetched over `raw.githubusercontent.com` by an installed plugin, a +lane running with one plugin installed and not the other. The binding fails precisely where it +binds. Name the public `/plugin:skill` invocation instead, keeping any section name so a reader +inside this checkout still lands in the right place. + +**KEEP a citation that is evidence about this tree at a moment in time.** Three shapes qualify: + +- a **dated record** whose claim is what a named file contained on that date, a changelog entry + quoting the citation it just removed included; +- a **worked example whose content is quoted inline**, so the reader gets the point without + following the path; +- a **statement whose subject is the path**, where naming the invocation instead would delete the + assertion rather than relocate it. + +Evidence does not fail when the cited file moves; it becomes a record of something that was true, +which is what a dated record is for. **This document's own five citations are the second and third +kinds, and they stay.** So does the ADR-bodies refusal that the sweep record already carries on this +ground. + +The test governs function, not form, so it takes nothing away from clause 3. A kept citation still +has to resolve from the base its own form implies, and a kept citation that has rotted is a clause 3 +defect whatever it is evidence of. A pin *into* a kept citation, a `:97` or a `steps 6 and 7`, is the +part with no evidentiary value once the content is quoted inline, and it is the part that rots first: +drop it and keep the path. + +### A skill's `scripts/` is its entry surface; `scripts/lib/` is not + +The contract's carve-out lets harness surfaces, CI workflows, git hooks and automation registries +path-cite a skill's `scripts/` **entry scripts**. It reaches the scripts a caller is meant to invoke, +and stops there. **A private subdirectory beneath `scripts/`, `scripts/lib/` above all, is outside +the carve-out, and is private on the contract's own definition of a skill's non-public files.** It +holds implementation a +caller never names, so citing it buys a reader nothing an invocation would not, and costs the same +rename exposure the contract exists to prevent. + +This was already ruled, in +[`detector-findings` 2.8.1](../conventions/detector-findings/CHANGELOG.md), against a row citing +`plugins/docs-hygiene/skills/audit-noise/scripts/lib/noise-shapes.sh`, and its remedy is the one to +copy: name the thing in the terms the sentence already uses ("its shape library", "the scanner", +"`/repo-hygiene:clean`'s bundled test-helper copy") so the assertion is unchanged and survives the +next rename. It is recorded here because a ruling that lives only in one convention's changelog is +invisible to the next doc that needs it: the commit that wrote 2.8.1 left the byte-identical shape +standing in `docs/conventions/shell-test-helpers/README.md`, and a separate pass had to close it. + +### Plugin-level non-skill trees are outside this ADR's privacy question, and inside clause 3 + +`plugins/

/reference/`, `context/`, `hooks/`, `scripts/` and `agents/` sit **outside every skill +directory**. The public-surface contract defines privacy relative to a skill directory, so it does +not reach them, and neither does clause 2's encapsulation half. The repo has ruled on exactly one +such site, and only in a changelog: +[`detector-findings` 2.8.1](../conventions/detector-findings/CHANGELOG.md) kept +`plugins/review/reference/topic-docs.md` because "that file sits outside every skill directory" and +because the section around it argues explicitly for naming that file by repo path. + +**That ruling generalizes on privacy and does not generalize into permission.** Nothing in these +trees is private under the contract, so no citation of them is a clause 2 violation. Clause 3 still +binds every one of them, and so does the fetched-contract problem that motivated clause 2: a +marketplace-checkout-relative `plugins/

/reference/…` is not on disk for a reader who installed the +plugin, so a citation of that shape that functions as an **address** is still to be fixed under the +test above, on its own merits rather than as an encapsulation defect. The 2.8.1 keep survives that +test on its second leg, not its first. + +**The population is measured, and it is not swept here.** A verifier counted 51 distinct `docs/**` +citations into those trees. Re-derived independently for this amendment with a different expression: +59 occurrences at 48 distinct `path:line` sites in 16 files outside `docs/specs/`, `docs/topics/` and +`docs/adr/`, and 148 occurrences at 132 sites once those three are included. The counts disagree +because the shape has no single search expression, which is the point: **a later pass must re-derive +against the live tree and must not cite any of these numbers as a total.** They are recorded as a +measured order of magnitude, so that pass knows it is buying dozens of judgments rather than a +handful. diff --git a/docs/conventions/native-references/CHANGELOG.md b/docs/conventions/native-references/CHANGELOG.md index 25774cac4..f9f278f35 100644 --- a/docs/conventions/native-references/CHANGELOG.md +++ b/docs/conventions/native-references/CHANGELOG.md @@ -27,6 +27,9 @@ internals become public invocations. - **Two of the three were standing findings.** They are `V-review-13` and `V-review-14` in [`docs-hygiene-sweep-unapplied-remediations.md`](../../specs/docs-hygiene-sweep-unapplied-remediations.md)'s L4 group of 34, recorded open on 2026-08-26 and unapplied since. That roster is a point-in-time - record and is not edited here, per its own decay rule; it is down to 32. Found by the whole-repo + record and is not edited here, per its own decay rule, and re-deriving it against its own text + test shows most of it is already closed: these two were the last open rows of its 24-row Group 1, + twelve of the rest having been closed by #3380 itself, and its eight Group 2 rows remain. Found by + the whole-repo extract-ssot sweep's encapsulation floor, which re-derived the shape rather than trusting the roster and reached the third site the roster did not carry. diff --git a/docs/conventions/plugin-data-report-keying/CHANGELOG.md b/docs/conventions/plugin-data-report-keying/CHANGELOG.md index f5d667b03..30846d43d 100644 --- a/docs/conventions/plugin-data-report-keying/CHANGELOG.md +++ b/docs/conventions/plugin-data-report-keying/CHANGELOG.md @@ -5,6 +5,27 @@ is versioned by the `Version:` stamp in `README.md` (SemVer). A rule whose `[SPE tightens is a major bump; a new rule or a new named example is a minor bump; wording and adoption-table updates are a patch. +## 1.0.1 — 2026-08-28 + +Patch under this contract's own rule — wording only. No `[SPEC]` obligation tightens, no rule is +added, and no worked example is added or removed. + +- **Rule 1c's two worked examples and rule 2's reference implementation stop pinning a location + inside the cited file.** They read `plugins/bugs/skills/write/SKILL.md:97`, + `plugins/claude-config/skills/unhobble/SKILL.md:53-62`, and "steps 6 and 7" of + `plugins/machine-health/skills/audit/SKILL.md`. All three resolved when re-derived, so none was + broken yet; the pin is the part that rots, and this contract has already lost one to rot — + [`detector-findings` 2.7.1](../detector-findings/CHANGELOG.md) dropped a `:414` pin as a class + after finding it had drifted onto a comment five lines past the check it named. Each of these + three citations already quotes the content it is pointing at, so the pin was carrying nothing the + sentence did not. The paths and the plugin name stay, the quotes stay, and the machine-health + citation now names what its procedure does — renders the report, then updates state — instead of + two step numbers that renumber on the next inserted step. +- **The citations themselves stay, and [ADR 0018](../../adr/0018-treat-the-plugin-as-the-encapsulation-boundary-for-skill-citation.md)'s + amendment of the same date says why.** Each is evidence about this checkout — a worked example + whose content is quoted inline — rather than the address a reader must visit to get a rule. The + amendment states that test, which until now was applied without being written down. + ## 1.0.0 — 2026-08-12 Initial published contract. Written because the hazard was already understood inside the fleet and diff --git a/docs/conventions/plugin-data-report-keying/README.md b/docs/conventions/plugin-data-report-keying/README.md index b7b85b16e..ad3d2d701 100644 --- a/docs/conventions/plugin-data-report-keying/README.md +++ b/docs/conventions/plugin-data-report-keying/README.md @@ -1,7 +1,7 @@ # Plugin-data report keying, retention, and overwrite -Version: 1.0.0 -Last updated: 2026-08-12 +Version: 1.0.1 +Last updated: 2026-08-28 A marketplace-wide contract for **how a plugin names what it writes under `${CLAUDE_PLUGIN_DATA}`** — the key, the retention shape, and whether a write may overwrite. It does not govern *what* may live @@ -95,7 +95,7 @@ skill's namespace during review. ### 1c — the "looks scoped but isn't" case, named so it is not repeated -`plugins/bugs/skills/write/SKILL.md:97` keys on the **kebab-cased basename of the project +`plugins/bugs/skills/write/SKILL.md` keys on the **kebab-cased basename of the project root**: > `${CLAUDE_PLUGIN_DATA}/bug-reports//` … The plugin data directory is per-plugin, not @@ -105,7 +105,7 @@ root**: The line states the hazard correctly and then picks a colliding key: two same-named checkouts — a fork, a same-named worktree, `~/work/api` and `~/oss/api` — share one slug directory, and the duplicate scan cross-matches between them. It escapes *overwrite* only because its filenames are -timestamped. `plugins/claude-config/skills/unhobble/SKILL.md:53-62` names the same insufficiency in +timestamped. `plugins/claude-config/skills/unhobble/SKILL.md` names the same insufficiency in prose: "`${CLAUDE_PLUGIN_DATA}` is machine-global, so two checkouts sharing a basename…". **This is recorded here as the worked example, not filed as a `bugs` defect.** A basename is @@ -120,8 +120,9 @@ writer; it obliges the next one not to repeat it. | Read back and served to the operator | `/` **and** the read must derive the same key | Serving the newest ≠ serving this project's | | A trend or a history | one file per run **plus** an appended line | A same-day rerun must not erase the earlier point | -The reference implementation of the third row is `plugins/machine-health/skills/audit/SKILL.md`, steps -6 and 7: `/reports/health-.md` — "one file per run, so a same-day rerun +The reference implementation of the third row is `plugins/machine-health/skills/audit/SKILL.md`, where +its procedure renders the report and then updates state: +`/reports/health-.md` — "one file per run, so a same-day rerun does not overwrite the earlier report" — plus `/state/latest.json` and one appended line in `/state/history.jsonl`, "the trend source of truth". Note it is *not* an adopter of rule 1: its roots are passed in explicitly by the caller rather than keyed, for a reason that file states — a diff --git a/docs/conventions/shell-test-helpers/README.md b/docs/conventions/shell-test-helpers/README.md index fd8163a60..1e47ae148 100644 --- a/docs/conventions/shell-test-helpers/README.md +++ b/docs/conventions/shell-test-helpers/README.md @@ -24,7 +24,8 @@ below are not that: they are already three genuinely different shapes, not one l [`claude-ops/hooks/claude-ops-test-helpers.sh`](../../../plugins/claude-ops/hooks/claude-ops-test-helpers.sh). - **Skill-script shape** (`pass`/`fail`, `FAILED`/`CASE_NUM` counters, file-existence assertions): [`source-control/scripts/test-helpers.sh`](../../../plugins/source-control/scripts/test-helpers.sh), - [`repo-hygiene/skills/clean/scripts/lib/test-helpers.sh`](../../../plugins/repo-hygiene/skills/clean/scripts/lib/test-helpers.sh). + and `/repo-hygiene:clean`'s bundled test-helper copy — named rather than linked because it sits + under that skill's private `scripts/lib/`, not on the entry surface the pointers below reach. - **Vendored-seam shape** (same `pass`/`fail` primitives, but owned by the seam itself so it stays correct wherever the seam is resolved from — bundled or consumer-vendored — independent of this repo's tooling): [`work-items/tools/work-item-tracker/tests/lib.sh`](../../../plugins/work-items/tools/work-item-tracker/tests/lib.sh). diff --git a/docs/native-surfaces/records.json b/docs/native-surfaces/records.json index f327c397e..e77166f63 100644 --- a/docs/native-surfaces/records.json +++ b/docs/native-surfaces/records.json @@ -1,6 +1,6 @@ { "schema": 1, - "note": "SSOT for native-surface overlap verdicts. Hand-editable and human-gated: every verdict here was written by a person, never by a run. docs/NATIVE-SURFACES.md is generated from this file and is never the source. Seeded candidates live in plugins/claude-ops/skills/audit-native-overlap/reference/canonical-pairs.json; a candidate becomes a row only when someone rules on it. Phrasing rules for anything baked out of these rows: docs/conventions/native-references/README.md.", + "note": "SSOT for native-surface overlap verdicts. Hand-editable and human-gated: every verdict here was written by a person, never by a run. docs/NATIVE-SURFACES.md is generated from this file and is never the source. Seeded candidates come from the canonical-pair seed bundled with /claude-ops:audit-native-overlap; a candidate becomes a row only when someone rules on it. Phrasing rules for anything baked out of these rows: docs/conventions/native-references/README.md.", "rows": [ { "native": { "name": "code-review", "class": "bundled-skill", "markers": [] }, diff --git a/docs/specs/extract-ssot-sweep-2026-08-28.md b/docs/specs/extract-ssot-sweep-2026-08-28.md index 2a7b9133d..3625838ce 100644 --- a/docs/specs/extract-ssot-sweep-2026-08-28.md +++ b/docs/specs/extract-ssot-sweep-2026-08-28.md @@ -32,6 +32,7 @@ having moved to `:16` when #3380 inserted a `## Contents` index above it. - [Verified, not applied](#verified-not-applied) - [Contradictions found](#contradictions-found) - [New refusals, recorded so nobody re-opens them](#new-refusals-recorded-so-nobody-re-opens-them) +- [Open remainder after the encapsulation close](#open-remainder-after-the-encapsulation-close) - [Recall limits this run declares](#recall-limits-this-run-declares) ## What this run did differently @@ -171,11 +172,16 @@ filtered probes print nothing. block to carry no `$` expansion other than a bare `$HOME`: an expansion leaves the composed block unverifiable to the worktree-isolation guard, and the skill then fails to load from an isolated agent (`session-flow` 0.17.16). `audit-derivability` demonstrates the binding, not a form to copy. -All five sites took an equivalent that adds no `$` at all and heads the same `&&` list with a -status-only run of the site's own probe: +All five sites first took an equivalent that adds no `$` at all and heads the same `&&` list with a +status-only run of the site's own probe. **That form was itself wrong, and is no longer what +ships.** It is correct only without `set -o pipefail`; under pipefail the `&&` list inherits the +pipeline's status, which both `grep` matching nothing and `git` taking SIGPIPE at the cap make +non-zero, firing the failure token on a healthy probe. `docs-hygiene` 0.21.24 and `code-tidying` +0.14.13 replaced it with the pipeline inside a brace group closed by `:`, a command that cannot +fail, which is correct under both settings: ```text -git status --porcelain >/dev/null 2>&1 && git status --porcelain 2>/dev/null | … | head -N || echo "(git status unavailable)" +git status --porcelain >/dev/null 2>&1 && { git status --porcelain 2>/dev/null | … | head -N; :; } || echo "(git status unavailable)" ``` The probe runs twice, which is the price of not capturing it. No line holds a `$` expansion after @@ -183,11 +189,14 @@ the change: the `$` characters that remain are end-of-line anchors in single-quo and field references in single-quoted `awk` programs, all pre-existing and none of them expanded by the shell. Each site kept its own filter, its own cap and its own label noun; each label gained `empty = none`; the failure token is the `(git status unavailable)` string the normalized status -probes already use. Verified by execution in three states per site: outside a repository each prints -the token, inside a repository whose dirty files do not match the filter each prints nothing, and -above the cap the cap holds with no spurious fallback. Landed in `docs-hygiene` 0.21.23 and -`code-tidying` 0.14.12, which also carry the two `detect.test.sh` extractors that read these lines -out of `SKILL.md` and were anchored on the old labels. +probes already use. Verified by execution in three states per site, **without pipefail**: outside a +repository each prints the token, inside a repository whose dirty files do not match the filter each +prints nothing, and above the cap the cap holds with no spurious fallback. That qualifier is the +whole lesson: the same three states under pipefail print the token in two of them, which is what +0.21.24 and 0.14.13 had to correct. A verification that fixes the shell's options and does not say +so proves less than it appears to. Landed in `docs-hygiene` 0.21.23 and `code-tidying` 0.14.12, +which also carry the two `detect.test.sh` extractors that read these lines out of `SKILL.md` and +were anchored on the old labels. One filtered probe with the unreachable shape remains open, and is out of this section's scope because its probe is not `git status`: @@ -328,6 +337,88 @@ Beyond the predecessor's 13. Each was resolved against the real files and refuse - Plus roughly 120 further per-cluster refusals across the eight triage batches, each recorded with its form and ground in the pull request that carries this file. +## Open remainder after the encapsulation close + +Recorded, not fixed. A later pass adjudicates these; this section exists so it does not have to +re-find them. Stamped 2026-08-28 and subject to the decay rule at the top of this file. + +### Twelve citations the encapsulation close created and never rostered + +The pass that closed the encapsulation floor wrote a convention CHANGELOG entry for each fix, and +every entry quotes the path it removed. Those quotes are themselves `docs/**` citations of the exact +shape the pass was sweeping, created by the sweep, counted by nobody. **Verified against +`origin/main`, twelve of them:** + +| File | Lines | +|---|---| +| `docs/conventions/config-cascade/CHANGELOG.md` | 12, 13, 14 | +| `docs/conventions/detector-findings/CHANGELOG.md` | 44, 57 | +| `docs/conventions/loop-lane/CHANGELOG.md` | 69 | +| `docs/conventions/native-references/CHANGELOG.md` | 16, 17, 18 | +| `docs/conventions/permission-rule-hygiene/CHANGELOG.md` | 14 | +| `docs/conventions/topic-docs/CHANGELOG.md` | 11, 12 | + +One correction to the roster this was handed as: the `loop-lane` row was reported at `:14`, which is +where it sat when the pass wrote it. Release `9.1.0` landed above it, and the citation is at `:69` +today, inside the `9.0.2` entry. That is the decay rule firing on a record less than a day old, and +it is why a later pass must **re-derive by the text and never by these numbers.** + +**The twelve are disjoint from the three this file already counts, confirmed by differencing the +tree at the fix commit's parent.** Before that commit, exactly three lines in +`docs/conventions/*/CHANGELOG.md` cited a plugin skill by path: two in `detector-findings` and one in +`loop-lane`. Those are the "three dated changelog entries that quote a citation as it stood" in the +kept set. Every one of the twelve above was written by the fix commit itself, so **the arithmetic is +16 fixed and 35 kept, not 16 and 23**, and the changelog-evidence class is fifteen rather than three. + +**All twelve are keep-correct** under the test now written into +[ADR 0018](../adr/0018-treat-the-plugin-as-the-encapsulation-boundary-for-skill-citation.md)'s +amendment: each is a dated changelog entry whose whole claim is what a named file contained on that +date. Nothing here needs an edit. What is missing is the judgment, and a pass that re-derives this +shape finds twelve rows nobody ruled on and has to adjudicate them from scratch to learn they were +fine. The general lesson is worth more than the twelve rows: **a sweep that documents each fix by +quoting the citation it removed manufactures new instances of the shape it is sweeping,** so its own +output has to be swept before the count is closed. + +**The pass writing this section did the same thing, deliberately, three more times.** +`docs/conventions/plugin-data-report-keying/CHANGELOG.md` 1.0.1 quotes the three pins it dropped, so +the class stands at eighteen. They are keep-correct on the same ground as the twelve, and they are +declared here rather than left for the next re-derivation to find. That is the only discipline +available: the alternative is a changelog entry that does not say what it changed. + +### Three trees the sweep excluded by fiat + +The sweep's encapsulation floor scoped itself to `docs/**` and then dropped five subtrees. Two of the +exclusions are defensible and stated as such: `docs/SKILL-CHEAT-SHEET.md` (162 citations, generated +and CI drift-checked, so an edit is reverted by its generator) and `docs/upstream/` (vendored, not +this repo's prose to style). **The other three rest on nothing.** `docs/specs/` (roughly 265 sites), +`docs/topics/` (roughly 43) and `docs/adr/` (25) were excluded on the assertion that "the dated +records under `docs/specs/`, `docs/adr/` and `docs/topics/` are out of scope by the same test", and +no carve-out in ADR 0018 authorizes any of it. + +The assertion is *probably* right and is *not* established. Under the amendment's test most of that +population is evidence: dated records, and ADR bodies that quote what they cite. But "most of a +265-site population is evidence" is a hypothesis about 265 sites, argued from the tree a citation +lives in rather than from what the citation does, which is the form-over-function reasoning the +amendment rejects. A tree-shaped exclusion is a carve-out; ADR 0018 grants none. Either the pass that +resumes this samples the three trees and records the ruling, or the ADR gains the carve-out +explicitly. It should not stay a habit. + +Counts re-derived for this record and rounded deliberately: the exact figure moves with the search +expression, and per this file's own recall-limits discipline none of these numbers is a total. + +### A stale record found in passing, not an ADR matter + +`docs/MIGRATION-PLAYBOOK.md:943` carries the `dometrain` security-review record: "Reviewed at +`0.1.0`; a version bump adding a new trust surface re-triggers this review." **The plugin's manifest +reads `0.2.7` today** (`plugins/dometrain/.claude-plugin/plugin.json`, confirmed against +`origin/main`). The trigger has had every opportunity to fire across those bumps and the review was +never re-run, so the record asserts a currency it does not have. + +This is not a citation defect and does not belong to the encapsulation lane. It is logged here +because the encapsulation pass kept that record's five file-and-frontmatter citations *on the +strength of* its re-trigger clause, which means the clause was read and its own condition was not +checked. Whoever re-runs the review owns the record; the citations are fine either way. + ## Recall limits this run declares A finding count read as a defect count is worse than no count. An adversarial verifier audited the @@ -391,8 +482,11 @@ figures to differ. this very change set added, written in the bare plugin-relative form ADR 0018 names as its real defect class), two conformance rows in `docs/conventions/config-cascade/README.md`, one in `docs/conventions/pre-pr-ordering/README.md`, three in - `docs/conventions/native-references/README.md` (two of them `V-review-13` and `V-review-14` off - the predecessor's 34-item L4 roster, which is therefore down to 32), and one adopter-row detail + `docs/conventions/native-references/README.md` (two of them `V-review-13` and `V-review-14`, the + last two rows of the predecessor's 34-item L4 roster still open by that roster's own text test: + re-derivation found 22 of its other 32 rows already closed, twelve of them by #3380 itself, so + what remains of the 34 is its eight Group 2 intra-plugin path-form defects, untouched here), and + one adopter-row detail in `docs/conventions/detector-findings/README.md`. **What remains is a judgment set, not a backlog**: 23 further in-shape citations were kept with reasons, because each is evidence about this checkout rather than an address for an obligation. They are the three worked examples in