@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.
- package/CHANGELOG.md +16 -0
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/api-design/references/adopted.md +1 -1
- package/claude/skills/context-fold/REQUIREMENT.md +2 -3
- package/claude/skills/context-fold/SKILL.md +1 -11
- package/claude/skills/context-fold/references/completion-report.md +8 -9
- package/claude/skills/create-skill/references/skill-practice.md +16 -1
- package/claude/skills/design-taste/references/adopted.md +17 -97
- package/claude/skills/design-taste/references/tells.md +2 -2
- package/claude/skills/document-health/REQUIREMENT.md +1 -1
- package/claude/skills/document-health/SKILL.md +3 -14
- package/claude/skills/document-health/references/axes.md +14 -16
- package/docs/agents/audits.md +2 -6
- package/docs/agents/context-audit.md +1 -1
- package/governance/ledger/skills/api-design/SKILL.md +62 -0
- package/governance/ledger/skills/code-craft/SKILL.md +53 -0
- package/governance/ledger/skills/deprecation-migration/SKILL.md +68 -0
- package/governance/ledger/skills/design-taste/SKILL.md +64 -0
- package/governance/ledger/skills/design-taste/craft.md +66 -0
- package/governance/ledger/skills/design-taste/kinds.md +58 -0
- package/governance/ledger/skills/design-taste/systems.md +42 -0
- package/governance/ledger/skills/design-taste/tells.md +103 -0
- package/package.json +1 -1
- package/src/audits/catalog.ts +0 -52
- package/src/claude/skills-audit.ts +117 -41
- package/src/cli/retired.ts +8 -0
- package/src/commands/claude.ts +11 -2
- package/src/commands/context.ts +0 -425
- package/src/context/citations.ts +9 -3
- package/src/gate/measures.ts +1 -1
- package/src/git/files.ts +8 -3
- package/src/markdown/gated.ts +1 -1
- package/src/markdown/scope.ts +0 -1
- package/claude/skills/context-fold/references/classify.md +0 -87
- package/src/context/classify/extract.ts +0 -443
- package/src/context/classify/ollama.ts +0 -172
- package/src/context/classify/patterns.ts +0 -75
- package/src/context/classify/prompts.ts +0 -74
- package/src/context/classify/run.ts +0 -348
- package/src/context/classify/settings.ts +0 -196
package/src/commands/context.ts
CHANGED
|
@@ -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 {
|
package/src/context/citations.ts
CHANGED
|
@@ -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
|
|
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) {
|
package/src/gate/measures.ts
CHANGED
|
@@ -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
|
-
|
|
166
|
+
const deleted = await run(['ls-files', '--deleted'])
|
|
167
|
+
if (!tracked || !untracked || !deleted) return undefined
|
|
165
168
|
|
|
166
|
-
|
|
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/markdown/gated.ts
CHANGED
|
@@ -120,7 +120,7 @@ export function appendGatedPrefix(
|
|
|
120
120
|
return 'appended'
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
/** Written by hand rather than serialized, the
|
|
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
|
package/src/markdown/scope.ts
CHANGED
|
@@ -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.
|