@erclx/canon 5.62.0 → 5.64.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.
Files changed (101) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/api-design/references/adopted.md +1 -1
  4. package/claude/skills/context-fold/REQUIREMENT.md +2 -3
  5. package/claude/skills/context-fold/SKILL.md +1 -11
  6. package/claude/skills/context-fold/references/completion-report.md +8 -9
  7. package/claude/skills/create-skill/references/skill-practice.md +17 -1
  8. package/claude/skills/design-record/references/design.md +1 -1
  9. package/claude/skills/design-taste/REQUIREMENT.md +2 -1
  10. package/claude/skills/design-taste/SKILL.md +6 -5
  11. package/claude/skills/design-taste/references/adopted.md +56 -8
  12. package/claude/skills/design-taste/references/craft.md +1 -1
  13. package/claude/skills/design-taste/references/kinds.md +8 -2
  14. package/claude/skills/design-taste/references/preflight.md +3 -3
  15. package/claude/skills/design-taste/references/tells.md +26 -5
  16. package/claude/skills/document-health/REQUIREMENT.md +1 -1
  17. package/claude/skills/document-health/SKILL.md +3 -14
  18. package/claude/skills/document-health/references/axes.md +14 -16
  19. package/claude/skills/plan-feature/SKILL.md +2 -2
  20. package/claude/skills/plan-feature/references/plan.md +24 -3
  21. package/claude/skills/review-craft/SKILL.md +1 -1
  22. package/claude/skills/role-worker/SKILL.md +2 -1
  23. package/claude/skills/ui-checklist/SKILL.md +1 -1
  24. package/claude/skills/ui-craft/REQUIREMENT.md +29 -0
  25. package/claude/skills/ui-craft/SKILL.md +44 -0
  26. package/{governance/rules/ui → claude/skills/ui-craft/references}/a11y.md +0 -10
  27. package/claude/skills/ui-craft/references/adopted.md +27 -0
  28. package/{governance/rules/ui/surface-capture.md → claude/skills/ui-craft/references/capture.md} +0 -10
  29. package/{governance/rules/ui/ux-controls.md → claude/skills/ui-craft/references/controls.md} +3 -10
  30. package/claude/skills/ui-craft/references/copy.md +45 -0
  31. package/{governance/rules/ui → claude/skills/ui-craft/references}/forms.md +0 -9
  32. package/{governance/rules/ui → claude/skills/ui-craft/references}/keyboard.md +0 -10
  33. package/{governance/rules/ui → claude/skills/ui-craft/references}/motion.md +0 -10
  34. package/{governance/rules/ui → claude/skills/ui-craft/references}/pointer.md +0 -10
  35. package/{governance/rules/ui/ux-completeness.md → claude/skills/ui-craft/references/states.md} +0 -10
  36. package/claude/skills/ux-audit/SKILL.md +1 -1
  37. package/claude/skills/write-human/references/adopted.md +4 -1
  38. package/docs/agents/audits.md +2 -6
  39. package/docs/agents/context-audit.md +1 -1
  40. package/docs/agents/install-and-sync.md +2 -2
  41. package/docs/target/migrations.md +3 -3
  42. package/docs/target/sync.md +3 -2
  43. package/docs/workflow/skill-map.md +1 -0
  44. package/docs/workflow/visual-design-workflow.md +2 -2
  45. package/governance/detect.toml +1 -45
  46. package/governance/ledger/lib/test-scope.md +1 -1
  47. package/governance/ledger/skills/api-design/SKILL.md +62 -0
  48. package/governance/ledger/skills/code-craft/SKILL.md +53 -0
  49. package/governance/ledger/skills/deprecation-migration/SKILL.md +68 -0
  50. package/governance/ledger/skills/design-taste/SKILL.md +64 -0
  51. package/governance/ledger/skills/design-taste/craft.md +66 -0
  52. package/governance/ledger/skills/design-taste/kinds.md +58 -0
  53. package/governance/ledger/skills/design-taste/systems.md +42 -0
  54. package/governance/ledger/skills/design-taste/tells.md +103 -0
  55. package/governance/ledger/{ui → skills/ui-craft}/a11y.md +3 -3
  56. package/governance/ledger/{ui/surface-capture.md → skills/ui-craft/capture.md} +3 -3
  57. package/governance/ledger/{ui/ux-controls.md → skills/ui-craft/controls.md} +12 -7
  58. package/governance/ledger/skills/ui-craft/copy.md +102 -0
  59. package/governance/ledger/{ui → skills/ui-craft}/forms.md +3 -3
  60. package/governance/ledger/{ui → skills/ui-craft}/keyboard.md +3 -3
  61. package/governance/ledger/{ui → skills/ui-craft}/motion.md +3 -3
  62. package/governance/ledger/{ui → skills/ui-craft}/pointer.md +3 -3
  63. package/governance/ledger/{ui/ux-completeness.md → skills/ui-craft/states.md} +3 -3
  64. package/governance/ledger/ui/craft.md +19 -0
  65. package/governance/renames.toml +17 -3
  66. package/governance/rules/lib/test-scope.md +1 -1
  67. package/governance/rules/ui/craft.md +22 -0
  68. package/governance/splits.toml +0 -2
  69. package/package.json +1 -1
  70. package/src/audits/catalog.ts +0 -52
  71. package/src/claude/cases/misc.ts +5 -0
  72. package/src/claude/skills-audit.ts +226 -12
  73. package/src/cli/retired.ts +8 -0
  74. package/src/commands/claude.ts +15 -1
  75. package/src/commands/context.ts +0 -425
  76. package/src/commands/tooling.ts +4 -1
  77. package/src/context/citations.ts +9 -3
  78. package/src/gate/measures.ts +4 -3
  79. package/src/git/files.ts +8 -3
  80. package/src/gov/audit.ts +6 -1
  81. package/src/inventory/subjects.ts +1 -1
  82. package/src/markdown/gated.ts +1 -1
  83. package/src/markdown/scope.ts +0 -1
  84. package/src/tooling/package.ts +22 -13
  85. package/tooling/web/reference.md +3 -1
  86. package/claude/skills/context-fold/references/classify.md +0 -87
  87. package/governance/ledger/ui/copy.md +0 -49
  88. package/governance/ledger/ui/design-taste.md +0 -19
  89. package/governance/ledger/ui/interface-casing.md +0 -47
  90. package/governance/ledger/ui/link-behavior.md +0 -29
  91. package/governance/rules/ui/copy.md +0 -28
  92. package/governance/rules/ui/design-taste.md +0 -18
  93. package/governance/rules/ui/interface-casing.md +0 -29
  94. package/governance/rules/ui/link-behavior.md +0 -19
  95. package/src/context/classify/extract.ts +0 -443
  96. package/src/context/classify/ollama.ts +0 -172
  97. package/src/context/classify/patterns.ts +0 -75
  98. package/src/context/classify/prompts.ts +0 -74
  99. package/src/context/classify/run.ts +0 -348
  100. package/src/context/classify/settings.ts +0 -196
  101. /package/tooling/web/{configs → seeds}/e2e/home.spec.ts +0 -0
@@ -30,26 +30,6 @@ import {
30
30
  type ShapeIssue,
31
31
  } from '@/context/audit'
32
32
  import { auditCitations, type CitationReport } from '@/context/citations'
33
- import {
34
- CANONICAL_DOC_TYPES,
35
- type CanonicalDocType,
36
- } from '@/context/classify/extract'
37
- import {
38
- classifyDiff,
39
- classifySweep,
40
- type ClassifyOutcome,
41
- type ClassifyRefusal,
42
- type DiffFinding,
43
- type DiffRecord,
44
- type SweepFinding,
45
- type SweepRecord,
46
- } from '@/context/classify/run'
47
- import {
48
- CLASSIFIER_CONFIG_REL,
49
- type ClassifierBackend,
50
- resolveClassifier,
51
- writeClassifierConfig,
52
- } from '@/context/classify/settings'
53
33
  import {
54
34
  type AuditedFolder,
55
35
  DEFAULT_FOLDERS,
@@ -159,411 +139,6 @@ export function register(program: Command): void {
159
139
  .action(async (path: string | undefined, opts: AuditCommandOptions) => {
160
140
  process.exitCode = await runAudit(path, opts)
161
141
  })
162
-
163
- const classify = context
164
- .command('classify')
165
- .description(
166
- 'Classify canonical-doc content as keep, replace/rewrite, history, or move',
167
- )
168
- .helpOption('-h, --help', 'Show this help message')
169
-
170
- classify
171
- .command('diff')
172
- .description(
173
- 'Classify the chunks a git range changed, each with the section it landed in',
174
- )
175
- .argument('[path]', 'Project root, defaulting to the current directory')
176
- .helpOption('-h, --help', 'Show this help message')
177
- .option('--base <ref>', 'Far side of the range, defaulting to the trunk')
178
- .option(
179
- '--doc-types <list>',
180
- `Comma-separated canonical doc types (default: all three: ${CANONICAL_DOC_TYPES.join(', ')})`,
181
- )
182
- .option(
183
- '--backend <name>',
184
- 'Override the resolved model backend for this run',
185
- )
186
- .option('--model <name>', 'Override the resolved model name for this run')
187
- .option('--json', 'Add a machine-readable record on stdout')
188
- .addHelpText(
189
- 'after',
190
- [
191
- '',
192
- 'The regex layer always runs. The model layer runs only when a',
193
- 'backend resolves through `canon context classifier show`, and a',
194
- 'configured-but-unreachable backend warns and falls back to regex',
195
- 'rather than failing the run.',
196
- '',
197
- 'Exit codes:',
198
- ' 0 the run completed, whatever the findings say',
199
- ' 1 refused: a bad range or a file this could not read',
200
- '',
201
- 'Examples:',
202
- ' canon context classify diff',
203
- ' canon context classify diff --base origin/main --json',
204
- ' canon context classify diff --doc-types context',
205
- '',
206
- ].join('\n'),
207
- )
208
- .action(async (path: string | undefined, opts: ClassifyDiffOptions) => {
209
- process.exitCode = await runClassifyDiff(path, opts)
210
- })
211
-
212
- classify
213
- .command('sweep')
214
- .description(
215
- 'Classify every section of the three canonical doc types, split at H3',
216
- )
217
- .argument('[path]', 'Project root, defaulting to the current directory')
218
- .helpOption('-h, --help', 'Show this help message')
219
- .option(
220
- '--doc-types <list>',
221
- `Comma-separated canonical doc types (default: all three: ${CANONICAL_DOC_TYPES.join(', ')})`,
222
- )
223
- .option(
224
- '--backend <name>',
225
- 'Override the resolved model backend for this run',
226
- )
227
- .option('--model <name>', 'Override the resolved model name for this run')
228
- .option('--json', 'Add a machine-readable record on stdout')
229
- .addHelpText(
230
- 'after',
231
- [
232
- '',
233
- 'Exit codes:',
234
- ' 0 the run completed, whatever the findings say',
235
- ' 1 refused: a file this could not read',
236
- '',
237
- 'Examples:',
238
- ' canon context classify sweep',
239
- ' canon context classify sweep --doc-types design,requirements --json',
240
- '',
241
- ].join('\n'),
242
- )
243
- .action(async (path: string | undefined, opts: ClassifySweepOptions) => {
244
- process.exitCode = await runClassifySweep(path, opts)
245
- })
246
-
247
- const classifier = context
248
- .command('classifier')
249
- .description('Read or write the project classifier setting')
250
- .helpOption('-h, --help', 'Show this help message')
251
-
252
- classifier
253
- .command('show')
254
- .description(
255
- 'Report the backend and model that would run, and which source decided it',
256
- )
257
- .argument('[path]', 'Project root, defaulting to the current directory')
258
- .helpOption('-h, --help', 'Show this help message')
259
- .option('--json', 'Add a machine-readable record on stdout')
260
- .action((path: string | undefined, opts: { json?: boolean }) => {
261
- runClassifierShow(path, opts)
262
- })
263
-
264
- classifier
265
- .command('set')
266
- .description('Write the project classifier setting')
267
- .argument('[path]', 'Project root, defaulting to the current directory')
268
- .helpOption('-h, --help', 'Show this help message')
269
- .requiredOption('--backend <name>', 'ollama or off')
270
- .option('--model <name>', 'Model name, required when --backend is ollama')
271
- .option('--json', 'Add a machine-readable record on stdout')
272
- .action((path: string | undefined, opts: ClassifierSetOptions) => {
273
- process.exitCode = runClassifierSet(path, opts)
274
- })
275
- }
276
-
277
- interface ClassifyDiffOptions {
278
- readonly json?: boolean
279
- readonly base?: string
280
- readonly docTypes?: string
281
- readonly backend?: string
282
- readonly model?: string
283
- }
284
-
285
- interface ClassifySweepOptions {
286
- readonly json?: boolean
287
- readonly docTypes?: string
288
- readonly backend?: string
289
- readonly model?: string
290
- }
291
-
292
- interface ClassifierSetOptions {
293
- readonly json?: boolean
294
- readonly backend: string
295
- readonly model?: string
296
- }
297
-
298
- function parseDocTypes(
299
- list: string | undefined,
300
- ): readonly CanonicalDocType[] | string {
301
- if (!list) return CANONICAL_DOC_TYPES
302
-
303
- const names = list
304
- .split(',')
305
- .map((name) => name.trim())
306
- .filter(Boolean)
307
-
308
- const invalid = names.filter(
309
- (name) => !CANONICAL_DOC_TYPES.includes(name as CanonicalDocType),
310
- )
311
- if (invalid.length > 0) {
312
- return `--doc-types takes ${CANONICAL_DOC_TYPES.join(', ')}: ${invalid.join(', ')}`
313
- }
314
-
315
- return names as CanonicalDocType[]
316
- }
317
-
318
- /**
319
- * Validates `--backend` up front, so a typo'd value refuses rather than
320
- * falling through `resolveBackend`'s tier chain to the environment, the
321
- * file, or the default, which reads as the flag having been ignored.
322
- */
323
- function parseBackendFlag(value: string | undefined): string | undefined {
324
- if (value === undefined || value === 'ollama' || value === 'off') {
325
- return undefined
326
- }
327
- return `--backend takes ollama or off: ${value}`
328
- }
329
-
330
- /** Widens the extraction refusals with the CLI's own argument-parsing failure. */
331
- type CliRefusal = ClassifyRefusal | 'bad-flags'
332
-
333
- function refuseClassify(
334
- reason: CliRefusal,
335
- message: string,
336
- emitJson: boolean,
337
- ): number {
338
- intro('canon context classify')
339
- logStep('Refused')
340
- logWarn(message)
341
- outro()
342
-
343
- if (emitJson) {
344
- process.stdout.write(
345
- `${JSON.stringify({ decision: 'refused', reason, message })}\n`,
346
- )
347
- }
348
- return 1
349
- }
350
-
351
- /** How each not-off model-layer state reads in the report. */
352
- const MODEL_LAYER_LABEL: Record<string, string> = {
353
- ran: 'ran',
354
- 'skipped-no-model': 'skipped, no model resolved for the configured backend',
355
- 'skipped-unreachable': 'skipped, the configured backend did not answer',
356
- }
357
-
358
- function reportLayers(record: DiffRecord | SweepRecord): void {
359
- logStep('Layers')
360
- logInfo('regex: ran')
361
-
362
- if (record.modelLayer === 'off') {
363
- logInfo('model: off, no backend configured')
364
- return
365
- }
366
-
367
- logInfo(
368
- `model: ${MODEL_LAYER_LABEL[record.modelLayer]} (${record.backend ?? 'none'}${record.model ? `, ${record.model}` : ''})`,
369
- )
370
- if (record.modelLayer !== 'ran') {
371
- logWarn('Falling back to the regex layer alone for this run.')
372
- }
373
- }
374
-
375
- function reportFindings(
376
- findings: readonly (DiffFinding | SweepFinding)[],
377
- label: (finding: DiffFinding | SweepFinding) => string,
378
- ): void {
379
- logStep('Findings')
380
-
381
- if (findings.length === 0) {
382
- logInfo('Nothing met the extraction floor under the requested doc types.')
383
- return
384
- }
385
-
386
- const flagged = findings.filter((finding) => finding.verdict !== 'KEEP')
387
- logInfo(
388
- `${plural(findings.length, 'item')} read, ${plural(flagged.length, 'flagged')}`,
389
- )
390
-
391
- if (flagged.length === 0) return
392
-
393
- pipeOutput(
394
- flagged
395
- .map((finding) => {
396
- const chosen =
397
- finding.decidedBy === 'model' ? finding.model : finding.regex
398
- return `${label(finding)} ${finding.verdict} (${finding.decidedBy})\n ${chosen?.quote ? `"${chosen.quote}" ` : ''}${chosen?.reason ?? ''}`
399
- })
400
- .join('\n'),
401
- )
402
- }
403
-
404
- async function runClassifyDiff(
405
- path: string | undefined,
406
- opts: ClassifyDiffOptions,
407
- ): Promise<number> {
408
- const root = resolve(path ?? process.cwd())
409
- const emitJson = opts.json ?? false
410
-
411
- const docTypes = parseDocTypes(opts.docTypes)
412
- if (typeof docTypes === 'string') {
413
- return refuseClassify('bad-flags', docTypes, emitJson)
414
- }
415
-
416
- const backendError = parseBackendFlag(opts.backend)
417
- if (backendError !== undefined) {
418
- return refuseClassify('bad-flags', backendError, emitJson)
419
- }
420
-
421
- const result: ClassifyOutcome<DiffRecord> = await classifyDiff(
422
- root,
423
- opts.base,
424
- {
425
- docTypes,
426
- flags: { backend: opts.backend, model: opts.model },
427
- },
428
- )
429
-
430
- if (result.kind === 'refused') {
431
- return refuseClassify(result.reason, result.message, emitJson)
432
- }
433
-
434
- intro('canon context classify diff')
435
- reportLayers(result.record)
436
- reportFindings(result.record.findings, (finding) => finding.file)
437
- outro()
438
-
439
- if (emitJson) {
440
- process.stdout.write(
441
- `${JSON.stringify({ decision: 'ok', ...result.record })}\n`,
442
- )
443
- }
444
-
445
- return 0
446
- }
447
-
448
- async function runClassifySweep(
449
- path: string | undefined,
450
- opts: ClassifySweepOptions,
451
- ): Promise<number> {
452
- const root = resolve(path ?? process.cwd())
453
- const emitJson = opts.json ?? false
454
-
455
- const docTypes = parseDocTypes(opts.docTypes)
456
- if (typeof docTypes === 'string') {
457
- return refuseClassify('bad-flags', docTypes, emitJson)
458
- }
459
-
460
- const backendError = parseBackendFlag(opts.backend)
461
- if (backendError !== undefined) {
462
- return refuseClassify('bad-flags', backendError, emitJson)
463
- }
464
-
465
- const result: ClassifyOutcome<SweepRecord> = await classifySweep(root, {
466
- docTypes,
467
- flags: { backend: opts.backend, model: opts.model },
468
- })
469
-
470
- if (result.kind === 'refused') {
471
- return refuseClassify(result.reason, result.message, emitJson)
472
- }
473
-
474
- intro('canon context classify sweep')
475
- reportLayers(result.record)
476
- reportFindings(
477
- result.record.findings,
478
- (finding) => `${finding.file}:${(finding as SweepFinding).heading}`,
479
- )
480
- outro()
481
-
482
- if (emitJson) {
483
- process.stdout.write(
484
- `${JSON.stringify({ decision: 'ok', ...result.record })}\n`,
485
- )
486
- }
487
-
488
- return 0
489
- }
490
-
491
- function runClassifierShow(
492
- path: string | undefined,
493
- opts: { json?: boolean },
494
- ): void {
495
- const root = resolve(path ?? process.cwd())
496
- const resolution = resolveClassifier(root, {})
497
-
498
- intro('canon context classifier show')
499
-
500
- if (resolution.kind === 'off') {
501
- logInfo(
502
- `Model layer off (source: ${resolution.source}). Regex layer always runs.`,
503
- )
504
- } else if (resolution.kind === 'no-model') {
505
- logWarn(
506
- `Backend ${resolution.backend} configured (source: ${resolution.source}) with no model name. \`classify\` runs regex only.`,
507
- )
508
- } else {
509
- logInfo(
510
- `Backend ${resolution.backend}, model ${resolution.model} (source: ${resolution.source}).`,
511
- )
512
- }
513
-
514
- logInfo(
515
- `Reads ${CLASSIFIER_CONFIG_REL} when neither a flag nor an environment variable decides.`,
516
- )
517
- outro()
518
-
519
- if (opts.json) {
520
- process.stdout.write(`${JSON.stringify(resolution)}\n`)
521
- }
522
- }
523
-
524
- function runClassifierSet(
525
- path: string | undefined,
526
- opts: ClassifierSetOptions,
527
- ): number {
528
- const root = resolve(path ?? process.cwd())
529
- const emitJson = opts.json ?? false
530
-
531
- if (opts.backend !== 'ollama' && opts.backend !== 'off') {
532
- return refuseClassify(
533
- 'bad-flags',
534
- '--backend takes ollama or off.',
535
- emitJson,
536
- )
537
- }
538
-
539
- if (opts.backend === 'ollama' && !opts.model) {
540
- return refuseClassify(
541
- 'bad-flags',
542
- '--model is required when --backend is ollama.',
543
- emitJson,
544
- )
545
- }
546
-
547
- const backend: ClassifierBackend | 'off' = opts.backend
548
- writeClassifierConfig(
549
- root,
550
- backend,
551
- backend === 'off' ? undefined : opts.model,
552
- )
553
-
554
- intro('canon context classifier set')
555
- logInfo(
556
- `Wrote ${CLASSIFIER_CONFIG_REL}: backend ${opts.backend}${opts.model ? `, model ${opts.model}` : ''}.`,
557
- )
558
- outro()
559
-
560
- if (emitJson) {
561
- process.stdout.write(
562
- `${JSON.stringify({ decision: 'ok', backend: opts.backend, model: opts.model })}\n`,
563
- )
564
- }
565
-
566
- return 0
567
142
  }
568
143
 
569
144
  function parseFolders(list: string | undefined): string[] | string {
@@ -258,7 +258,7 @@ export function register(program: Command): void {
258
258
  'never overwrites one.',
259
259
  '',
260
260
  'Examples:',
261
- ' canon tooling customize e2e/home.spec.ts',
261
+ ' canon tooling customize e2e/screenshot.ts',
262
262
  '',
263
263
  ].join('\n'),
264
264
  )
@@ -1014,6 +1014,9 @@ function reportPackage(result: ScanResult): void {
1014
1014
  logStep('Scanning dependencies')
1015
1015
  for (const entry of result.deps) {
1016
1016
  if (entry.state === 'present') logInfo(entry.name)
1017
+ if (entry.state === 'ahead') {
1018
+ logInfo(`${entry.name} ${entry.installed} (ahead of ${entry.spec})`)
1019
+ }
1017
1020
  }
1018
1021
  for (const entry of result.deps) {
1019
1022
  if (entry.state === 'missing') logAdd(entry.spec)
@@ -1,9 +1,10 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { readFile } from 'node:fs/promises'
3
- import { resolve } from 'node:path'
3
+ import { relative, resolve } from 'node:path'
4
4
  import { listRepositoryFiles } from '@/git/files'
5
5
  import { RECORD_ROOTS } from '@/roots/record'
6
6
  import { SURFACE_ROOTS } from '@/roots/surface'
7
+ import { stampPaths } from '@/sync/stamp'
7
8
 
8
9
  /**
9
10
  * Suppresses citation checking for the source line carrying it.
@@ -191,7 +192,11 @@ export type CitationReport =
191
192
  | { readonly kind: 'unavailable' }
192
193
 
193
194
  /**
194
- * Resolves every cited path outside the fixture trees.
195
+ * Resolves every cited path outside the fixture trees and the install stamp.
196
+ *
197
+ * The stamp is written by the toolkit, and `canon tooling decline` records the
198
+ * declined path as a key in it, so scanning it reports a deliberate decline as
199
+ * a stale citation.
195
200
  *
196
201
  * This is the only check that gates the repository check, which makes it the
197
202
  * only one whose false positives cost a contributor anything. It reports a
@@ -206,7 +211,8 @@ export async function auditCitations(
206
211
  const listed = await listRepositoryFiles(root)
207
212
  if (!listed) return { kind: 'unavailable' }
208
213
 
209
- const candidates = listed.filter((rel) => !isFixture(rel))
214
+ const stamps = new Set(stampPaths(root).map((path) => relative(root, path)))
215
+ const candidates = listed.filter((rel) => !isFixture(rel) && !stamps.has(rel))
210
216
  const citations: Citation[] = []
211
217
 
212
218
  for (const rel of candidates) {
@@ -12,6 +12,7 @@ import {
12
12
  auditSkills,
13
13
  LEDGER_SKILLS,
14
14
  PRACTICE_LEDGER,
15
+ SKILL_LEDGERS,
15
16
  PRACTICE_SKILLS,
16
17
  SHIPPED_CORPUS,
17
18
  } from '@/claude/skills-audit'
@@ -384,7 +385,7 @@ export const skillProvenance: Measure = async (ctx) => {
384
385
 
385
386
  /**
386
387
  * Every listed practice skill against the closing sections, and every skill on
387
- * the wider ledger list against the ledger, both of which
388
+ * the wider ledger list against a ledger that cites something, both of which
388
389
  * `claude/skills/create-skill/references/skill-practice.md` requires.
389
390
  *
390
391
  * Read in-process for the reason `skillProvenance` is, so the verb keeps its
@@ -407,14 +408,14 @@ export const skillPracticeShape: Measure = async (ctx) => {
407
408
  .join(', ')
408
409
  return {
409
410
  emissions: [],
410
- failure: `${plural(found.length, 'practice shape finding')}: ${where}. Add the closing H2 exactly as named to a practice skill, or the ledger at ${PRACTICE_LEDGER} to any skill on the ledger list.`,
411
+ failure: `${plural(found.length, 'practice shape finding')}: ${where}. Add the closing H2 exactly as named to a practice skill, or the ledger at ${PRACTICE_LEDGER} to any skill on the ledger list, citing one linked source or one House choice row. A reference of a skill on the per-bullet list, unless exempted, takes one ledger row per bullet under the same H2 at ${SKILL_LEDGERS}.`,
411
412
  }
412
413
  }
413
414
 
414
415
  return {
415
416
  emissions: [
416
417
  info(
417
- `${plural(PRACTICE_SKILLS.length, 'practice skill')} carry every closing section, and ${plural(LEDGER_SKILLS.length, 'listed skill')} carry a ledger`,
418
+ `${plural(PRACTICE_SKILLS.length, 'practice skill')} carry every closing section, and ${plural(LEDGER_SKILLS.length, 'listed skill')} carry a ledger citing a source`,
418
419
  ),
419
420
  ],
420
421
  }
package/src/git/files.ts CHANGED
@@ -140,7 +140,9 @@ export async function listIgnoreAdditions(
140
140
  /**
141
141
  * Lists the files under `root`: tracked, plus untracked files git does not
142
142
  * ignore. The untracked half is what keeps a file added on this branch in scope
143
- * rather than one push later.
143
+ * rather than one push later. A tracked path deleted in the working tree is
144
+ * left out, since a caller reading the listing would otherwise open a file that
145
+ * is gone.
144
146
  *
145
147
  * Returns undefined when git cannot answer, which every caller reports rather
146
148
  * than smoothing into an empty list. An empty list passes each of its zero
@@ -161,9 +163,12 @@ export async function listRepositoryFiles(
161
163
 
162
164
  const tracked = await run(['ls-files'])
163
165
  const untracked = await run(['ls-files', '--others', '--exclude-standard'])
164
- if (!tracked || !untracked) return undefined
166
+ const deleted = await run(['ls-files', '--deleted'])
167
+ if (!tracked || !untracked || !deleted) return undefined
165
168
 
166
- return [...new Set([...tracked, ...untracked])].sort()
169
+ const gone = new Set(deleted)
170
+ const present = tracked.filter((path) => !gone.has(path))
171
+ return [...new Set([...present, ...untracked])].sort()
167
172
  }
168
173
 
169
174
  /**
package/src/gov/audit.ts CHANGED
@@ -294,7 +294,12 @@ export function countLedgerRows(text: string): HeadingCount[] {
294
294
  return [...counts].map(([key, count]) => ({ heading: key, count }))
295
295
  }
296
296
 
297
- function compareLedger(
297
+ /**
298
+ * Pairs bullets and ledger rows under each heading, naming every heading whose
299
+ * counts differ. The skill audit reads a practice skill's references through
300
+ * it, so a rule and a reference are held to one comparison.
301
+ */
302
+ export function compareLedger(
298
303
  groups: readonly HeadingCount[],
299
304
  rows: readonly HeadingCount[],
300
305
  ): LedgerMismatch[] {
@@ -27,7 +27,7 @@ export interface Subject {
27
27
 
28
28
  /**
29
29
  * Reports what each element changes about itself when it takes focus, which is
30
- * the reading `governance/rules/ui/keyboard.md` states three rules against and
30
+ * the reading the `ui-craft` keyboard reference states three rules against and
31
31
  * nothing measures.
32
32
  *
33
33
  * The difference between rest and focus is the treatment, rather than the
@@ -120,7 +120,7 @@ export function appendGatedPrefix(
120
120
  return 'appended'
121
121
  }
122
122
 
123
- /** Written by hand rather than serialized, the way `classifier.toml` is. */
123
+ /** Written by hand rather than serialized, so the list keeps its one-entry-per-line form. */
124
124
  function writeGatedList(root: string, prefixes: readonly string[]): void {
125
125
  const path = join(root, GATED_CONFIG_REL)
126
126
  const mode = existsSync(path) ? statSync(path).mode : undefined
@@ -154,7 +154,6 @@ export const GATED_PATHS: readonly string[] = [
154
154
  'claude/skills/codebase-layout/references/tests.md',
155
155
  'claude/skills/context-fold/REQUIREMENT.md',
156
156
  'claude/skills/context-fold/SKILL.md',
157
- 'claude/skills/context-fold/references/classify.md',
158
157
  'claude/skills/context-fold/references/completion-report.md',
159
158
  'claude/skills/context-fold/references/context-refresh.md',
160
159
  'claude/skills/context-fold/references/context.md',
@@ -17,7 +17,9 @@ export interface ScriptState {
17
17
  export interface DepState {
18
18
  readonly spec: string
19
19
  readonly name: string
20
- readonly state: 'missing' | 'present'
20
+ readonly state: 'missing' | 'present' | 'ahead'
21
+ /** The spec the target records, set only when it is `ahead` of the pin. */
22
+ readonly installed?: string
21
23
  }
22
24
 
23
25
  export function readPackage(path: string): PackageJson | undefined {
@@ -89,18 +91,24 @@ function firstInteger(value: string): string | undefined {
89
91
 
90
92
  /**
91
93
  * Compares a manifest pin against what package.json records. A spec counts as
92
- * missing when absent, or when the pinned major differs from the installed
93
- * major. Anything looser than a major mismatch is left alone.
94
+ * missing when absent or on an earlier major than the pin, and ahead when it
95
+ * sits on a later one, which a write leaves alone. A spec with no integer to
96
+ * compare, such as `latest` or `workspace:*`, counts as present.
94
97
  */
95
- function isSatisfied(installed: string | undefined, pin: string): boolean {
96
- if (installed === undefined) return false
97
- if (pin === '') return true
98
+ function compareSpec(
99
+ installed: string | undefined,
100
+ pin: string,
101
+ ): DepState['state'] {
102
+ if (installed === undefined) return 'missing'
103
+ if (pin === '') return 'present'
98
104
 
99
105
  const pinMajor = firstInteger(pin)
100
106
  const installedMajor = firstInteger(installed)
101
- if (pinMajor === undefined) return true
107
+ if (pinMajor === undefined || installedMajor === undefined) return 'present'
102
108
 
103
- return pinMajor === installedMajor
109
+ const delta = Number(installedMajor) - Number(pinMajor)
110
+ if (delta === 0) return 'present'
111
+ return delta > 0 ? 'ahead' : 'missing'
104
112
  }
105
113
 
106
114
  /**
@@ -121,11 +129,12 @@ export function collectDeps(
121
129
  if (name === '' || seen.has(name)) continue
122
130
  seen.add(name)
123
131
 
124
- states.push({
125
- spec,
126
- name,
127
- state: isSatisfied(installed[name], pin) ? 'present' : 'missing',
128
- })
132
+ const state = compareSpec(installed[name], pin)
133
+ states.push(
134
+ state === 'ahead'
135
+ ? { spec, name, state, installed: installed[name] }
136
+ : { spec, name, state },
137
+ )
129
138
  }
130
139
  }
131
140
 
@@ -45,6 +45,8 @@ The web layer covers the tooling shared across Vite + React, Astro, and Next.js.
45
45
 
46
46
  ## Screenshot cases
47
47
 
48
+ The starter `e2e/home.spec.ts` is seeded once. Sync copies it into a project that has none and leaves it alone after, so grow it freely.
49
+
48
50
  - Edit the `CASES` in the seeded `e2e/capture/cases.ts`, one entry per output file naming a section, a theme, and a route, since sync overwrites the `e2e/screenshot.ts` harness
49
51
  - Move cases from `e2e/screenshot.ts` to `e2e/capture/cases.ts` before a sync, and set `EVIDENCE_ROOT = 'evidence'` to keep an existing baseline
50
52
  - Expect each case crossed with `VIEWPORTS`, written to `screenshots/<hostname>/<section>/<width>--<theme>.png`, so local and deployed runs stay apart
@@ -76,7 +78,7 @@ The web layer covers the tooling shared across Vite + React, Astro, and Next.js.
76
78
 
77
79
  - Expect the sweep under `screenshots/` to be ignored and only a flagged case's `EVIDENCE_ROOT` output tracked, so a target's first capture is the baseline it commits
78
80
  - Size a case's `sections` list against the frames each later change regenerates, since a case naming sections commits a folder per section
79
- - Run the capture after a surface changes, component files included, per `governance/rules/ui/surface-capture.md`, unless the production build strips the component out
81
+ - Run the capture after a surface changes, component files included, per the `ui-craft` skill's capture reference, unless the production build strips the component out
80
82
 
81
83
  ## Screenshot scripts and workflows
82
84