@erclx/aitk 0.36.0 → 0.38.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/claude/skills/claude-design-extract/REQUIREMENT.md +42 -0
- package/claude/skills/claude-diagram/REQUIREMENT.md +45 -0
- package/claude/skills/claude-groundwork/REQUIREMENT.md +44 -0
- package/claude/skills/claude-memory-capture/REQUIREMENT.md +40 -0
- package/claude/skills/claude-orchestrate/REQUIREMENT.md +44 -0
- package/claude/skills/claude-roadmap/REQUIREMENT.md +41 -0
- package/claude/skills/claude-screencast/REQUIREMENT.md +39 -0
- package/claude/skills/claude-slides-draft/REQUIREMENT.md +39 -0
- package/claude/skills/claude-tasks/REQUIREMENT.md +43 -0
- package/claude/skills/claude-ui-test/REQUIREMENT.md +40 -0
- package/claude/skills/claude-ux-audit/REQUIREMENT.md +39 -0
- package/claude/skills/claude-worktree/REQUIREMENT.md +41 -0
- package/claude/skills/cli-script/REQUIREMENT.md +40 -0
- package/claude/skills/create-rule/REQUIREMENT.md +42 -0
- package/docs/agents.md +21 -8
- package/docs/operating-model.md +1 -1
- package/docs/visual-design-workflow.md +1 -1
- package/package.json +1 -1
- package/src/commands/context.ts +129 -14
- package/src/context/audit.ts +119 -0
- package/src/context/folders.ts +95 -12
- package/tooling/base/seeds/.claude/context/ci.md +7 -1
- package/tooling/base/seeds/.claude/context/development.md +8 -1
- package/tooling/base/seeds/.claude/context/index.md +11 -0
- package/tooling/claude/seeds/.claude/context/index.md +0 -8
package/docs/agents.md
CHANGED
|
@@ -80,7 +80,7 @@ Full help: `aitk <command> --help`.
|
|
|
80
80
|
| `aitk transcripts <url>` | Fetch a YouTube transcript with metadata frontmatter (needs `yt-dlp`) |
|
|
81
81
|
| `aitk tasks archive` | Move a shipped task off the board, clear its ordering row, and regenerate the index |
|
|
82
82
|
| `aitk comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git |
|
|
83
|
-
| `aitk context audit` | Report
|
|
83
|
+
| `aitk context audit` | Report required sections, length, depth, bullet weight, cited paths, provenance, and drift |
|
|
84
84
|
| `aitk capture [source]` | Render HTML capture sources to PNG, toolkit-only and absent from an installed package |
|
|
85
85
|
|
|
86
86
|
### Domain commands
|
|
@@ -377,20 +377,31 @@ aitk context audit
|
|
|
377
377
|
aitk context audit --json
|
|
378
378
|
aitk context audit --citations-only
|
|
379
379
|
aitk context audit --folder context,diagrams
|
|
380
|
+
aitk context audit --folder docs
|
|
380
381
|
```
|
|
381
382
|
|
|
382
|
-
| Option | Behavior
|
|
383
|
-
| ------------------ |
|
|
384
|
-
| `--json` | Add a machine-readable record on stdout, keeping the frame
|
|
385
|
-
| `--folder <list>` | Comma-separated folder names
|
|
386
|
-
| `--citations-only` | Run the gating citation check alone, printing nothing when it passes
|
|
383
|
+
| Option | Behavior |
|
|
384
|
+
| ------------------ | -------------------------------------------------------------------- |
|
|
385
|
+
| `--json` | Add a machine-readable record on stdout, keeping the frame |
|
|
386
|
+
| `--folder <list>` | Comma-separated folder names (default: the three below) |
|
|
387
|
+
| `--citations-only` | Run the gating citation check alone, printing nothing when it passes |
|
|
387
388
|
|
|
388
389
|
Scope defaults to `context`, `diagrams`, and `wireframes`, and a folder the project does not carry is skipped rather than reported. A domain that outgrew one file and split into `<domain>/` is audited as its own folder, so a split entry measures at the same grain as a flat one.
|
|
389
390
|
|
|
390
|
-
|
|
391
|
+
A name passed to `--folder` resolves under `.claude/` first and at the project root second, which is what puts `docs/` and any later corpus in reach of the same engine. The root base is reached only by a name the caller passes, so the default list still resolves under `.claude/` alone and a project holding a root `wireframes/` is not audited against a standard it never adopted. The scope line prints the resolved path, so a project carrying both spellings reads which one was taken. The JSON record carries the base per folder as `folders[].base`.
|
|
392
|
+
|
|
393
|
+
A run where no requested name resolves refuses, whichever list it read. Naming the absent ones narrows to `--folder`, since a project carrying one of the three default folders is the ordinary case and a name it never asked for is not a typo. The JSON record carries those names as `unresolvedFolders`.
|
|
394
|
+
|
|
395
|
+
Exit codes are `0` for a clean run, `1` for a refusal, and `2` for an unresolved citation. Only the citation check sets a failing code. Required-section, length, depth, bullet weight, table, provenance, and index findings print and return `0`, because each is a judgment and failing a push on one would make the check something to route around.
|
|
391
396
|
|
|
392
397
|
### What each check reports
|
|
393
398
|
|
|
399
|
+
The required-section check reports what does not declare `## Overview` and `## Layout`, the two sections `.claude/standards/context.md` marks required. The list is held in code beside the numeric checkpoints rather than parsed out of the standard, so it fails on a defect rather than on a rewrite of the wording around it. A heading at any level counts, because a domain that split into a folder carries its overview in a sibling named `overview.md` where the section is the `#` title and an `##` beneath it would repeat the filename. Matching exactly is what keeps `## Layout catalog` from satisfying `Layout`.
|
|
400
|
+
|
|
401
|
+
Which unit answers depends on the folder. Entries of the folder named under `.claude/` are one domain each, so each answers for itself and a finding names the entry. Entries of a folder a domain split into describe that one domain between them, so any sibling answers and a finding names the folder. Holding a split folder to the rule per file would report every child beside its `overview.md`, and rolling the named folder up would let one conforming entry stand in for every other domain sitting next to it.
|
|
402
|
+
|
|
403
|
+
It reports rather than gates, the closer call because a missing section reads more like a fact than the other judgments. What settles it is that the standard sanctions omitting `## Layout` from a domain owning no paths in the repo, and no measure separates that from an entry that forgot it. A domain covering only external tools is that case, and it reports on every run. The JSON record carries the findings as `missingSections` and the list as `checkpoints.requiredSections`.
|
|
404
|
+
|
|
394
405
|
Length and depth quote their checkpoints from `.claude/standards/context.md`: roughly 150 rendered lines for an entry, roughly 40 for a run no heading breaks. Depth measures the longest such run rather than everything under one `##`, skips fenced blocks so a markdown example does not read as three headings, and exempts a run whose lines are all list items at one indent averaging under 130 characters. The weight condition is what separates a scannable catalog of one-liners from a stack of paragraph-bullets, which reach the same count and read nothing alike.
|
|
395
406
|
|
|
396
407
|
Both checks count rendered lines rather than source lines, wrapping each line at 80 columns and summing the heights. 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. Measuring one checkpoint in each unit would put an entry length beside a run length that mean different things. Their exclusions still differ: the file measure counts fenced blocks and frontmatter, while the run measure skips a fence so an example cannot break the run around it. A reference-heavy entry therefore ranks by its examples, which the length legend states on every run. Runs count blank lines, which the standard leaves open, so a hand reader who drops them lands a line or two lower. Both sections state the width on every run, since a number in rendered lines cannot be reproduced without it.
|
|
@@ -401,7 +412,7 @@ The table check reports a catalog that grows a row per shipped thing, not a tabl
|
|
|
401
412
|
|
|
402
413
|
The provenance check reports the markers narrating how a domain reached its shape rather than describing what it is: a date, a change number, or a release label. The standard admits a rejected alternative and the reasoning that killed it while refusing the provenance attached to it, so a marker names a line to read rather than a line to delete. Findings group by entry and sort left to right within a line, since what a reader acts on is which file to open. Fenced blocks are excluded, which keeps a pinned version in an install command from reading as a claim the entry makes. Frontmatter is excluded with them, since the content checks read the body alone, and that is what keeps a diagram entry's dated `verified` stamp a record of its last check rather than a marker to settle. Length is the exception, counting the whole file, so a reader applying the 150-rendered-line checkpoint against the body alone lands a few lines under what the tool reports.
|
|
403
414
|
|
|
404
|
-
The provenance
|
|
415
|
+
The provenance, bullet-weight, and required-section checks cover `.claude/context/` alone, while length, depth, and the table finding reach every audited folder. The rule 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. The split is between kinds of rule rather than kinds of folder, and what decides it is whether the remedy is actionable rather than what the check measures. Subdividing a run and splitting a file mean something in any entry, so length and depth generalize. Moving an incident out of a bullet and keeping the decision means nothing in a folder whose entries declare no decisions, which is why bullet weight narrows despite measuring a distance like the two that do not. 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. 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`.
|
|
405
416
|
|
|
406
417
|
Index drift compares an index against its siblings in both directions. An entry the index does not link is invisible to a session choosing what to open, and a linked name resolving to nothing sends one to a path that opens nothing.
|
|
407
418
|
|
|
@@ -413,6 +424,8 @@ Three exclusions keep it from firing on prose about paths. Fenced blocks are ski
|
|
|
413
424
|
|
|
414
425
|
What remains is a sentence naming a hypothetical entry to show the shape of a name, which no syntax separates from a real reference. Append `<!-- audit-ignore-citations -->` to that source line. The marker suppresses citation checking for its own line only.
|
|
415
426
|
|
|
427
|
+
The pattern spells the `.claude/` prefix, so a folder resolved at the project root is measured by every other check and contributes nothing here. Widening it to a bare `docs/x.md` would match prose that references nothing, which is a separate decision from where entries come from. A run whose folders all resolved at the root says the check is out of scope rather than reporting that zero paths resolved, and the same run under `--citations-only` refuses, because a gate exiting clean on a scope it could not build is the failure the gate exists to catch.
|
|
428
|
+
|
|
416
429
|
## Runtime catalogs
|
|
417
430
|
|
|
418
431
|
Use these to discover what's available instead of hardcoding names.
|
package/docs/operating-model.md
CHANGED
|
@@ -104,4 +104,4 @@ port-collision detail.
|
|
|
104
104
|
|
|
105
105
|
- [Claude Code and git worktrees](../wiki/claude-worktrees.md) for the isolation and fan-out mechanics
|
|
106
106
|
- [Claude Code subagents](../wiki/claude-subagents.md) for in-session parallelism without worktrees
|
|
107
|
-
- `.claude/context/claude-plugin/
|
|
107
|
+
- `.claude/context/claude-plugin/skill-strategy.md` for how the skills in the loop are categorized
|
|
@@ -130,7 +130,7 @@ Zero or one yes: tier 0. Two or three: tier 1. Four: tier 2. Resist over-tiering
|
|
|
130
130
|
|
|
131
131
|
- [Stitch](../wiki/stitch.md): Gemini-powered design product with the MCP server that anchors tier 1
|
|
132
132
|
- [Claude Design](../wiki/claude-design.md): first-party hosted design product and handoff bundle
|
|
133
|
-
- `.claude/context/claude-plugin/
|
|
133
|
+
- `.claude/context/claude-plugin/skill-strategy.md`: how to decide between workflow and domain-knowledge skills
|
|
134
134
|
- [Visual wireframes](../wiki/visual-wireframes.md): Excalidraw research and setup for the tier 1 wireframe companion
|
|
135
135
|
- [Community skills and plugins](../wiki/community-skills.md): catalog of frontend design skills and integrations
|
|
136
136
|
- [Community MCP servers](../wiki/community-mcp-servers.md): catalog of MCPs referenced across all tiers
|
package/package.json
CHANGED
package/src/commands/context.ts
CHANGED
|
@@ -6,10 +6,13 @@ import {
|
|
|
6
6
|
governsContent,
|
|
7
7
|
LENGTH_CHECKPOINT,
|
|
8
8
|
measureFolders,
|
|
9
|
+
missingSections,
|
|
9
10
|
PEER_BULLET_CHECKPOINT,
|
|
10
11
|
PROVENANCE_FOLDER,
|
|
11
12
|
RENDER_WIDTH,
|
|
13
|
+
REQUIRED_SECTIONS,
|
|
12
14
|
RUN_CHECKPOINT,
|
|
15
|
+
type SectionFinding,
|
|
13
16
|
} from '@/context/audit'
|
|
14
17
|
import { auditCitations, type CitationReport } from '@/context/citations'
|
|
15
18
|
import {
|
|
@@ -51,12 +54,15 @@ export function register(program: Command): void {
|
|
|
51
54
|
context
|
|
52
55
|
.command('audit')
|
|
53
56
|
.description(
|
|
54
|
-
'Report entry length, depth, bullet weight, citations, provenance, and index drift',
|
|
57
|
+
'Report required sections, entry length, depth, bullet weight, citations, provenance, and index drift',
|
|
55
58
|
)
|
|
56
59
|
.argument('[path]', 'Project root, defaulting to the current directory')
|
|
57
60
|
.helpOption('-h, --help', 'Show this help message')
|
|
58
61
|
.option('--json', 'Add a machine-readable record on stdout')
|
|
59
|
-
.option(
|
|
62
|
+
.option(
|
|
63
|
+
'--folder <list>',
|
|
64
|
+
'Comma-separated folder names, resolved under .claude/ then the project root',
|
|
65
|
+
)
|
|
60
66
|
.option('--citations-only', 'Run the gating citation check alone')
|
|
61
67
|
.addHelpText(
|
|
62
68
|
'after',
|
|
@@ -67,14 +73,15 @@ export function register(program: Command): void {
|
|
|
67
73
|
' 1 refused, with the reason on stderr',
|
|
68
74
|
' 2 a cited path did not resolve',
|
|
69
75
|
'',
|
|
70
|
-
'Only unresolved citations set a failing exit code.
|
|
71
|
-
'bullet, table, provenance, and index findings are advisory.',
|
|
76
|
+
'Only unresolved citations set a failing exit code. Section, length,',
|
|
77
|
+
'depth, bullet, table, provenance, and index findings are advisory.',
|
|
72
78
|
'',
|
|
73
79
|
'Examples:',
|
|
74
80
|
' aitk context audit',
|
|
75
81
|
' aitk context audit --json',
|
|
76
82
|
' aitk context audit --citations-only',
|
|
77
83
|
' aitk context audit --folder context,diagrams',
|
|
84
|
+
' aitk context audit --folder docs',
|
|
78
85
|
'',
|
|
79
86
|
].join('\n'),
|
|
80
87
|
)
|
|
@@ -93,11 +100,11 @@ function parseFolders(list: string | undefined): string[] | string {
|
|
|
93
100
|
|
|
94
101
|
if (names.length === 0) return 'Empty --folder list. Pass at least one name.'
|
|
95
102
|
|
|
96
|
-
// `..` would resolve the audited folder above
|
|
97
|
-
// and the citation pattern outside the tree the audit describes.
|
|
103
|
+
// `..` would resolve the audited folder above the project root, taking the
|
|
104
|
+
// scan and the citation pattern outside the tree the audit describes.
|
|
98
105
|
const invalid = names.filter((name) => !FOLDER_NAME.test(name))
|
|
99
106
|
if (invalid.length > 0) {
|
|
100
|
-
return `--folder takes folder names
|
|
107
|
+
return `--folder takes folder names, not paths: ${invalid.join(', ')}`
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
return names
|
|
@@ -113,15 +120,38 @@ async function runAudit(
|
|
|
113
120
|
|
|
114
121
|
if (typeof names === 'string') return refuse(names, gateOnly)
|
|
115
122
|
|
|
116
|
-
|
|
123
|
+
// The root base is opt-in. A target carrying a root `wireframes/` would
|
|
124
|
+
// otherwise be audited against a standard it never adopted, on a bare run
|
|
125
|
+
// that named nothing.
|
|
126
|
+
const named = opts.folder !== undefined
|
|
127
|
+
const { folders, missing } = await resolveFolders(root, names, {
|
|
128
|
+
canResolveAtRoot: named,
|
|
129
|
+
})
|
|
117
130
|
if (folders.length === 0) {
|
|
118
131
|
return refuse(
|
|
119
|
-
`No audited folder found under .claude
|
|
132
|
+
`No audited folder found ${named ? 'under .claude/ or the project root' : 'under .claude/'}. Looked for: ${names.join(', ')}.`,
|
|
120
133
|
gateOnly,
|
|
121
134
|
)
|
|
122
135
|
}
|
|
123
136
|
|
|
124
|
-
|
|
137
|
+
// A default folder a project does not carry is the ordinary case and stays
|
|
138
|
+
// silent. A name passed by hand that resolves nowhere is a typo, and the run
|
|
139
|
+
// measuring the names that did resolve reads as a pass against a folder it
|
|
140
|
+
// never opened.
|
|
141
|
+
const unresolved = named ? missing : []
|
|
142
|
+
|
|
143
|
+
// The gate runs one check. Letting it exit 0 against a scope it could not
|
|
144
|
+
// build reports a pass on nothing measured, which is the outcome a gate is
|
|
145
|
+
// there to prevent.
|
|
146
|
+
const cited = presentNames(folders)
|
|
147
|
+
if (gateOnly && cited.length === 0) {
|
|
148
|
+
return refuse(
|
|
149
|
+
`The citation check spells the .claude/ prefix and no audited folder resolved there. Looked for: ${names.join(', ')}.`,
|
|
150
|
+
gateOnly,
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const citations = await auditCitations(root, cited)
|
|
125
155
|
if (citations.kind === 'unavailable') {
|
|
126
156
|
return refuse(
|
|
127
157
|
'git could not list the tree, so no citation was checked. Run inside a git repository.',
|
|
@@ -131,13 +161,15 @@ async function runAudit(
|
|
|
131
161
|
|
|
132
162
|
const entries = gateOnly ? [] : await measureFolders(root, folders)
|
|
133
163
|
const drift = gateOnly ? [] : await auditIndexes(folders)
|
|
164
|
+
const sections = gateOnly ? [] : missingSections(root, folders, entries)
|
|
134
165
|
|
|
135
166
|
if (gateOnly) {
|
|
136
167
|
reportGate(citations)
|
|
137
168
|
} else {
|
|
138
169
|
intro('aitk context audit')
|
|
139
|
-
reportScope(folders)
|
|
140
|
-
reportCitations(citations)
|
|
170
|
+
reportScope(folders, unresolved)
|
|
171
|
+
reportCitations(citations, cited)
|
|
172
|
+
reportSections(sections, folders)
|
|
141
173
|
reportLength(entries)
|
|
142
174
|
reportDepth(entries)
|
|
143
175
|
reportBullets(entries, folders)
|
|
@@ -153,15 +185,18 @@ async function runAudit(
|
|
|
153
185
|
root,
|
|
154
186
|
folders: folders.map((folder) => ({
|
|
155
187
|
path: folder.rel,
|
|
188
|
+
base: folder.base,
|
|
156
189
|
entries: folder.entries.length,
|
|
157
190
|
governsContent: governsContent(folder),
|
|
158
191
|
})),
|
|
192
|
+
unresolvedFolders: unresolved,
|
|
159
193
|
citations: {
|
|
160
194
|
scanned: citations.scanned,
|
|
161
195
|
total: citations.total,
|
|
162
196
|
unresolved: citations.unresolved,
|
|
163
197
|
},
|
|
164
198
|
entries,
|
|
199
|
+
missingSections: sections,
|
|
165
200
|
indexDrift: drift,
|
|
166
201
|
checkpoints: {
|
|
167
202
|
lines: LENGTH_CHECKPOINT,
|
|
@@ -171,6 +206,7 @@ async function runAudit(
|
|
|
171
206
|
peerBullet: PEER_BULLET_CHECKPOINT,
|
|
172
207
|
bullet: BULLET_CHECKPOINT,
|
|
173
208
|
provenanceFolder: PROVENANCE_FOLDER,
|
|
209
|
+
requiredSections: REQUIRED_SECTIONS,
|
|
174
210
|
},
|
|
175
211
|
})}\n`,
|
|
176
212
|
)
|
|
@@ -221,18 +257,54 @@ function plural(count: number, noun: string): string {
|
|
|
221
257
|
return `${count} ${noun}${count === 1 ? '' : 's'}`
|
|
222
258
|
}
|
|
223
259
|
|
|
224
|
-
|
|
260
|
+
/**
|
|
261
|
+
* Names the resolved path of every audited folder, plus the requested names
|
|
262
|
+
* that resolved nowhere.
|
|
263
|
+
*
|
|
264
|
+
* The path is what says which base a name was taken from, which matters once a
|
|
265
|
+
* name can resolve under `.claude/` or at the project root and a project may
|
|
266
|
+
* carry both.
|
|
267
|
+
*/
|
|
268
|
+
function reportScope(
|
|
269
|
+
folders: readonly AuditedFolder[],
|
|
270
|
+
unresolved: readonly string[],
|
|
271
|
+
): void {
|
|
225
272
|
logStep('Scope')
|
|
226
273
|
|
|
227
274
|
for (const folder of folders) {
|
|
228
275
|
logInfo(`${folder.rel}: ${folder.entries.length} entries`)
|
|
229
276
|
}
|
|
277
|
+
|
|
278
|
+
if (unresolved.length === 0) return
|
|
279
|
+
|
|
280
|
+
logWarn(
|
|
281
|
+
`Under neither .claude/ nor the project root: ${unresolved.join(', ')}`,
|
|
282
|
+
)
|
|
230
283
|
}
|
|
231
284
|
|
|
232
285
|
type ScannedCitations = Extract<CitationReport, { kind: 'scanned' }>
|
|
233
286
|
|
|
234
|
-
|
|
287
|
+
/**
|
|
288
|
+
* States the reach before the count, for the reason the provenance report
|
|
289
|
+
* states its own.
|
|
290
|
+
*
|
|
291
|
+
* A run auditing a folder at the project root builds no pattern, and a count of
|
|
292
|
+
* zero followed by a line saying every path resolves is indistinguishable from
|
|
293
|
+
* a corpus that cites nothing.
|
|
294
|
+
*/
|
|
295
|
+
function reportCitations(
|
|
296
|
+
report: ScannedCitations,
|
|
297
|
+
cited: readonly string[],
|
|
298
|
+
): void {
|
|
235
299
|
logStep('Citations')
|
|
300
|
+
|
|
301
|
+
if (cited.length === 0) {
|
|
302
|
+
logInfo(
|
|
303
|
+
'Out of scope. The pattern spells the .claude/ prefix, and no audited folder resolved there.',
|
|
304
|
+
)
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
236
308
|
logInfo(
|
|
237
309
|
`${plural(report.total, 'cited path')} across ${plural(report.scanned, 'file')}, fixtures and fenced examples excluded`,
|
|
238
310
|
)
|
|
@@ -250,6 +322,49 @@ function reportCitations(report: ScannedCitations): void {
|
|
|
250
322
|
)
|
|
251
323
|
}
|
|
252
324
|
|
|
325
|
+
/**
|
|
326
|
+
* Names the path each finding belongs to, which is an entry in the folder named
|
|
327
|
+
* under `.claude/` and the folder itself in a domain split across one. States
|
|
328
|
+
* the reach on every run for the reason the provenance report does.
|
|
329
|
+
*
|
|
330
|
+
* This prints ahead of the four readability measures because a missing section
|
|
331
|
+
* asks whether the entry is the right shape at all, which precedes asking
|
|
332
|
+
* whether it has grown too long.
|
|
333
|
+
*/
|
|
334
|
+
function reportSections(
|
|
335
|
+
missing: readonly SectionFinding[],
|
|
336
|
+
folders: readonly AuditedFolder[],
|
|
337
|
+
): void {
|
|
338
|
+
logStep('Sections')
|
|
339
|
+
|
|
340
|
+
const governed = folders.filter(governsContent)
|
|
341
|
+
if (governed.length === 0) {
|
|
342
|
+
logInfo(
|
|
343
|
+
`Out of scope. The list is stated in the standard governing .claude/${PROVENANCE_FOLDER}/, and no audited folder is that one.`,
|
|
344
|
+
)
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
logInfo(
|
|
349
|
+
`Covers .claude/${PROVENANCE_FOLDER}/ alone, whose standard requires ${REQUIRED_SECTIONS.join(' and ')}.`,
|
|
350
|
+
)
|
|
351
|
+
logInfo(
|
|
352
|
+
'A heading at any level counts. Each entry answers for itself, except in a domain split across a folder, where a sibling answers for the rest.',
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
if (missing.length === 0) {
|
|
356
|
+
logInfo('Every entry declares each required section.')
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
logWarn(`${plural(missing.length, 'path')} short a required section`)
|
|
361
|
+
pipeOutput(
|
|
362
|
+
missing
|
|
363
|
+
.map((found) => `${found.rel} missing: ${found.missing.join(', ')}`)
|
|
364
|
+
.join('\n'),
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
|
|
253
368
|
function reportLength(entries: readonly EntryReport[]): void {
|
|
254
369
|
logStep('Length')
|
|
255
370
|
logInfo(
|
package/src/context/audit.ts
CHANGED
|
@@ -50,9 +50,25 @@ export const CATALOG_ROW_CHECKPOINT = 6
|
|
|
50
50
|
/** Share of first cells that must name an artifact for a table to qualify. */
|
|
51
51
|
const CATALOG_NAMED_RATIO = 0.6
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Sections `standards/context.md` marks required, in the order it states them.
|
|
55
|
+
*
|
|
56
|
+
* The list is held here rather than read out of the standard, the way the four
|
|
57
|
+
* checkpoints above quote their numbers. A parser over the standard's prose
|
|
58
|
+
* would decide which sections are required from the wording around them, so it
|
|
59
|
+
* fails on a rewrite of that wording rather than on a defect in an entry.
|
|
60
|
+
*
|
|
61
|
+
* These names do not generalize the way a length threshold does, which is why
|
|
62
|
+
* the measure is scoped to the folder `governsContent` names. A diagram entry
|
|
63
|
+
* declares a heading per kind and a wireframe entry per screen, and neither
|
|
64
|
+
* sibling standard states a required section at all.
|
|
65
|
+
*/
|
|
66
|
+
export const REQUIRED_SECTIONS: readonly string[] = ['Overview', 'Layout']
|
|
67
|
+
|
|
53
68
|
const FRONTMATTER = /^---\n[\s\S]*?\n---\n?/
|
|
54
69
|
const FENCE = /^\s*(```|~~~)/
|
|
55
70
|
const HEADING = /^#{1,6}\s/
|
|
71
|
+
const HEADING_TEXT = /^#{1,6}\s+(.+?)\s*$/
|
|
56
72
|
const LIST_ITEM = /^(\s*)([-*+]|\d+\.)\s+/
|
|
57
73
|
const TABLE_ROW = /^\s*\|/
|
|
58
74
|
const TABLE_SEPARATOR = /^\s*\|[\s:|-]+\|\s*$/
|
|
@@ -144,6 +160,23 @@ export interface EntryReport {
|
|
|
144
160
|
readonly provenance: readonly ProvenanceFinding[]
|
|
145
161
|
/** Empty for the same reason `provenance` is, and under the same folder. */
|
|
146
162
|
readonly heavyBullets: readonly BulletFinding[]
|
|
163
|
+
/**
|
|
164
|
+
* Required sections this entry declares, in the standard's order, and empty
|
|
165
|
+
* outside the folder whose standard names them. What the folder is short of
|
|
166
|
+
* is `missingSections`, since one entry answers for its siblings.
|
|
167
|
+
*/
|
|
168
|
+
readonly sections: readonly string[]
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface SectionFinding {
|
|
172
|
+
/**
|
|
173
|
+
* Repo-relative path of whatever owes the sections: the entry itself in the
|
|
174
|
+
* folder named under `.claude/`, and the folder in a domain split across
|
|
175
|
+
* one, since the split folder's entries answer for each other.
|
|
176
|
+
*/
|
|
177
|
+
readonly rel: string
|
|
178
|
+
/** Required sections the path above does not declare, never empty. */
|
|
179
|
+
readonly missing: readonly string[]
|
|
147
180
|
}
|
|
148
181
|
|
|
149
182
|
interface BodyLine {
|
|
@@ -422,6 +455,38 @@ function provenance(lines: readonly BodyLine[]): ProvenanceFinding[] {
|
|
|
422
455
|
.map((each) => each.finding)
|
|
423
456
|
}
|
|
424
457
|
|
|
458
|
+
/**
|
|
459
|
+
* Finds which required sections the entry declares.
|
|
460
|
+
*
|
|
461
|
+
* A heading at any level counts rather than the `##` the standard writes its
|
|
462
|
+
* examples at. A domain that split into a folder puts its overview in a sibling
|
|
463
|
+
* named for it, where the section is the `#` title and an `##` beneath it would
|
|
464
|
+
* repeat the file's own name. All three split folders in this repository are
|
|
465
|
+
* that shape, so matching `##` alone would report every one of them. Nothing is
|
|
466
|
+
* titled for a required section without being about it, so the looser match
|
|
467
|
+
* costs no precision.
|
|
468
|
+
*
|
|
469
|
+
* Fenced blocks are skipped for the reason the scans above skip them. A
|
|
470
|
+
* standard quoted inside an example declares nothing about the entry quoting it.
|
|
471
|
+
*/
|
|
472
|
+
function declaredSections(lines: readonly BodyLine[]): string[] {
|
|
473
|
+
const found = new Set<string>()
|
|
474
|
+
let fenced = false
|
|
475
|
+
|
|
476
|
+
for (const line of lines) {
|
|
477
|
+
if (FENCE.test(line.text)) {
|
|
478
|
+
fenced = !fenced
|
|
479
|
+
continue
|
|
480
|
+
}
|
|
481
|
+
if (fenced) continue
|
|
482
|
+
|
|
483
|
+
const match = line.text.match(HEADING_TEXT)
|
|
484
|
+
if (match && REQUIRED_SECTIONS.includes(match[1])) found.add(match[1])
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return REQUIRED_SECTIONS.filter((section) => found.has(section))
|
|
488
|
+
}
|
|
489
|
+
|
|
425
490
|
/**
|
|
426
491
|
* Measures one entry, scanning for provenance only when a standard claims it.
|
|
427
492
|
*
|
|
@@ -448,6 +513,7 @@ export function measureEntry(
|
|
|
448
513
|
catalogTables: catalogTables(lines),
|
|
449
514
|
provenance: governsContent ? provenance(lines) : [],
|
|
450
515
|
heavyBullets: governsContent ? heavyBullets(lines) : [],
|
|
516
|
+
sections: governsContent ? declaredSections(lines) : [],
|
|
451
517
|
}
|
|
452
518
|
}
|
|
453
519
|
|
|
@@ -484,3 +550,56 @@ export async function measureFolders(
|
|
|
484
550
|
export function governsContent(folder: AuditedFolder): boolean {
|
|
485
551
|
return folder.name === PROVENANCE_FOLDER
|
|
486
552
|
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Names what does not declare the sections the standard requires.
|
|
556
|
+
*
|
|
557
|
+
* Which unit answers depends on what the folder is. A split folder's entries
|
|
558
|
+
* describe one domain between them and carry the overview and the layout in a
|
|
559
|
+
* sibling named for them, so any one of them answers and a per-file rule there
|
|
560
|
+
* would report every other child of all three shipped splits. The entries of
|
|
561
|
+
* the folder named under `.claude/` are one domain each, so each answers for
|
|
562
|
+
* itself. Rolling those up too was the first shape of this check, and it let a
|
|
563
|
+
* single sibling stand in for thirteen domains it says nothing about.
|
|
564
|
+
*
|
|
565
|
+
* The judgment sits in the caller because the split case needs the folder's
|
|
566
|
+
* other entries, which `measureFolders` holds and `measureEntry` does not. A
|
|
567
|
+
* folder with no entries of its own is a split parent holding an index and
|
|
568
|
+
* subfolders, and it has nothing to require a section of.
|
|
569
|
+
*
|
|
570
|
+
* The standard sanctions omitting `## Layout` from a domain owning no paths,
|
|
571
|
+
* which no measure can tell from an entry that forgot it. An entry of that
|
|
572
|
+
* shape therefore reports, which is a reason this is printed and never gated on.
|
|
573
|
+
*/
|
|
574
|
+
export function missingSections(
|
|
575
|
+
root: string,
|
|
576
|
+
folders: readonly AuditedFolder[],
|
|
577
|
+
entries: readonly EntryReport[],
|
|
578
|
+
): SectionFinding[] {
|
|
579
|
+
const byRel = new Map(entries.map((entry) => [entry.rel, entry]))
|
|
580
|
+
const findings: SectionFinding[] = []
|
|
581
|
+
|
|
582
|
+
const shortOf = (declared: readonly string[]): string[] =>
|
|
583
|
+
REQUIRED_SECTIONS.filter((name) => !declared.includes(name))
|
|
584
|
+
|
|
585
|
+
for (const folder of folders) {
|
|
586
|
+
if (!governsContent(folder) || folder.entries.length === 0) continue
|
|
587
|
+
|
|
588
|
+
const reports = folder.entries
|
|
589
|
+
.map((path) => byRel.get(relative(root, path)))
|
|
590
|
+
.filter((entry) => entry !== undefined)
|
|
591
|
+
|
|
592
|
+
if (folder.nested) {
|
|
593
|
+
const missing = shortOf(reports.flatMap((entry) => entry.sections))
|
|
594
|
+
if (missing.length > 0) findings.push({ rel: folder.rel, missing })
|
|
595
|
+
continue
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
for (const entry of reports) {
|
|
599
|
+
const missing = shortOf(entry.sections)
|
|
600
|
+
if (missing.length > 0) findings.push({ rel: entry.rel, missing })
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return findings
|
|
605
|
+
}
|