@erclx/canon 5.63.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 (40) hide show
  1. package/CHANGELOG.md +16 -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 +16 -1
  8. package/claude/skills/design-taste/references/adopted.md +17 -97
  9. package/claude/skills/design-taste/references/tells.md +2 -2
  10. package/claude/skills/document-health/REQUIREMENT.md +1 -1
  11. package/claude/skills/document-health/SKILL.md +3 -14
  12. package/claude/skills/document-health/references/axes.md +14 -16
  13. package/docs/agents/audits.md +2 -6
  14. package/docs/agents/context-audit.md +1 -1
  15. package/governance/ledger/skills/api-design/SKILL.md +62 -0
  16. package/governance/ledger/skills/code-craft/SKILL.md +53 -0
  17. package/governance/ledger/skills/deprecation-migration/SKILL.md +68 -0
  18. package/governance/ledger/skills/design-taste/SKILL.md +64 -0
  19. package/governance/ledger/skills/design-taste/craft.md +66 -0
  20. package/governance/ledger/skills/design-taste/kinds.md +58 -0
  21. package/governance/ledger/skills/design-taste/systems.md +42 -0
  22. package/governance/ledger/skills/design-taste/tells.md +103 -0
  23. package/package.json +1 -1
  24. package/src/audits/catalog.ts +0 -52
  25. package/src/claude/skills-audit.ts +117 -41
  26. package/src/cli/retired.ts +8 -0
  27. package/src/commands/claude.ts +11 -2
  28. package/src/commands/context.ts +0 -425
  29. package/src/context/citations.ts +9 -3
  30. package/src/gate/measures.ts +1 -1
  31. package/src/git/files.ts +8 -3
  32. package/src/markdown/gated.ts +1 -1
  33. package/src/markdown/scope.ts +0 -1
  34. package/claude/skills/context-fold/references/classify.md +0 -87
  35. package/src/context/classify/extract.ts +0 -443
  36. package/src/context/classify/ollama.ts +0 -172
  37. package/src/context/classify/patterns.ts +0 -75
  38. package/src/context/classify/prompts.ts +0 -74
  39. package/src/context/classify/run.ts +0 -348
  40. package/src/context/classify/settings.ts +0 -196
@@ -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 {
@@ -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) {
@@ -408,7 +408,7 @@ export const skillPracticeShape: Measure = async (ctx) => {
408
408
  .join(', ')
409
409
  return {
410
410
  emissions: [],
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 on the per-bullet list takes one ledger row per bullet under the same H2 at ${SKILL_LEDGERS}.`,
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}.`,
412
412
  }
413
413
  }
414
414
 
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
  /**
@@ -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',
@@ -1,87 +0,0 @@
1
- ---
2
- title: Classify the fold's diff baseline
3
- description: The classify diff invocation, the record fields to read, applying a finding, the one-line keep reason, the unreachable and missing-subcommand lines, and the report shape
4
- ---
5
-
6
- # Classify the fold's diff baseline
7
-
8
- Mechanics for the classify half of Step 4 of `context-fold`. The body owns the skip condition and the shared baseline, and this file owns the invocation, what the record carries, and what applying a finding does.
9
-
10
- ## Invocation
11
-
12
- Call the verb rather than reimplementing its pattern or prompt in this skill:
13
-
14
- ```bash
15
- canon context classify diff --base <base> --json
16
- ```
17
-
18
- Never substitute a different verb for it, such as `canon docs <name>` (a documentation lookup, not a classification).
19
-
20
- Reuse the base the Diff baseline section already resolved for Steps 2 and 5 rather than resolving a second time. Never pick or configure a backend here. The project setting decides it, and this step reports `modelLayer` as the record returns it.
21
-
22
- ## Reading the record
23
-
24
- Branch on the record's `decision` field, never on the exit code, which a shell function wrapping `canon` can flatten to zero.
25
-
26
- - `decision: 'refused'`: a bad range or a file the verb could not read. Report the record's `message` and continue. See "When the verb is absent or refused" below.
27
- - `decision: 'ok'`: the record carries `modelLayer` (`'off'`, `'ran'`, `'skipped-no-model'`, or `'skipped-unreachable'`) and `findings`.
28
-
29
- Each finding carries `file`, a `verdict` of `KEEP`, `REPLACE`, `HISTORY`, or `MOVE`, and `decidedBy` (`'regex'` or `'model'`) naming which layer's reading won. Take the `quote` and `reason` from whichever of the finding's `regex` or `model` fields `decidedBy` names. Skip every `KEEP` finding: nothing decided against that text.
30
-
31
- ## Scope
32
-
33
- Answer every non-`KEEP` finding the verb returns, not only the files Step 3 wrote this run, since a branch carrying a prior session's commits has doc edits the fold is equally responsible for.
34
-
35
- The verb's own extraction already scopes to canonical doc types and reports nothing when the range carries none, so there is no narrower check to add here.
36
-
37
- ## Applying a finding
38
-
39
- Locate the finding by its `quote` in the file's current content. The quote is often a narrow fragment rather than the whole clause it sits in, since the regex layer's own match is a short pattern hit, so the edit below targets the sentence or bullet the quote sits in rather than the literal substring alone.
40
-
41
- - **Found exactly once.** Apply the verdict under its heading below.
42
- - **Found more than once, or not found at all.** Report it as not located rather than guessing an occurrence or silently rewriting nothing. A model paraphrasing its quote is the usual cause.
43
-
44
- Run the classifier once per fold. Do not re-run it after applying a finding to check the edit, since a second pass over what this step wrote is the loop the verb's own reference already warns against.
45
-
46
- ### REPLACE
47
-
48
- Rewrite the sentence or bullet carrying the quote in place with the fact that now stands, per Step 3's rewrite-in-place rule. State what is current and drop what the quote restated, in the one edit.
49
-
50
- ### HISTORY
51
-
52
- Rewrite the sentence or bullet carrying the quote to drop the narration and keep any current fact the same clause states. The quote narrates how the fact arrived rather than stating the fact, which a canonical doc excludes regardless of what wrote it.
53
-
54
- A literal cut of the fragment alone would leave the rest of the clause grammatically stranded.
55
-
56
- ### MOVE
57
-
58
- Do not cut. The verdict means correct content sits on the wrong surface, such as a rule written into a context entry that belongs in a path-scoped rule, and only the model layer returns it.
59
-
60
- Report the finding instead, naming the surface the `reason` names as where the content belongs, and leave the file unedited. Cutting would delete content a fold with no model configured would never have flagged at all.
61
-
62
- ## Keeping a finding
63
-
64
- Keep a non-`KEEP` finding without applying it only when the quote is itself the alternative a nearby decision states and lost, or the file already carries the current fact elsewhere. State the reason in one line in the report. A keep costs one line, where an incorrect apply costs a rewrite of prose nobody asked to change.
65
-
66
- ## When the verb is absent or refused
67
-
68
- The verb ships with the CLI and this skill ships with the plugin, so a target holding an older installed binary carries no `context classify` subcommand. Never read a missing subcommand as a clean pass, which would report a run that never checked anything as one that found nothing.
69
-
70
- | Case | Report, then continue |
71
- | ------------------ | ------------------------------------------------------------------------------------ |
72
- | Subcommand missing | `⚠ Classify: canon context classify not available on the installed binary. Skipped.` |
73
- | Range refused | `⚠ Classify: <message>`, naming the verb's own message |
74
-
75
- Neither case stops the fold. This step checks what the fold wrote, and a check that cannot run is not a reason to leave the fold's own output unshipped.
76
-
77
- ## Report
78
-
79
- Print one line naming what ran, `✅ Classify: <n> findings, regex ran, model <modelLayer>`, then one line per non-`KEEP` finding:
80
-
81
- - `✏ Rewrote: <file>, "<quote>" (<reason>)`
82
- - `✂ Cut: <file>, "<quote>" (<reason>)`
83
- - `➡ Move needed: <file>, "<quote>" (<reason>)`, for a `MOVE` this step reported rather than cut
84
- - `⏭ Kept: <file>, "<quote>", <reason for keeping>`
85
- - `⚠ Not located: <file>, "<quote>" not found or found more than once`
86
-
87
- When every finding reads `KEEP`, output `✅ Classify: <n> findings, all KEEP` and nothing further.