@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.
- package/CHANGELOG.md +26 -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 +17 -1
- package/claude/skills/design-record/references/design.md +1 -1
- package/claude/skills/design-taste/REQUIREMENT.md +2 -1
- package/claude/skills/design-taste/SKILL.md +6 -5
- package/claude/skills/design-taste/references/adopted.md +56 -8
- package/claude/skills/design-taste/references/craft.md +1 -1
- package/claude/skills/design-taste/references/kinds.md +8 -2
- package/claude/skills/design-taste/references/preflight.md +3 -3
- package/claude/skills/design-taste/references/tells.md +26 -5
- 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/claude/skills/plan-feature/SKILL.md +2 -2
- package/claude/skills/plan-feature/references/plan.md +24 -3
- package/claude/skills/review-craft/SKILL.md +1 -1
- package/claude/skills/role-worker/SKILL.md +2 -1
- package/claude/skills/ui-checklist/SKILL.md +1 -1
- package/claude/skills/ui-craft/REQUIREMENT.md +29 -0
- package/claude/skills/ui-craft/SKILL.md +44 -0
- package/{governance/rules/ui → claude/skills/ui-craft/references}/a11y.md +0 -10
- package/claude/skills/ui-craft/references/adopted.md +27 -0
- package/{governance/rules/ui/surface-capture.md → claude/skills/ui-craft/references/capture.md} +0 -10
- package/{governance/rules/ui/ux-controls.md → claude/skills/ui-craft/references/controls.md} +3 -10
- package/claude/skills/ui-craft/references/copy.md +45 -0
- package/{governance/rules/ui → claude/skills/ui-craft/references}/forms.md +0 -9
- package/{governance/rules/ui → claude/skills/ui-craft/references}/keyboard.md +0 -10
- package/{governance/rules/ui → claude/skills/ui-craft/references}/motion.md +0 -10
- package/{governance/rules/ui → claude/skills/ui-craft/references}/pointer.md +0 -10
- package/{governance/rules/ui/ux-completeness.md → claude/skills/ui-craft/references/states.md} +0 -10
- package/claude/skills/ux-audit/SKILL.md +1 -1
- package/claude/skills/write-human/references/adopted.md +4 -1
- package/docs/agents/audits.md +2 -6
- package/docs/agents/context-audit.md +1 -1
- package/docs/agents/install-and-sync.md +2 -2
- package/docs/target/migrations.md +3 -3
- package/docs/target/sync.md +3 -2
- package/docs/workflow/skill-map.md +1 -0
- package/docs/workflow/visual-design-workflow.md +2 -2
- package/governance/detect.toml +1 -45
- package/governance/ledger/lib/test-scope.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/governance/ledger/{ui → skills/ui-craft}/a11y.md +3 -3
- package/governance/ledger/{ui/surface-capture.md → skills/ui-craft/capture.md} +3 -3
- package/governance/ledger/{ui/ux-controls.md → skills/ui-craft/controls.md} +12 -7
- package/governance/ledger/skills/ui-craft/copy.md +102 -0
- package/governance/ledger/{ui → skills/ui-craft}/forms.md +3 -3
- package/governance/ledger/{ui → skills/ui-craft}/keyboard.md +3 -3
- package/governance/ledger/{ui → skills/ui-craft}/motion.md +3 -3
- package/governance/ledger/{ui → skills/ui-craft}/pointer.md +3 -3
- package/governance/ledger/{ui/ux-completeness.md → skills/ui-craft/states.md} +3 -3
- package/governance/ledger/ui/craft.md +19 -0
- package/governance/renames.toml +17 -3
- package/governance/rules/lib/test-scope.md +1 -1
- package/governance/rules/ui/craft.md +22 -0
- package/governance/splits.toml +0 -2
- package/package.json +1 -1
- package/src/audits/catalog.ts +0 -52
- package/src/claude/cases/misc.ts +5 -0
- package/src/claude/skills-audit.ts +226 -12
- package/src/cli/retired.ts +8 -0
- package/src/commands/claude.ts +15 -1
- package/src/commands/context.ts +0 -425
- package/src/commands/tooling.ts +4 -1
- package/src/context/citations.ts +9 -3
- package/src/gate/measures.ts +4 -3
- package/src/git/files.ts +8 -3
- package/src/gov/audit.ts +6 -1
- package/src/inventory/subjects.ts +1 -1
- package/src/markdown/gated.ts +1 -1
- package/src/markdown/scope.ts +0 -1
- package/src/tooling/package.ts +22 -13
- package/tooling/web/reference.md +3 -1
- package/claude/skills/context-fold/references/classify.md +0 -87
- package/governance/ledger/ui/copy.md +0 -49
- package/governance/ledger/ui/design-taste.md +0 -19
- package/governance/ledger/ui/interface-casing.md +0 -47
- package/governance/ledger/ui/link-behavior.md +0 -29
- package/governance/rules/ui/copy.md +0 -28
- package/governance/rules/ui/design-taste.md +0 -18
- package/governance/rules/ui/interface-casing.md +0 -29
- package/governance/rules/ui/link-behavior.md +0 -19
- 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/tooling/web/{configs → seeds}/e2e/home.spec.ts +0 -0
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/commands/tooling.ts
CHANGED
|
@@ -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/
|
|
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)
|
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
|
@@ -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
|
|
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
|
-
|
|
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/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
|
-
|
|
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 `
|
|
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
|
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',
|
package/src/tooling/package.ts
CHANGED
|
@@ -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
|
|
93
|
-
*
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
107
|
+
if (pinMajor === undefined || installedMajor === undefined) return 'present'
|
|
102
108
|
|
|
103
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
|
package/tooling/web/reference.md
CHANGED
|
@@ -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 `
|
|
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
|
|