@erclx/aitk 0.79.0 → 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.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/docs/agents/commands.md +27 -27
- package/docs/agents/context-audit-checks.md +11 -1
- package/docs/agents/context-audit.md +2 -2
- package/package.json +1 -1
- package/src/commands/context.ts +65 -3
- package/src/context/audit.ts +87 -2
- package/src/context/citations.ts +17 -0
- package/standards/context.md +1 -0
package/docs/agents/commands.md
CHANGED
|
@@ -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,
|
|
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
|
|
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
package/src/commands/context.ts
CHANGED
|
@@ -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,
|
|
89
|
-
'narration findings are judgments and stay advisory
|
|
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
|
package/src/context/audit.ts
CHANGED
|
@@ -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
|
*
|
package/src/context/citations.ts
CHANGED
|
@@ -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
|
*
|
package/standards/context.md
CHANGED
|
@@ -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
|
|