@erclx/aitk 0.78.1 → 0.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aitk",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "0.78.1",
4
+ "version": "0.80.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -9,6 +9,8 @@ description: What the planning-doc reconcile is for, the gaps it closes, and why
9
9
 
10
10
  Without this skill, the planning docs describe the scope the session opened with. An outcome the diff shipped stays unchecked, so the board reports finished work as open and the next session re-plans it. A requirement or architecture decision that changed mid-cycle lives only in the conversation and dies with it. Plans for closed tasks accumulate in the live folder, so the folder stops indicating what is in flight.
11
11
 
12
+ A decision's verification anchor has the same shape of gap in the other direction. The reasoning keeps reading as current while the number it cites moves underneath, and an amendment written without an anchor leaves nothing recording which numbers were checked and which nobody has read since.
13
+
12
14
  ## Must
13
15
 
14
16
  - Take completion from the diff and everything else from the session, since completion is a fact about the repository rather than about the conversation
@@ -17,6 +19,8 @@ Without this skill, the planning docs describe the scope the session opened with
17
19
  - Write tracked docs at the current worktree root and the task board at the main root, since only the first commits with the branch
18
20
  - Count every other citation before archiving a plan, comparing resolved targets rather than raw strings or bare filenames
19
21
  - Retarget a closed task at the archived plan, so the reasoning behind finished work stays reachable
22
+ - Anchor a decision entry this run writes or amends whose reasoning cites a measured number, re-reading the number against the tree before writing the marker
23
+ - Report an anchored decision whose cited path the diff touched, since the number was read before the branch moved what it counted
20
24
 
21
25
  ## Must not
22
26
 
@@ -25,6 +29,8 @@ Without this skill, the planning docs describe the scope the session opened with
25
29
  - Widen what a writing step reads when the baseline is unusable. Widening a read is safe and widening a write stubs a surface for every file in the repository.
26
30
  - Edit `CLAUDE.md` inline. Every change there goes through a diff-and-approve gate, so this skill only flags.
27
31
  - Create a context entry or delete a plan
32
+ - Write an anchor onto a decision the run did not amend, or refresh one without re-reading the number. A date from a pass that measured nothing is the false confidence the marker exists to prevent.
33
+ - Anchor an entry written before the rule, which dates it by blame rather than by a read
28
34
 
29
35
  ## Guards
30
36
 
@@ -37,3 +43,4 @@ Without this skill, the planning docs describe the scope the session opened with
37
43
  - Public-facing docs, which `docs-sync` owns. This skill reconciles the `.claude/` planning surface.
38
44
  - Regenerating the task index, owned by a hook
39
45
  - Redrawing diagrams, which `claude-diagram` owns. This skill flags staleness and leaves the re-run to the author.
46
+ - Re-measuring an architecture claim to decide whether its number moved. The sweep keys on a cited path entering the diff, so a claim whose number moved with no cited path in the diff goes unflagged.
@@ -9,11 +9,11 @@ description: Updates `.claude/` planning docs to reflect decisions made during t
9
9
 
10
10
  - If no `.claude/` directory exists, stop: `❌ No .claude/ directory found. Run aitk claude init to set up the workflow.`
11
11
 
12
- The skip for a session that changed nothing lives at the end of Step 2, because it needs the diff to decide. It drops the doc rewrite alone. The diff-driven sweeps in Steps 4 and 5 still run.
12
+ The skip for a session that changed nothing lives at the end of Step 2, because it needs the diff to decide. It drops the doc rewrite alone. The diff-driven sweeps in Steps 4, 5, and 6 still run.
13
13
 
14
14
  ## Diff baseline
15
15
 
16
- Steps 2, 4, 5, and 7 share one diff on the usable path. An unusable baseline splits them, per the rule below. Resolve the base ref once and reuse it:
16
+ Steps 2, 4, 5, 6, and 8 share one diff on the usable path. An unusable baseline splits them, per the rule below. Resolve the base ref once and reuse it:
17
17
 
18
18
  ```bash
19
19
  git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main 2>/dev/null
@@ -32,11 +32,13 @@ An unusable baseline costs only the committed half. `git diff <base> HEAD` is em
32
32
 
33
33
  **Step 2 recovers the committed half.** Read `git log -p -1`, widening to `git log -p -<n>` when the session spans several commits, and read the candidate task files against the working tree. That yields names and content both, which is what lets Step 2 decide on behavior rather than on filenames. A fresh `git init` on `main` with no remote is the ordinary shape of a scaffolded project, so this path carries the evidence rather than covering an edge case.
34
34
 
35
- **Steps 4, 5, and 7 keep the scoped set.** Run them on the working tree and untracked files alone, and skip only when that set comes out empty, each reporting the warning its own step names.
35
+ **Steps 4, 5, 6, and 8 keep the scoped set.** Run them on the working tree and untracked files alone, and skip only when that set comes out empty, each reporting the warning its own step names.
36
36
 
37
- Never substitute the whole tree for a missing baseline, and do not reuse Step 2's commit read in these three for consistency. On a fresh `git init` project the last commit is the scaffold commit, so `git log -p -1` is the whole tree by another route. Step 2 tolerates that because it only reads, and it matches conservatively against outcomes already on the board. These three write, so the same set stubs a wireframe for every uncovered surface in the repository, stubs a diagram for every source signal the scaffold introduced, and rewrites every context entry that tree touches.
37
+ Never substitute the whole tree for a missing baseline, and do not reuse Step 2's commit read in these four for consistency. On a fresh `git init` project the last commit is the scaffold commit, so `git log -p -1` is the whole tree by another route. Step 2 tolerates that because it only reads, and it matches conservatively against outcomes already on the board. Steps 4, 5, and 8 write, so the same set stubs a wireframe for every uncovered surface in the repository, stubs a diagram for every source signal the scaffold introduced, and rewrites every context entry that tree touches.
38
38
 
39
- Widening what a step reads is safe. Widening what a step writes is not.
39
+ Step 6 only reports, and the whole tree costs it a different way. Every anchored decision cites a path the scaffold commit carries, so the sweep flags the entire record and the reader learns nothing about which number moved.
40
+
41
+ Widening what a step reads is safe. Widening what a step writes is not, and widening what a step flags spends the reader's attention on entries nothing put in doubt.
40
42
 
41
43
  ## Step 1: read current docs
42
44
 
@@ -49,7 +51,7 @@ Read these in parallel from the current worktree root (`pwd`), not the main work
49
51
 
50
52
  Read the task board from the main worktree root instead, per Worktrees in `CLAUDE.md`. It is gitignored scratch and never commits with the branch:
51
53
 
52
- - `.claude/tasks/index.md` first, then the task files this session touched. That narrow read serves the marking step. Step 8 reads every file in the folder for its plans sweep and states that where it gives the instruction.
54
+ - `.claude/tasks/index.md` first, then the task files this session touched. That narrow read serves the marking step. Step 9 reads every file in the folder for its plans sweep and states that where it gives the instruction.
53
55
 
54
56
  ## Step 2: identify what changed
55
57
 
@@ -72,19 +74,20 @@ Keep the match conservative:
72
74
 
73
75
  Skip Step 3 when the session shows no divergence **and** the diff matches no queued outcome, reporting `✅ No doc updates needed. Session matched the original plan.` Both conditions have to hold. Shipping a queued task exactly as planned is the ordinary case and it reads as no divergence, so a session-only skip would drop the marking step with it.
74
76
 
75
- Then run Steps 4 through 8. Step 3 is the only one this skips, because it is the only one driven by the session rather than by the diff or the board. A project with an empty task board making a mechanical change satisfies both conditions above, and stopping here would put an uncovered surface and an uncovered diagram kind out of reach in every such project.
77
+ Then run Steps 4 through 9. Step 3 is the only one this skips, because it is the only one driven by the session rather than by the diff or the board. A project with an empty task board making a mechanical change satisfies both conditions above, and stopping here would put an uncovered surface and an uncovered diagram kind out of reach in every such project.
76
78
 
77
- Three of the steps that follow write, so each earns the reach separately:
79
+ The steps that follow reach past the session, so each earns the reach separately:
78
80
 
79
81
  - Steps 4 and 5 stub against the diff. These are why the skip is not a stop. A session that changed no docs is exactly when an uncovered surface or diagram kind goes unnoticed.
80
- - Step 7 rewrites context entries against the diff and against the facts `claude-memory-capture` routed. The Diff baseline section above groups its diff half with Steps 4 and 5 as a scoped-set step, so a quiet session is no different from any other for it. The routed half reads a named file and runs whatever the diff shows.
81
- - Step 8 reads the board rather than the session. Its board-wide scan exists to clear a plan an earlier run stranded, and a run that stops at Step 2 can never reach one.
82
+ - Step 6 reads the architecture record against the diff. A run that amended no decision is the one where an anchored number moves under a reasoning nobody reread, which is the case the marker exists to surface.
83
+ - Step 8 rewrites context entries against the diff and against the facts `claude-memory-capture` routed. The Diff baseline section above groups its diff half with Steps 4 and 5 as a scoped-set step, so a quiet session is no different from any other for it. The routed half reads a named file and runs whatever the diff shows.
84
+ - Step 9 reads the board rather than the session. Its board-wide scan exists to clear a plan an earlier run stranded, and a run that stops at Step 2 can never reach one.
82
85
 
83
- This changes which steps the skill reaches. It does not widen what any of them reads. Steps 4, 5, and 7 still take the same scoped set the Diff baseline section defines, and that section's rule is about the input a write is handed rather than about which writes run.
86
+ This changes which steps the skill reaches. It does not widen what any of them reads. Steps 4, 5, 6, and 8 still take the same scoped set the Diff baseline section defines, and that section's rule is about the input a step is handed rather than about which steps run.
84
87
 
85
88
  ## Step 3: update
86
89
 
87
- For each doc with relevant changes, apply updates following these rules. Read a standard this skill names, here or in Step 7, from `${CLAUDE_SKILL_DIR}/../../standards/` when the project does not have it.
90
+ For each doc with relevant changes, apply updates following these rules. Read a standard this skill names, here or in a later step, from `${CLAUDE_SKILL_DIR}/../../standards/` when the project does not have it.
88
91
 
89
92
  **`.claude/tasks/`**
90
93
 
@@ -98,6 +101,8 @@ For each doc with relevant changes, apply updates following these rules. Read a
98
101
  - Update only the sections affected by session decisions.
99
102
  - Do not rewrite sections unrelated to what changed.
100
103
  - Follow `.claude/standards/prose.md` and `.claude/standards/markdown.md` for all edits.
104
+ - Close a decision entry in `.claude/ARCHITECTURE.md` with its verification anchor whenever this run writes that entry or amends its reasoning and that reasoning cites a measured number. Re-read the number against the tree first, since the marker records the read rather than the edit. `.claude/standards/architecture.md` fixes the sentence.
105
+ - Leave every decision entry this run did not write alone, anchored or not. The rule is scoped forward, so an entry written before it is dated by blame rather than by a read. Step 6 reports a stale anchor and no step writes one on an entry it did not amend.
101
106
 
102
107
  Write each updated file immediately. Claude Code's tool permission dialog is the confirmation gate. Do not wait for user input.
103
108
 
@@ -121,7 +126,19 @@ Both findings key on something literally entering or leaving the tree. Anything
121
126
 
122
127
  Past the skip above, read `${CLAUDE_SKILL_DIR}/references/diagram-sweep.md` for the two findings, the signal table deciding an uncovered kind, the stub it writes, and the output lines. Both tests need the cited paths and the signal list that file carries, so the folder check is the only one the body can settle on its own.
123
128
 
124
- ## Step 6: flag CLAUDE.md drift
129
+ ## Step 6: architecture anchor sweep
130
+
131
+ Skip this step silently when `.claude/ARCHITECTURE.md` does not exist at `pwd` or carries no decision entry with a verification anchor. A record written before the rule holds none, and a project is not told on every ship that nothing has been checked when the standard calls that state correct. When the baseline is unusable, scope the sweep to the working tree and untracked files, and skip it only when that set is empty, reporting `⚠ No diff to scope against. Skipped the anchor sweep.`
132
+
133
+ This step reports and never writes. The record carries no frontmatter, so an anchor is a sentence sharing a paragraph with the claim it marks, and a pass editing prose to mark prose has no structural guard against editing the claim beside it. The diagram sweep gets that separation from YAML and this one cannot.
134
+
135
+ Step 3 holds the writer, and the two never meet. Anchoring fires when this run amends a decision, and this sweep fires when the diff moves a path under one, so a single step covering both would gate the anchor obligation on a signal that has nothing to do with it.
136
+
137
+ Follow `.claude/standards/architecture.md` for the anchor sentence this step matches on.
138
+
139
+ Past the skip above, read `${CLAUDE_SKILL_DIR}/references/anchor-sweep.md` for how an entry's cited paths are collected, the finding the diff fires, and the report line.
140
+
141
+ ## Step 7: flag CLAUDE.md drift
125
142
 
126
143
  If this session established or changed a cross-cutting behavior rule that belongs in root `CLAUDE.md` (a new always-on convention, a revised workflow rule), surface a one-line warning:
127
144
 
@@ -129,7 +146,7 @@ If this session established or changed a cross-cutting behavior rule that belong
129
146
 
130
147
  Do not edit `CLAUDE.md` inline. Every `CLAUDE.md` change goes through the show-diff-and-approve gate, so this step only flags. Skip silently when the session made no cross-cutting behavior decision.
131
148
 
132
- ## Step 7: refresh context entries
149
+ ## Step 8: refresh context entries
133
150
 
134
151
  Read `.claude/context/index.md` at `pwd` to see which domain entries exist. Skip this step silently if the directory does not exist or has no entries.
135
152
 
@@ -160,7 +177,7 @@ Add a line naming the handoff when one was consumed:
160
177
 
161
178
  The base lint-staged config runs `aitk indexes regen` on every committed `*.md`, so `.claude/context/index.md` refreshes automatically on commit. No manual step needed.
162
179
 
163
- ## Step 8: sweep consumed scratch
180
+ ## Step 9: sweep consumed scratch
164
181
 
165
182
  Sweep reviews this session consumed, and sweep plans across the whole board. Resolve all paths at the main worktree root, not the current worktree. See Worktrees in `CLAUDE.md`.
166
183
 
@@ -0,0 +1,58 @@
1
+ ---
2
+ title: Architecture anchor staleness sweep
3
+ description: How an anchored decision's cited paths are collected, the finding the diff fires, and the report line
4
+ ---
5
+
6
+ # Architecture anchor staleness sweep
7
+
8
+ Mechanics for Step 6 of `claude-docs`. The body owns the skip conditions, the report-only constraint, and the standard citation, and this file owns what the sweep does once the diff touches a path an anchored decision cites.
9
+
10
+ ## Anchored entries
11
+
12
+ Read `.claude/ARCHITECTURE.md` and take the H3 entries under `## Key technical decisions`. An entry is anchored when its reasoning closes on the marker the standard fixes:
13
+
14
+ ```plaintext
15
+ Measured at <short-sha> on <YYYY-MM-DD>.
16
+ ```
17
+
18
+ Skip every entry without one. An unanchored entry either predates the rule or cites no measured number, and the standard calls both correct, so the sweep has nothing to say about either.
19
+
20
+ For each anchored entry, collect the backticked code paths its reasoning cites. Same read the diagram sweep runs over an explanation, against a paragraph instead of a diagram.
21
+
22
+ Most of these name a folder rather than a file, because the number an entry anchors is usually a count over a tree. Match accordingly:
23
+
24
+ - A citation naming a file matches when the diff carries that file, wherever it sits. A file at the repository root such as `CLAUDE.md` is a file citation like any other.
25
+ - A folder citation, which is one ending in a slash, matches when the diff carries any path under it.
26
+ - A folder citation of a single segment matches nothing and is skipped.
27
+
28
+ The third rule reaches folders alone, which is what keeps it off `CLAUDE.md`. Breadth is the whole reason it exists, and a root file has none: it names one path the diff either carries or does not.
29
+
30
+ What it does stop is a citation of `.claude/` matching every branch. `src/`, `scripts/`, and `.claude/` each name a tree a project organizes most of its work under, so a prefix match on one fires constantly and rebuilds the ignored warning this sweep exists to avoid. Measured against this repository's record at `87b11e87`, the skip drops 13 of the 26 folder citations and keeps the ones carrying a signal, among them `src/tooling/`, `.claude/rules/`, and `governance/rules/`.
31
+
32
+ A decision whose folder citations are all a single segment, and which cites no file, therefore never fires. That is the correct outcome rather than a gap: a count over a whole tree moves on nearly every branch, so flagging it every time tells a reader that time has passed and nothing else.
33
+
34
+ ## The finding
35
+
36
+ Report an entry when the diff touches one of its cited paths, however it was touched. A delete, a rename, and an edit inside the file all move a count of what sits under that path, and the entry's number was read before any of them landed. The diagram sweep narrows to deletes because a diagram survives a body edit, and a number does not.
37
+
38
+ The anchor's own SHA settles nothing here. A branch is compared against its merge base rather than against the commit the anchor names, so an entry anchored at a commit this branch already contains is still due a read once the branch moves what it counted.
39
+
40
+ Report an entry once however many of its cited paths the diff carries. Name the first as the evidence and leave the rest, since the reader opens the entry either way and a line per path buries the entry it is about.
41
+
42
+ What this misses is a claim whose number moved with no matching citation in the diff, whether because the branch never touched the tree it counts or because the entry cites nothing narrower than one segment. That is the recall this trades for precision, and the alternative is re-running an arbitrary measurement read out of prose, which no sweep does reliably.
43
+
44
+ ## What the sweep never does
45
+
46
+ - Write into the entry. The record carries no frontmatter, so the marker and the claim share one paragraph and an edit reaching one reaches the other.
47
+ - Refresh an anchor. Re-reading the number is the act the marker records, so a date written by a pass that measured nothing is the false confidence the marker exists to prevent.
48
+ - Flag an unanchored entry. The standard scopes the rule forward, and an entry written before it is dated by blame rather than by a read.
49
+
50
+ The session amending a decision writes its anchor, which Step 3 already requires.
51
+
52
+ ## Output
53
+
54
+ Output one line per finding:
55
+
56
+ `⚠ Anchor stale: "<decision name>" cites <path>, which this branch changed. Re-measure and refresh the anchor.`
57
+
58
+ If no anchored entry cites a changed path, skip silently. A record carrying no anchored entry produces no output on any branch, which is every project until a decision is written or amended under the rule.
@@ -9,33 +9,33 @@ Full help: `aitk <command> --help`. Behavior notes for the install and sync verb
9
9
 
10
10
  ## Project-level
11
11
 
12
- | Command | Purpose |
13
- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
14
- | `aitk init [path]` | Bootstrap a project with selected toolkit domains |
15
- | `aitk sync [path]` | Sync all installed domains in a target project |
16
- | `aitk sync --check` | Report toolkit drift without writing (`--json`, `--exit-code`) |
17
- | `aitk sandbox [cat:cmd]` | Run sandbox scenarios (interactive or routed), toolkit-only like the tree it reads |
18
- | `aitk sandbox reset` | Reset sandbox to baseline |
19
- | `aitk sandbox clean` | Wipe the sandbox |
20
- | `aitk sandbox check` | Score a provisioned sandbox against a scenario expectation (`--json` for the verdict) |
21
- | `aitk sandbox coverage` | Report which scenarios declare expectations (`--json`, `--strict`, `--skills`) |
22
- | `aitk indexes regen` | Regenerate `index.md` files from sibling frontmatter |
23
- | `aitk docs [topic]` | Emit toolkit reference docs (`list`, or a topic by name) |
24
- | `aitk design render` | Render `.claude/DESIGN.md` tokens to HTML and CSS |
25
- | `aitk slides render` | Render a `.claude/SLIDES.md` source into a PowerPoint deck |
26
- | `aitk slides list` | List the available slide layouts (`--json` for the catalog) |
27
- | `aitk feedback` | Write toolkit feedback from stdin to `.claude/review/`, or open a GitHub issue with `--github` |
28
- | `aitk transcripts <url>` | Fetch a YouTube transcript with metadata frontmatter (needs `yt-dlp`) |
29
- | `aitk tasks archive` | Move a shipped task off the board, clear its ordering row, and regenerate the index |
30
- | `aitk tasks pull-request` | Record a pull request number on the task a branch closes, by stem or `--plan` (`--json`) |
31
- | `aitk tasks outcome` | Mark outcomes `[x]` on a task by position, repeating `--close` (`--json`) |
32
- | `aitk tasks validate` | Report board rows whose plan, task file, group, or file set does not hold (`--json`) |
33
- | `aitk records validate` | Report a session record against the standard governing it, per kind (`--json`) |
34
- | `aitk comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git |
35
- | `aitk context audit` | Report required sections, length, cited paths, catalog tables, provenance, superseded-decision narration, and index drift |
36
- | `aitk markdown audit` | Fail any markdown path on a banned character, word, or spelling, and report the structural checkpoints |
37
- | `aitk claude skills audit` | Report both skill corpora against the mechanical rules in `standards/skill.md` |
38
- | `aitk capture [source]` | Render HTML capture sources to PNG, toolkit-only and absent from an installed package |
12
+ | Command | Purpose |
13
+ | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
14
+ | `aitk init [path]` | Bootstrap a project with selected toolkit domains |
15
+ | `aitk sync [path]` | Sync all installed domains in a target project |
16
+ | `aitk sync --check` | Report toolkit drift without writing (`--json`, `--exit-code`) |
17
+ | `aitk sandbox [cat:cmd]` | Run sandbox scenarios (interactive or routed), toolkit-only like the tree it reads |
18
+ | `aitk sandbox reset` | Reset sandbox to baseline |
19
+ | `aitk sandbox clean` | Wipe the sandbox |
20
+ | `aitk sandbox check` | Score a provisioned sandbox against a scenario expectation (`--json` for the verdict) |
21
+ | `aitk sandbox coverage` | Report which scenarios declare expectations (`--json`, `--strict`, `--skills`) |
22
+ | `aitk indexes regen` | Regenerate `index.md` files from sibling frontmatter |
23
+ | `aitk docs [topic]` | Emit toolkit reference docs (`list`, or a topic by name) |
24
+ | `aitk design render` | Render `.claude/DESIGN.md` tokens to HTML and CSS |
25
+ | `aitk slides render` | Render a `.claude/SLIDES.md` source into a PowerPoint deck |
26
+ | `aitk slides list` | List the available slide layouts (`--json` for the catalog) |
27
+ | `aitk feedback` | Write toolkit feedback from stdin to `.claude/review/`, or open a GitHub issue with `--github` |
28
+ | `aitk transcripts <url>` | Fetch a YouTube transcript with metadata frontmatter (needs `yt-dlp`) |
29
+ | `aitk tasks archive` | Move a shipped task off the board, clear its ordering row, and regenerate the index |
30
+ | `aitk tasks pull-request` | Record a pull request number on the task a branch closes, by stem or `--plan` (`--json`) |
31
+ | `aitk tasks outcome` | Mark outcomes `[x]` on a task by position, repeating `--close` (`--json`) |
32
+ | `aitk tasks validate` | Report board rows whose plan, task file, group, or file set does not hold (`--json`) |
33
+ | `aitk records validate` | Report a session record against the standard governing it, per kind (`--json`) |
34
+ | `aitk comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git |
35
+ | `aitk context audit` | Report required sections, length, cited paths, reference form, catalog tables, provenance, superseded-decision narration, and index drift |
36
+ | `aitk markdown audit` | Fail any markdown path on a banned character, word, or spelling, and report the structural checkpoints |
37
+ | `aitk claude skills audit` | Report both skill corpora against the mechanical rules in `standards/skill.md` |
38
+ | `aitk capture [source]` | Render HTML capture sources to PNG, toolkit-only and absent from an installed package |
39
39
 
40
40
  ## Domain commands
41
41
 
@@ -17,6 +17,16 @@ It reports rather than gates by default, the closer call because a missing secti
17
17
 
18
18
  `--gate` promotes the finding to a failing exit code, which the seed stage runs and no other caller does. That mode needs an answer to the sanctioned omission above, so a file declaring `stub: true` in its frontmatter is dropped before the check and reported nowhere. Both are described in `context-audit.md`.
19
19
 
20
+ ## Reference form
21
+
22
+ The reference-form check reports an entry naming a sibling entry by bare filename where `.claude/standards/context.md` asks for the path it sits at. A bare name resolves against whichever folder its reader is already in, so a domain that splits into subfolders strands every inbound reference and the break surfaces nowhere. A path is checkable, which is what makes the form rule worth measuring at all: once a reference spells its path, the citation gate resolves it and a split that moves the file fails the push.
23
+
24
+ A finding is a backticked filename carrying no folder, matched against the entries beside the one that wrote it. The backticks are required, since a filename in running prose is not a reference a reader follows. A name matching no sibling is left alone, which is the measure reaching less than the rule does rather than the rule stopping there. The standard governs a reference to any other entry, so a split entry naming one that sits in a different folder is a violation this check never sees. What the sibling set buys is that a name resolving inside the folder is a reference by construction, where a bare filename matched anywhere would report every sentence that happens to name a file. An entry naming itself is left alone on separate grounds, since no split can strand it. Fenced blocks are excluded with the scans above, and a line carrying the citation ignore marker is excluded because that marker already means the line displays a name rather than pointing at one.
25
+
26
+ The check covers the folders a domain split into and stops at the flat folder above them. A split folder's entries are named for sub-areas of one domain, so a bare name matching one of them points at it. The flat folder's entries are named for whole domains, and a domain name is a common noun that a seed or another tree spells the same way, which is where both false positives measured against this corpus sat. What the exemption costs is the references a future split of the flat folder would strand, and it is taken because a report firing on correct prose teaches a reader to stop reading the section.
27
+
28
+ It reports and never gates, which is the split the citation check already draws. An unresolved path is a fact and a form violation is a judgment with a measured false-positive rate, the same line `aitk markdown audit` draws between a ban and a weight checkpoint. The JSON record carries the findings per entry as `entries[].bareReferences`.
29
+
20
30
  ## Length
21
31
 
22
32
  Length quotes its checkpoint from `.claude/standards/context.md`: roughly 150 rendered lines for an entry, applied to each of the 60 entries the audit measures, 51 of them under `.claude/context/` and 9 diagram files beside them. It counts rendered lines rather than source lines, wrapping each line at 80 columns and summing the heights. A line is wrapped at the width it renders at, so a link counts as its anchor text and a backticked path counts whole, which parts the rendered count from a source count wherever an entry cites a path in backticks rather than as a link. Entries here are authored one line per bullet, so a block of fifteen paragraph-bullets occupies fifteen source lines and renders past sixty, which source counting cannot see. The measure counts fenced blocks and frontmatter, so a reference-heavy entry ranks by its examples, which the legend states on every run alongside the width, since a number in rendered lines cannot be reproduced without it.
@@ -51,7 +61,7 @@ Precision is the whole value, so recall is the accepted exposure, and two shapes
51
61
 
52
62
  ## Which folders each check reaches
53
63
 
54
- The provenance, required-section, and narration checks cover `.claude/context/` alone, while length and the table finding reach every audited folder. What narrows the three is stated in `.claude/standards/context.md`, which opens its scope by handing diagrams and wireframes to `diagrams.md` and `wireframes.md`, and the sibling standards do not restate it. A marker reported in a diagram entry would cite a rule that entry's own standard routes elsewhere, and a diagram entry carries a heading per kind rather than a run of bullets deciding anything. The split is between kinds of rule rather than kinds of folder, and what decides it is which tier states the rule rather than what the check measures. Length and the table finding generalize as judgments about how far a reader travels, so both reach wherever the audit is pointed. Required sections narrow for a plainer reason: the names are the context standard's own, and neither sibling standard states a required section at all.
64
+ The provenance, required-section, and narration checks cover `.claude/context/` alone, the reference-form check covers the split folders inside it, and length and the table finding reach every audited folder. What narrows the three is stated in `.claude/standards/context.md`, which opens its scope by handing diagrams and wireframes to `diagrams.md` and `wireframes.md`, and the sibling standards do not restate it. A marker reported in a diagram entry would cite a rule that entry's own standard routes elsewhere, and a diagram entry carries a heading per kind rather than a run of bullets deciding anything. The split is between kinds of rule rather than kinds of folder, and what decides it is which tier states the rule rather than what the check measures. Length and the table finding generalize as judgments about how far a reader travels, so both reach wherever the audit is pointed. Required sections narrow for a plainer reason: the names are the context standard's own, and neither sibling standard states a required section at all.
55
65
 
56
66
  The same test is what moved depth and bullet weight out of this command entirely. A rule stated at the attribute tier reaches every markdown file, and a check reaching every markdown file has no reason to require a folder that resolves. The scoping key is the folder an entry was audited under, so `--folder` still reaches a folder the default list does not carry, and a domain split into `context/<sub-area>/` is governed as `context`. Every run states the reach, including a run where no audited folder is the governed one. The JSON record carries it as `checkpoints.provenanceFolder` and a per-folder `governsContent`.
57
67
 
@@ -37,7 +37,7 @@ A run where no requested name resolves refuses, whichever list it read. Naming t
37
37
 
38
38
  ## Exit codes
39
39
 
40
- Exit codes are `0` for a clean run, `1` for a refusal, and `2` for a gating finding. An unresolved citation gates under every mode. Length, table, provenance, and narration findings print and return `0` under every mode, because each is a judgment and failing a push on one would make the check something to route around. Narration is the weakest of the four, since whether two bullets share a subject is a call the measure approximates from structure alone, and one of the shapes it matches is the rejected alternative the standard asks an entry to keep.
40
+ Exit codes are `0` for a clean run, `1` for a refusal, and `2` for a gating finding. An unresolved citation gates under every mode. Length, reference form, table, provenance, and narration findings print and return `0` under every mode, because each is a judgment and failing a push on one would make the check something to route around. Narration is the weakest of the five, since whether two bullets share a subject is a call the measure approximates from structure alone, and one of the shapes it matches is the rejected alternative the standard asks an entry to keep.
41
41
 
42
42
  Required-section and index findings sit between the two. Both are answerable from the file rather than weighed, so `--gate` promotes them to failing codes while a bare run leaves them advisory. The toolkit runs the bare form against itself and the widened form against the seed tree, described below.
43
43
 
@@ -76,7 +76,7 @@ Three exclusions keep it from firing on prose about paths. Fenced blocks are ski
76
76
 
77
77
  Two cases remain, and no syntax separates either from a real reference. One is a sentence naming a hypothetical entry to show the shape of a name. The other is a line instructing a target project about its own tree, which resolves against the target rather than against this repository and passes here only while the two layouts agree. Splitting a seeded domain into a folder ends that agreement and turns every such line red at once.
78
78
 
79
- Append `<!-- audit-ignore-citations -->` to the source line in either case. The marker suppresses citation checking for its own line only.
79
+ Append `<!-- audit-ignore-citations -->` to the source line in either case. The marker suppresses citation checking for its own line only, and the reference-form check reads it too, since both ask whether a line points at a file and the marker is how a line says it displays a name instead.
80
80
 
81
81
  The marker itself stays out of anything that installs. A seed, a plugin skill body, and a stack reference all reach a target, so a marker there lands as toolkit bookkeeping in someone else's tree. Reword those lines to drop the path instead, and where a stop message has to spell it, move that message into a fenced block, which this check already skips.
82
82
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "0.78.1",
4
+ "version": "0.80.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -4,6 +4,7 @@ import {
4
4
  type EntryReport,
5
5
  governsContent,
6
6
  LENGTH_CHECKPOINT,
7
+ matchesSiblings,
7
8
  measureFolders,
8
9
  missingSections,
9
10
  PROVENANCE_FOLDER,
@@ -60,7 +61,7 @@ export function register(program: Command): void {
60
61
  context
61
62
  .command('audit')
62
63
  .description(
63
- 'Report required sections, entry length, citations, catalog tables, provenance, superseded-decision narration, and index drift',
64
+ 'Report required sections, entry length, citations, reference form, catalog tables, provenance, superseded-decision narration, and index drift',
64
65
  )
65
66
  .argument('[path]', 'Project root, defaulting to the current directory')
66
67
  .helpOption('-h, --help', 'Show this help message')
@@ -85,8 +86,9 @@ export function register(program: Command): void {
85
86
  '',
86
87
  'An unresolved citation always gates. --gate widens the gate to the',
87
88
  'other two findings that are facts rather than judgments: a missing',
88
- 'required section and index drift. Length, table, provenance, and',
89
- 'narration findings are judgments and stay advisory under both.',
89
+ 'required section and index drift. Length, reference form, table,',
90
+ 'provenance, and narration findings are judgments and stay advisory',
91
+ 'under both.',
90
92
  '',
91
93
  'Depth and bullet weight are stated over every markdown file rather',
92
94
  'than over a context entry, so `aitk markdown audit` measures them.',
@@ -205,6 +207,7 @@ async function runAudit(
205
207
  intro('aitk context audit')
206
208
  reportScope(folders, unresolved)
207
209
  reportCitations(citations, cited)
210
+ reportReferenceForm(entries, folders)
208
211
  reportSections(sections, folders)
209
212
  reportLength(entries)
210
213
  reportTables(entries)
@@ -370,6 +373,65 @@ function reportCitations(
370
373
  )
371
374
  }
372
375
 
376
+ /**
377
+ * Reports the references naming a sibling entry by bare filename.
378
+ *
379
+ * This prints beside the citation check rather than among the readability
380
+ * measures, since the two read the same thing: one resolves a path a reference
381
+ * spells and this one finds the references that spell none. The reach line
382
+ * names the split folders rather than the governed folder alone, because a run
383
+ * whose only context folder is flat measures nothing here and would otherwise
384
+ * print the same clean line as a run that measured every split.
385
+ */
386
+ function reportReferenceForm(
387
+ entries: readonly EntryReport[],
388
+ folders: readonly AuditedFolder[],
389
+ ): void {
390
+ logStep('Reference form')
391
+
392
+ const scoped = folders.filter(matchesSiblings)
393
+ if (scoped.length === 0) {
394
+ logInfo(
395
+ `Out of scope. A bare name is matched against the siblings of a domain split into a folder, and no audited folder under .claude/${PROVENANCE_FOLDER}/ is one.`,
396
+ )
397
+ return
398
+ }
399
+
400
+ logInfo(
401
+ `Covers ${plural(scoped.length, 'split folder')} under .claude/${PROVENANCE_FOLDER}/, whose standard asks a reference to spell its path.`,
402
+ )
403
+ logInfo(
404
+ 'The flat folder is out of reach, since a domain filename there is shared by seeds and other trees.',
405
+ )
406
+
407
+ const carrying = entries
408
+ .filter((entry) => entry.bareReferences.length > 0)
409
+ .sort((a, b) => b.bareReferences.length - a.bareReferences.length)
410
+
411
+ if (carrying.length === 0) {
412
+ logInfo('Every reference to a sibling entry spells its path.')
413
+ return
414
+ }
415
+
416
+ const total = carrying.reduce(
417
+ (sum, entry) => sum + entry.bareReferences.length,
418
+ 0,
419
+ )
420
+ logWarn(
421
+ `${plural(total, 'bare name')} across ${carrying.length} ${carrying.length === 1 ? 'entry' : 'entries'}`,
422
+ )
423
+ pipeOutput(
424
+ carrying
425
+ .map(
426
+ (entry) =>
427
+ `${entry.rel} ${plural(entry.bareReferences.length, 'bare name')}\n${entry.bareReferences
428
+ .map((found) => ` :${found.line} ${found.name}`)
429
+ .join('\n')}`,
430
+ )
431
+ .join('\n'),
432
+ )
433
+ }
434
+
373
435
  /**
374
436
  * Names the path each finding belongs to, which is an entry in the folder named
375
437
  * under `.claude/` and the folder itself in a domain split across one. States
@@ -1,5 +1,6 @@
1
1
  import { readFile } from 'node:fs/promises'
2
- import { relative } from 'node:path'
2
+ import { basename, relative } from 'node:path'
3
+ import { BARE_NAME, IGNORE_MARKER } from '@/context/citations'
3
4
  import type { AuditedFolder } from '@/context/folders'
4
5
  import type { NarrationTerms } from '@/context/narration'
5
6
  import { type BodyLine, bodyLines, maskDisplayed } from '@/markdown/scan'
@@ -109,6 +110,12 @@ export interface ProvenanceFinding {
109
110
  readonly text: string
110
111
  }
111
112
 
113
+ export interface BareReferenceFinding {
114
+ readonly line: number
115
+ /** The name as written, so a report says which reference to respell. */
116
+ readonly name: string
117
+ }
118
+
112
119
  export interface NarrationFinding {
113
120
  readonly line: number
114
121
  /** The opening that points back at the bullet above. */
@@ -141,6 +148,12 @@ export interface EntryReport {
141
148
  * since an empty list here is silent about which one produced it.
142
149
  */
143
150
  readonly narration: readonly NarrationFinding[]
151
+ /**
152
+ * References naming a sibling entry by bare filename, and empty for a caller
153
+ * that passed no sibling names. Which entries have siblings worth matching is
154
+ * the caller's judgment, stated where it builds the list.
155
+ */
156
+ readonly bareReferences: readonly BareReferenceFinding[]
144
157
  /**
145
158
  * Required sections this entry declares, in the standard's order, and empty
146
159
  * outside the folder whose standard names them. What the folder is short of
@@ -397,18 +410,62 @@ function declaredSections(lines: readonly BodyLine[]): string[] {
397
410
  return REQUIRED_SECTIONS.filter((section) => found.has(section))
398
411
  }
399
412
 
413
+ /**
414
+ * Finds the references naming a sibling entry by bare filename.
415
+ *
416
+ * The standard asks for the path because a bare name resolves against whichever
417
+ * folder the reader is in, so a folder split strands every inbound reference
418
+ * and nothing reads the break. A path is checkable and a bare name is not,
419
+ * which is what makes this the one form rule worth measuring.
420
+ *
421
+ * Matching stops at the sibling set, which reaches less than the rule does. The
422
+ * standard governs a reference to any other entry, so a split entry naming one
423
+ * that sits in a different folder is a violation this never sees. What the set
424
+ * buys is that a name resolving inside the folder is a reference by
425
+ * construction, where a bare filename matched anywhere would report every
426
+ * sentence that happens to name a file. The entry's own name is out of the set
427
+ * on separate grounds, since naming itself points at nothing a split can strand.
428
+ *
429
+ * Fenced blocks are skipped for the reason the scans above skip them, and a line
430
+ * carrying the citation ignore marker is skipped because that marker already
431
+ * means the line displays a name rather than pointing at one.
432
+ */
433
+ function bareReferences(
434
+ lines: readonly BodyLine[],
435
+ siblings: readonly string[],
436
+ ): BareReferenceFinding[] {
437
+ if (siblings.length === 0) return []
438
+
439
+ const named = new Set(siblings)
440
+ const findings: BareReferenceFinding[] = []
441
+
442
+ for (const line of lines) {
443
+ if (line.fenced || line.text.includes(IGNORE_MARKER)) continue
444
+
445
+ for (const match of line.text.matchAll(BARE_NAME)) {
446
+ if (named.has(match[1])) {
447
+ findings.push({ line: line.number, name: match[1] })
448
+ }
449
+ }
450
+ }
451
+
452
+ return findings
453
+ }
454
+
400
455
  /**
401
456
  * Measures one entry, scanning for provenance only when a standard claims it.
402
457
  *
403
458
  * The caller passes jurisdiction rather than deriving it from `rel`, because a
404
459
  * path prefix hardcodes what `--folder` exists to override and misses a domain
405
- * split into `context/<sub-area>/`.
460
+ * split into `context/<sub-area>/`. Sibling names arrive the same way and for
461
+ * the same reason, since the folder an entry sits in is what holds them.
406
462
  */
407
463
  export function measureEntry(
408
464
  rel: string,
409
465
  source: string,
410
466
  governsContent = true,
411
467
  terms?: NarrationTerms,
468
+ siblings: readonly string[] = [],
412
469
  ): EntryReport {
413
470
  const lines = bodyLines(source)
414
471
 
@@ -421,6 +478,7 @@ export function measureEntry(
421
478
  catalogTables: catalogTables(lines),
422
479
  provenance: governsContent ? provenance(lines) : [],
423
480
  narration: governsContent && terms ? narration(lines, terms) : [],
481
+ bareReferences: bareReferences(lines, siblings),
424
482
  sections: governsContent ? declaredSections(lines) : [],
425
483
  stub: isStubSeed(source),
426
484
  }
@@ -442,13 +500,20 @@ export async function measureFolders(
442
500
  const reports: EntryReport[] = []
443
501
 
444
502
  for (const folder of folders) {
503
+ const names = matchesSiblings(folder)
504
+ ? folder.entries.map((path) => basename(path))
505
+ : []
506
+
445
507
  for (const path of folder.entries) {
508
+ const self = basename(path)
509
+
446
510
  reports.push(
447
511
  measureEntry(
448
512
  relative(root, path),
449
513
  await readFile(path, 'utf8'),
450
514
  governsContent(folder),
451
515
  terms,
516
+ names.filter((name) => name !== self),
452
517
  ),
453
518
  )
454
519
  }
@@ -462,6 +527,26 @@ export function governsContent(folder: AuditedFolder): boolean {
462
527
  return folder.name === PROVENANCE_FOLDER
463
528
  }
464
529
 
530
+ /**
531
+ * Reports whether a bare sibling name here is a reference by construction.
532
+ *
533
+ * A split folder's entries are named for sub-areas of one domain, so a bare
534
+ * name matching one of them points at it and nothing else. The folder named
535
+ * under `.claude/` is where that stops holding, since its entries are named for
536
+ * whole domains and a domain name is a common noun that a seed, a script, or
537
+ * another tree spells the same way. Both false positives this measure was tuned
538
+ * against sat there, naming a seed that shares a filename with the entry beside
539
+ * them, and no signal in the name separates the two.
540
+ *
541
+ * What the exemption costs is the references a future split of the named folder
542
+ * would strand, which are the ones this measure would most like to hold. It is
543
+ * taken because a report firing on correct prose is what teaches a reader to
544
+ * stop reading the section.
545
+ */
546
+ export function matchesSiblings(folder: AuditedFolder): boolean {
547
+ return governsContent(folder) && folder.nested
548
+ }
549
+
465
550
  /**
466
551
  * Names what does not declare the sections the standard requires.
467
552
  *
@@ -49,6 +49,23 @@ export function citationPattern(folders: readonly string[]): RegExp {
49
49
  )
50
50
  }
51
51
 
52
+ /**
53
+ * A backticked filename carrying no folder, the form a reference takes when it
54
+ * names a sibling rather than a path.
55
+ *
56
+ * `citationPattern` spells the `.claude/` prefix and cannot see this shape at
57
+ * all, which is the reason the form rule exists. Widening that expression to
58
+ * admit a bare name was the alternative and it puts one match in the position of
59
+ * answering two questions, since a spelled path is a reference by construction
60
+ * and a bare name is a candidate whichever caller found it still has to test
61
+ * against the folder it sits in.
62
+ *
63
+ * The backticks are required rather than incidental. A filename written into
64
+ * running prose without them is not a reference a reader follows, and matching
65
+ * one would report every sentence that happens to name a file.
66
+ */
67
+ export const BARE_NAME = /`([A-Za-z0-9._-]+\.md)`/g
68
+
52
69
  /**
53
70
  * Pulls the cited paths out of one file's text.
54
71
  *
@@ -83,6 +83,7 @@ Only the `development` entry carries this section. It is not a general-purpose h
83
83
  - Decisions specific to the domain. Broader cross-domain decisions belong in `.claude/ARCHITECTURE.md`.
84
84
  - Constraints, gotchas, things tried and rejected
85
85
  - Domain-specific conventions that do not fit a `paths:`-scoped rule
86
+ - A reference to another entry, spelled as the path that entry sits at rather than as its bare filename. A bare name resolves against whichever folder the reader is already in, so a domain that splits into subfolders strands every inbound reference and the break surfaces nowhere. A reference to a seed, a standard, or a file the project owns elsewhere keeps the form its own surface uses.
86
87
 
87
88
  ## What does not go in
88
89