@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.
@@ -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.79.0",
4
+ "version": "0.80.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -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.79.0",
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