@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/audits/catalog.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type { SkillsAuditRefusal } from '@/claude/skills-audit'
|
|
|
2
2
|
import type { RankRefusal } from '@/claude/skills-rank'
|
|
3
3
|
import type { ReachRefusal } from '@/claude/skills-reach'
|
|
4
4
|
import type { ContextAuditRefusal } from '@/context/audit'
|
|
5
|
-
import type { SweepVerdict } from '@/context/classify/patterns'
|
|
6
5
|
import type { AuditRefusal } from '@/deps/audit'
|
|
7
6
|
import type { RuleAuditRefusal } from '@/gov/audit'
|
|
8
7
|
import type { RestatedRefusal } from '@/gov/restated'
|
|
@@ -161,45 +160,6 @@ function requirementsCounts(
|
|
|
161
160
|
return { requirementsOverWords: record.words > record.wordCap ? 1 : 0 }
|
|
162
161
|
}
|
|
163
162
|
|
|
164
|
-
/**
|
|
165
|
-
* The retained key for each verdict the sweep can return. A record rather than
|
|
166
|
-
* a list, so the compiler fails here when the classifier gains a verdict,
|
|
167
|
-
* instead of that verdict's sections dropping out of the tally.
|
|
168
|
-
*/
|
|
169
|
-
const PLACEMENT_KEYS: Record<SweepVerdict, string> = {
|
|
170
|
-
KEEP: 'keep',
|
|
171
|
-
REWRITE: 'rewrite',
|
|
172
|
-
MOVE: 'move',
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function isPlacementVerdict(value: unknown): value is SweepVerdict {
|
|
176
|
-
return typeof value === 'string' && Object.hasOwn(PLACEMENT_KEYS, value)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Tallies the sweep's findings by verdict, with a zero for a verdict nothing
|
|
181
|
-
* returned, so a baseline reads the same three keys on every run.
|
|
182
|
-
*
|
|
183
|
-
* `KEEP` is retained beside the two that ask for a change. It moves when a
|
|
184
|
-
* section is added or split, which is what tells a falling `rewrite` count
|
|
185
|
-
* from a corpus that shrank.
|
|
186
|
-
*/
|
|
187
|
-
function placementCounts(record: unknown): Record<string, number> | undefined {
|
|
188
|
-
const findings = asObject(record)?.findings
|
|
189
|
-
if (!Array.isArray(findings)) return undefined
|
|
190
|
-
|
|
191
|
-
const counts: Record<string, number> = Object.fromEntries(
|
|
192
|
-
Object.values(PLACEMENT_KEYS).map((key) => [key, 0]),
|
|
193
|
-
)
|
|
194
|
-
for (const finding of findings) {
|
|
195
|
-
const verdict = asObject(finding)?.verdict
|
|
196
|
-
if (!isPlacementVerdict(verdict)) return undefined
|
|
197
|
-
counts[PLACEMENT_KEYS[verdict]] += 1
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return counts
|
|
201
|
-
}
|
|
202
|
-
|
|
203
163
|
function contextCounts(record: unknown): Record<string, number> | undefined {
|
|
204
164
|
const root = asObject(record)
|
|
205
165
|
if (root === undefined) return undefined
|
|
@@ -526,18 +486,6 @@ export const AUDITS: readonly AuditSpec[] = [
|
|
|
526
486
|
absentReasons: ['no-folders'] satisfies ContextAuditRefusal[],
|
|
527
487
|
counts: contextCounts,
|
|
528
488
|
},
|
|
529
|
-
{
|
|
530
|
-
id: 'placement',
|
|
531
|
-
label: 'Context placement',
|
|
532
|
-
// Pinned to the regex layer rather than the resolved classifier setting.
|
|
533
|
-
// A count that moves with whichever model a machine runs is not
|
|
534
|
-
// comparable across clones, and the model layer turns a sub-second read
|
|
535
|
-
// into a multi-minute one. `document-health` is where the model reads.
|
|
536
|
-
argv: ['context', 'classify', 'sweep', '--backend', 'off', '--json'],
|
|
537
|
-
gatingExits: [],
|
|
538
|
-
corpus: 'tracked',
|
|
539
|
-
counts: placementCounts,
|
|
540
|
-
},
|
|
541
489
|
{
|
|
542
490
|
id: 'markdown',
|
|
543
491
|
label: 'Markdown corpus',
|
|
@@ -5,8 +5,8 @@ import { plural } from '@/cli/ui'
|
|
|
5
5
|
import {
|
|
6
6
|
compareLedger,
|
|
7
7
|
countLedgerRows,
|
|
8
|
+
type HeadingCount,
|
|
8
9
|
type LedgerMismatch,
|
|
9
|
-
measureRule,
|
|
10
10
|
} from '@/gov/audit'
|
|
11
11
|
import { parseFrontmatter, readField } from '@/indexes/frontmatter'
|
|
12
12
|
import {
|
|
@@ -84,19 +84,49 @@ export const LEDGER_SKILLS: readonly string[] = [
|
|
|
84
84
|
join(SHIPPED_CORPUS, 'ui-craft'),
|
|
85
85
|
]
|
|
86
86
|
|
|
87
|
+
function references(...names: string[]): string[] {
|
|
88
|
+
return names.map((name) => join('references', `${name}.md`))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface BulletLedgerScope {
|
|
92
|
+
/** References left off, each a checklist or glossary naming no new source. */
|
|
93
|
+
readonly exempt: readonly string[]
|
|
94
|
+
/** True when `SKILL.md` carries directives of its own to ledger. */
|
|
95
|
+
readonly body: boolean
|
|
96
|
+
}
|
|
97
|
+
|
|
87
98
|
/**
|
|
88
99
|
* The ledger-listed skills whose every reference but the source record carries
|
|
89
100
|
* a ledger of its own, one table row per bullet under each H2, the way a rule
|
|
90
|
-
* in a practice folder does.
|
|
91
|
-
*
|
|
101
|
+
* in a practice folder does. A reference is ledgered unless exempted by name,
|
|
102
|
+
* so one added later fails until its rows land rather than passing unread. The
|
|
103
|
+
* ledgers sit outside the plugin as rule ledgers do, so a target pays nothing
|
|
104
|
+
* for them.
|
|
92
105
|
*/
|
|
93
|
-
export const BULLET_LEDGER_SKILLS:
|
|
94
|
-
|
|
95
|
-
]
|
|
106
|
+
export const BULLET_LEDGER_SKILLS: Readonly<Record<string, BulletLedgerScope>> =
|
|
107
|
+
{
|
|
108
|
+
[join(SHIPPED_CORPUS, 'ui-craft')]: { exempt: [], body: false },
|
|
109
|
+
[join(SHIPPED_CORPUS, 'api-design')]: { exempt: [], body: true },
|
|
110
|
+
[join(SHIPPED_CORPUS, 'code-craft')]: { exempt: [], body: true },
|
|
111
|
+
[join(SHIPPED_CORPUS, 'deprecation-migration')]: { exempt: [], body: true },
|
|
112
|
+
[join(SHIPPED_CORPUS, 'design-taste')]: {
|
|
113
|
+
exempt: references('preflight', 'vocabulary'),
|
|
114
|
+
body: true,
|
|
115
|
+
},
|
|
116
|
+
}
|
|
96
117
|
|
|
97
|
-
/** Where a per-bullet ledger lives, as `<skill>/<
|
|
118
|
+
/** Where a per-bullet ledger lives, as `<skill>/<file>.md` below it. */
|
|
98
119
|
export const SKILL_LEDGERS = join('governance', 'ledger', 'skills')
|
|
99
120
|
|
|
121
|
+
/**
|
|
122
|
+
* The ledger a ledgered file pairs with, keyed by the file's own name, so a
|
|
123
|
+
* reference drops its `references/` segment and a `SKILL.md` keeps its name.
|
|
124
|
+
*/
|
|
125
|
+
export function skillLedgerPath(folder: string, file: string): string {
|
|
126
|
+
const inside = relative('references', file)
|
|
127
|
+
return join(SKILL_LEDGERS, folder, inside.startsWith('..') ? file : inside)
|
|
128
|
+
}
|
|
129
|
+
|
|
100
130
|
/** The closing H2s a practice skill carries, matched exactly outside fences. */
|
|
101
131
|
export const PRACTICE_SECTIONS: readonly string[] = [
|
|
102
132
|
'Excuses and rebuttals',
|
|
@@ -104,6 +134,16 @@ export const PRACTICE_SECTIONS: readonly string[] = [
|
|
|
104
134
|
'Before handing over',
|
|
105
135
|
]
|
|
106
136
|
|
|
137
|
+
/**
|
|
138
|
+
* The H2s that end a listed body's concern groups. The closing sections
|
|
139
|
+
* restate the groups' rules as excuses, signs, and checks, and the delegates
|
|
140
|
+
* section routes, so neither states a rule of its own to source.
|
|
141
|
+
*/
|
|
142
|
+
export const BODY_CLOSERS: readonly string[] = [
|
|
143
|
+
...PRACTICE_SECTIONS,
|
|
144
|
+
'What this delegates',
|
|
145
|
+
]
|
|
146
|
+
|
|
107
147
|
/** The source ledger every practice skill carries beside its body. */
|
|
108
148
|
export const PRACTICE_LEDGER = join('references', 'adopted.md')
|
|
109
149
|
|
|
@@ -157,6 +197,12 @@ const ISSUE_NUMBER = /(?<![\w&/#])#\d+\b/g
|
|
|
157
197
|
|
|
158
198
|
const TOP_LEVEL_BULLET = /^(?:[-*+]|\d+[.)])\s/
|
|
159
199
|
|
|
200
|
+
/**
|
|
201
|
+
* A line a skill ledger pairs with a row: a top-level `- ` bullet as a rule
|
|
202
|
+
* writes one, or a top-level ordered step, which states a directive as surely.
|
|
203
|
+
*/
|
|
204
|
+
const LEDGERED_LINE = /^(?:- |\d+[.)]\s)/
|
|
205
|
+
|
|
160
206
|
const WORD = /\S*[\p{L}\p{N}]\S*/gu
|
|
161
207
|
|
|
162
208
|
export interface SkillFinding {
|
|
@@ -213,8 +259,10 @@ interface SkillSource {
|
|
|
213
259
|
* requirement is where the standard sends a date.
|
|
214
260
|
*/
|
|
215
261
|
readonly bodies: readonly SkillText[]
|
|
216
|
-
/** One entry per
|
|
262
|
+
/** One entry per ledgered file of a skill on the per-bullet ledger list. */
|
|
217
263
|
readonly referenceLedgers: readonly ReferenceLedger[]
|
|
264
|
+
/** Exempted references the skill no longer carries, which exempt nothing. */
|
|
265
|
+
readonly staleExemptions: readonly string[]
|
|
218
266
|
}
|
|
219
267
|
|
|
220
268
|
interface ReferenceLedger {
|
|
@@ -342,15 +390,22 @@ async function readSkill(
|
|
|
342
390
|
|
|
343
391
|
const fields = parseFrontmatter(body)
|
|
344
392
|
const skillRel = join(rel, folder)
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
393
|
+
const scope = BULLET_LEDGER_SKILLS[skillRel]
|
|
394
|
+
const referenceLedgers =
|
|
395
|
+
scope === undefined
|
|
396
|
+
? []
|
|
397
|
+
: await readReferenceLedgers(root, skillRel, folder, [
|
|
398
|
+
...(scope.body ? [{ path: 'SKILL.md', text: body }] : []),
|
|
399
|
+
...references
|
|
400
|
+
.map((path, index) => ({ path, text: referenceTexts[index] ?? '' }))
|
|
401
|
+
.filter(
|
|
402
|
+
({ path }) =>
|
|
403
|
+
path !== PRACTICE_LEDGER && !scope.exempt.includes(path),
|
|
404
|
+
),
|
|
405
|
+
])
|
|
406
|
+
const staleExemptions = (scope?.exempt ?? [])
|
|
407
|
+
.filter((path) => !references.includes(path))
|
|
408
|
+
.map((path) => join(skillRel, path))
|
|
354
409
|
|
|
355
410
|
return {
|
|
356
411
|
rel: skillRel,
|
|
@@ -373,44 +428,61 @@ async function readSkill(
|
|
|
373
428
|
})),
|
|
374
429
|
],
|
|
375
430
|
referenceLedgers,
|
|
431
|
+
staleExemptions,
|
|
376
432
|
}
|
|
377
433
|
}
|
|
378
434
|
|
|
379
435
|
/**
|
|
380
|
-
* Pairs each
|
|
381
|
-
* audit's own comparison, so a
|
|
382
|
-
* The source record names authorities rather than stating bullets, so it is
|
|
383
|
-
* the one reference asked for no ledger.
|
|
436
|
+
* Pairs each ledgered file's directives with its ledger's rows through the rule
|
|
437
|
+
* audit's own comparison, so a skill file and a rule mismatch alike.
|
|
384
438
|
*/
|
|
385
439
|
async function readReferenceLedgers(
|
|
386
440
|
root: string,
|
|
387
441
|
skillRel: string,
|
|
388
442
|
folder: string,
|
|
389
|
-
|
|
390
|
-
texts: readonly string[],
|
|
443
|
+
files: readonly { path: string; text: string }[],
|
|
391
444
|
): Promise<ReferenceLedger[]> {
|
|
392
445
|
return Promise.all(
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
? compareLedger(
|
|
405
|
-
measureRule(text).groups,
|
|
406
|
-
countLedgerRows(await Bun.file(ledgerPath).text()),
|
|
407
|
-
)
|
|
408
|
-
: undefined
|
|
409
|
-
return { rel: join(skillRel, path), ledgerRel, mismatches }
|
|
410
|
-
}),
|
|
446
|
+
files.map(async ({ path, text }) => {
|
|
447
|
+
const ledgerRel = skillLedgerPath(folder, path)
|
|
448
|
+
const ledgerPath = join(root, ledgerRel)
|
|
449
|
+
const mismatches = existsSync(ledgerPath)
|
|
450
|
+
? compareLedger(
|
|
451
|
+
ledgerGroups(text, path === 'SKILL.md'),
|
|
452
|
+
countLedgerRows(await Bun.file(ledgerPath).text()),
|
|
453
|
+
)
|
|
454
|
+
: undefined
|
|
455
|
+
return { rel: join(skillRel, path), ledgerRel, mismatches }
|
|
456
|
+
}),
|
|
411
457
|
)
|
|
412
458
|
}
|
|
413
459
|
|
|
460
|
+
/**
|
|
461
|
+
* Directives per H2, `null` for any above the first. A contents list repeats
|
|
462
|
+
* the headings rather than stating a rule, so `## Contents` takes no rows. A
|
|
463
|
+
* nested bullet elaborates its parent, so only a top-level line counts. A body
|
|
464
|
+
* stops at its first closer, and a ledger table under one then reads as rows
|
|
465
|
+
* with no bullets rather than passing unread.
|
|
466
|
+
*/
|
|
467
|
+
function ledgerGroups(text: string, isBody: boolean): HeadingCount[] {
|
|
468
|
+
const counts = new Map<string | null, number>()
|
|
469
|
+
let heading: string | null = null
|
|
470
|
+
|
|
471
|
+
for (const line of bodyLines(text)) {
|
|
472
|
+
if (line.fenced) continue
|
|
473
|
+
const section = SECTION.exec(line.text)?.[1]
|
|
474
|
+
if (section !== undefined) {
|
|
475
|
+
if (isBody && BODY_CLOSERS.includes(section)) break
|
|
476
|
+
heading = section
|
|
477
|
+
continue
|
|
478
|
+
}
|
|
479
|
+
if (heading === CONTENTS_SECTION || !LEDGERED_LINE.test(line.text)) continue
|
|
480
|
+
counts.set(heading, (counts.get(heading) ?? 0) + 1)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return [...counts].map(([key, count]) => ({ heading: key, count }))
|
|
484
|
+
}
|
|
485
|
+
|
|
414
486
|
/**
|
|
415
487
|
* A key carrying an empty value declares nothing, so blank reads as absent
|
|
416
488
|
* rather than as a name that disagrees with every folder.
|
|
@@ -603,6 +675,10 @@ function practiceFindings(source: SkillSource): SkillFinding[] {
|
|
|
603
675
|
...sections,
|
|
604
676
|
...ledgerFindings(source),
|
|
605
677
|
...source.referenceLedgers.flatMap(referenceLedgerFindings),
|
|
678
|
+
...source.staleExemptions.map((rel) => ({
|
|
679
|
+
rel,
|
|
680
|
+
detail: 'missing file: exempt from a ledger',
|
|
681
|
+
})),
|
|
606
682
|
]
|
|
607
683
|
}
|
|
608
684
|
|
package/src/cli/retired.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface RetiredVerb {
|
|
|
9
9
|
const DESIGN_MOVED =
|
|
10
10
|
'the design values now live in the stylesheet your canon/DESIGN.md lead names'
|
|
11
11
|
|
|
12
|
+
const CONTEXT_MOVED = '`canon context audit`, which reports placement findings'
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* Every verb a release dropped, with the release and where the work went. The
|
|
14
16
|
* router reads this ahead of the parser, and `canon migrate status` reads it to
|
|
@@ -41,6 +43,12 @@ export const RETIRED_VERBS: readonly RetiredVerb[] = [
|
|
|
41
43
|
release: '5.53.0',
|
|
42
44
|
movedTo: 'a tool built for composed video',
|
|
43
45
|
},
|
|
46
|
+
{ path: ['context', 'classify'], release: '5.64.0', movedTo: CONTEXT_MOVED },
|
|
47
|
+
{
|
|
48
|
+
path: ['context', 'classifier'],
|
|
49
|
+
release: '5.64.0',
|
|
50
|
+
movedTo: CONTEXT_MOVED,
|
|
51
|
+
},
|
|
44
52
|
]
|
|
45
53
|
|
|
46
54
|
/**
|
package/src/commands/claude.ts
CHANGED
|
@@ -168,6 +168,15 @@ interface RoutingOptions {
|
|
|
168
168
|
const SEEDED_FILES: readonly string[] = ['REQUIREMENTS.md', 'DESIGN.md']
|
|
169
169
|
const SEEDED_DIRS: readonly string[] = ['tasks']
|
|
170
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Where a `SEEDED_FILES` entry resolves for the sync presence check, at either
|
|
173
|
+
* surface root, so a target holding its records under `canon/` is not sent to
|
|
174
|
+
* `canon claude init`.
|
|
175
|
+
*/
|
|
176
|
+
export function seededFilePath(resolved: string, name: string): string {
|
|
177
|
+
return surfaceDir(resolved, name)
|
|
178
|
+
}
|
|
179
|
+
|
|
171
180
|
/**
|
|
172
181
|
* Where a `SEEDED_DIRS` entry resolves for the sync presence check.
|
|
173
182
|
*
|
|
@@ -679,7 +688,7 @@ async function runSync(target: string): Promise<number> {
|
|
|
679
688
|
// already holds.
|
|
680
689
|
logStep('Seeded')
|
|
681
690
|
for (const name of SEEDED_FILES) {
|
|
682
|
-
if (existsSync(
|
|
691
|
+
if (existsSync(seededFilePath(resolved, name))) logInfo(name)
|
|
683
692
|
else logWarn(`${name} missing. Run \`canon claude init\``)
|
|
684
693
|
}
|
|
685
694
|
for (const name of SEEDED_DIRS) {
|
|
@@ -1771,7 +1780,7 @@ function reportPracticeShape(report: SkillsAudit): void {
|
|
|
1771
1780
|
`A listed practice skill closes with ${PRACTICE_SECTIONS.map((section) => `## ${section}`).join(', ')}, and every skill on the wider ledger list carries ${PRACTICE_LEDGER}.`,
|
|
1772
1781
|
)
|
|
1773
1782
|
logInfo(
|
|
1774
|
-
`A skill on the per-bullet list, ${plural(BULLET_LEDGER_SKILLS.length, 'skill')} here, also matches each
|
|
1783
|
+
`A skill on the per-bullet list, ${plural(Object.keys(BULLET_LEDGER_SKILLS).length, 'skill')} here, also matches each reference it does not exempt, and a listed body, to a ledger under ${SKILL_LEDGERS}, one row per bullet under each H2.`,
|
|
1775
1784
|
)
|
|
1776
1785
|
logInfo(
|
|
1777
1786
|
`Both lists are kept here and name ${plural(PRACTICE_SKILLS.length, 'practice skill')} and ${plural(LEDGER_SKILLS.length, 'ledger skill')}, so a skill outside ${SHIPPED_CORPUS} is never read.`,
|