@erclx/canon 5.73.0 → 5.75.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 (87) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/api-design/references/adopted.md +5 -3
  4. package/claude/skills/build-in-slices/references/adopted.md +4 -2
  5. package/claude/skills/code-craft/references/adopted.md +9 -7
  6. package/claude/skills/codebase-layout/references/adopted.md +8 -6
  7. package/claude/skills/create-rule/SKILL.md +5 -1
  8. package/claude/skills/create-rule/references/ledger.md +73 -0
  9. package/claude/skills/create-rule/references/rule.md +1 -1
  10. package/claude/skills/create-skill/references/skill-practice.md +6 -1
  11. package/claude/skills/deprecation-migration/references/adopted.md +5 -3
  12. package/claude/skills/design-pick/references/board.md +2 -2
  13. package/claude/skills/plan-feature/SKILL.md +2 -1
  14. package/claude/skills/plan-groundwork/SKILL.md +2 -0
  15. package/claude/skills/pr-evidence/REQUIREMENT.md +1 -0
  16. package/claude/skills/pr-evidence/SKILL.md +2 -0
  17. package/claude/skills/pr-evidence/references/evidence.md +11 -4
  18. package/claude/skills/pr-evidence/references/reel.md +108 -0
  19. package/claude/skills/proposal-critique/REQUIREMENT.md +34 -0
  20. package/claude/skills/proposal-critique/SKILL.md +126 -0
  21. package/claude/skills/proposal-critique/references/adopted.md +44 -0
  22. package/claude/skills/review-craft/references/adopted.md +4 -4
  23. package/claude/skills/search-craft/references/adopted.md +9 -7
  24. package/claude/skills/systematic-debugging/references/adopted.md +4 -2
  25. package/claude/skills/teach-craft/references/adopted.md +8 -6
  26. package/claude/skills/test-craft/references/adopted.md +14 -12
  27. package/claude/skills/test-first/SKILL.md +1 -1
  28. package/claude/skills/test-first/references/adopted.md +9 -5
  29. package/claude/skills/ui-film/REQUIREMENT.md +4 -5
  30. package/claude/skills/ui-film/SKILL.md +21 -19
  31. package/claude/skills/ui-film/references/compose-craft.md +2 -2
  32. package/claude/skills/ui-film/references/handoff.md +2 -7
  33. package/claude/skills/write-human/references/adopted.md +8 -3
  34. package/docs/agents/audits.md +24 -24
  35. package/docs/agents/migrate.md +10 -10
  36. package/docs/migrations/by-verb.md +2 -0
  37. package/docs/target/index.md +1 -0
  38. package/docs/target/outside-tools.md +39 -0
  39. package/docs/workflow/skill-map.md +1 -0
  40. package/governance/detect.toml +4 -0
  41. package/governance/ledger/claude/rule.md +16 -0
  42. package/governance/ledger/skills/build-in-slices/SKILL.md +55 -0
  43. package/governance/ledger/skills/proposal-critique/SKILL.md +66 -0
  44. package/governance/ledger/skills/search-craft/SKILL.md +54 -0
  45. package/governance/ledger/skills/systematic-debugging/SKILL.md +64 -0
  46. package/governance/ledger/skills/test-first/SKILL.md +28 -0
  47. package/governance/ledger/skills/write-human/SKILL.md +62 -0
  48. package/governance/ledger/skills/write-human/density.md +36 -0
  49. package/governance/ledger/skills/write-human/machine-tells.md +30 -0
  50. package/governance/rules/claude/rule.md +13 -0
  51. package/package.json +1 -1
  52. package/scripts/ci/docs-only.sh +24 -0
  53. package/scripts/ci/refresh-frames-pr.sh +11 -12
  54. package/src/audits/catalog.ts +7 -2
  55. package/src/capture/stamp.ts +3 -5
  56. package/src/claude/cases/misc.ts +14 -0
  57. package/src/claude/skills-audit.ts +154 -8
  58. package/src/cli/help.ts +1 -1
  59. package/src/commands/claude.ts +6 -2
  60. package/src/commands/gov.ts +12 -7
  61. package/src/commands/migrate.ts +6 -2
  62. package/src/commands/pr.ts +110 -13
  63. package/src/commands/take.ts +36 -7
  64. package/src/counts/catalogs.ts +3 -5
  65. package/src/gate/measures.ts +2 -145
  66. package/src/gate/sequencer.ts +7 -1
  67. package/src/gate/stages.ts +36 -47
  68. package/src/gov/audit.ts +59 -13
  69. package/src/markdown/scope.ts +4 -0
  70. package/src/migrate/session-defaults.ts +114 -4
  71. package/src/pr/checks.ts +75 -3
  72. package/src/pr/evidence.ts +62 -1
  73. package/src/pr/lift.ts +23 -2
  74. package/src/pr/merge.ts +13 -3
  75. package/src/take/encode.ts +9 -1
  76. package/src/take/film.ts +17 -2
  77. package/src/take/fixtures/busy.html +107 -0
  78. package/src/take/perform.ts +99 -25
  79. package/src/take/screencast.ts +4 -3
  80. package/src/take/stepper.ts +33 -20
  81. package/src/targets/pulls.ts +42 -0
  82. package/tooling/claude/reference.md +4 -2
  83. package/tooling/claude/seeds/.claude/hooks/plan-guard.sh +239 -0
  84. package/tooling/claude/seeds/.claude/settings.json +8 -0
  85. package/claude/skills/ui-film/references/pr-clip.md +0 -108
  86. package/scripts/core/regen-hero.ts +0 -113
  87. package/src/capture/frame-values.ts +0 -326
@@ -1,326 +0,0 @@
1
- import { FAVICON_COLORS, renderFavicon } from '@/design/favicon'
2
- import type { GovCatalog } from '@/gov/list'
3
- import type { StackSummary } from '@/tooling/list'
4
-
5
- export const LISTED = 10
6
-
7
- /**
8
- * The first seven are the method and exist nowhere else. The last three are
9
- * recognizable on sight and are what keeps a column of chosen names from
10
- * reading as an all-`claude-` catalog, which is the failure the sampler was
11
- * written against.
12
- */
13
- export const FEATURED_SKILLS = [
14
- 'plan-feature',
15
- 'plan-groundwork',
16
- 'plan-intake',
17
- 'role-orchestrator',
18
- 'auto-ship',
19
- 'review-pr',
20
- 'task-board',
21
- 'git-ship',
22
- 'target-setup',
23
- 'systematic-debugging',
24
- ]
25
-
26
- /**
27
- * A terminal frame sets `white-space: pre` at a fixed window width, so an
28
- * overlong cell is clipped by the window edge rather than wrapped.
29
- */
30
- const GLOB_WIDTH = 44
31
-
32
- /** A refusal the script prints as `regen-hero: <message>` and exits 1 on. */
33
- export class FrameError extends Error {}
34
-
35
- export interface FrameCatalogs {
36
- readonly skills: readonly string[]
37
- readonly gov: GovCatalog
38
- readonly toolingStacks: readonly StackSummary[]
39
- readonly commandCount: number
40
- readonly tokenCss: string
41
- readonly markSvg: string
42
- }
43
-
44
- export interface Frame {
45
- readonly template: string
46
- readonly out: string
47
- readonly tokens: string
48
- }
49
-
50
- export interface FrameValues {
51
- readonly values: Readonly<Record<string, string>>
52
- readonly frames: (templates: readonly string[]) => Frame[]
53
- }
54
-
55
- const escape = (value: string): string =>
56
- value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
57
-
58
- /**
59
- * Even spacing across the sorted catalog rather than its first N, so a column
60
- * whose names share a prefix does not read as a narrower catalog than the one
61
- * that ships. Rules sample. Skills do not, because the even step
62
- * lands on the entries closest to commodity and on none of the workflow the
63
- * toolkit exists to carry.
64
- */
65
- export function sample<T>(items: readonly T[], listed = LISTED): T[] {
66
- if (items.length <= listed) return [...items]
67
- const step = items.length / listed
68
- return Array.from({ length: listed }, (_, i) => items[Math.floor(i * step)])
69
- }
70
-
71
- /**
72
- * A chosen name is maintained by hand where a sample never goes stale, so a
73
- * rename has to fail the run rather than quietly shrink the column. The length
74
- * is asserted because the `+N more` figure counts down from `LISTED` rather
75
- * than from what this returns.
76
- */
77
- export function featured(
78
- names: readonly string[],
79
- chosen: readonly string[],
80
- listed = LISTED,
81
- ): string[] {
82
- if (chosen.length !== listed) {
83
- throw new FrameError(`${chosen.length} featured skills, expected ${listed}`)
84
- }
85
- const catalog = new Set(names)
86
- const missing = chosen.filter((name) => !catalog.has(name))
87
- if (missing.length > 0) {
88
- throw new FrameError(
89
- `featured skills missing from the catalog: ${missing.join(', ')}`,
90
- )
91
- }
92
- return [...chosen]
93
- }
94
-
95
- /**
96
- * Truncating keeps the longest row inside the capture, and the ellipsis stops a
97
- * clipped value from reading as the whole value.
98
- */
99
- export function clip(value: string, width: number): string {
100
- return value.length <= width ? value : `${value.slice(0, width - 1)}…`
101
- }
102
-
103
- /**
104
- * Padding happens before escaping, since an entity is longer than the
105
- * character it replaces and would push a column out of line.
106
- */
107
- export function pad(value: string, width: number): string {
108
- return value + ' '.repeat(Math.max(0, width - value.length))
109
- }
110
-
111
- const markup = (names: readonly string[]): string =>
112
- names
113
- .map((name) => ` <div class="entry">${escape(name)}</div>`)
114
- .join('\n')
115
-
116
- const remaining = (names: readonly unknown[], listed = LISTED): string =>
117
- String(Math.max(0, names.length - listed))
118
-
119
- const plural = (count: number, noun: string): string =>
120
- `${count} ${noun}${count === 1 ? '' : 's'}`
121
-
122
- const frameRow = (cells: string): string =>
123
- `<span class="frame">│</span> <span class="ok">✓</span> ${cells}`
124
-
125
- /** Indented to the depth a rule inside the `<style>` block sits at. */
126
- const indentTokens = (css: string): string =>
127
- css
128
- .trimEnd()
129
- .split('\n')
130
- .map((line) => (line === '' ? '' : ` ${line}`))
131
- .join('\n')
132
-
133
- /**
134
- * The frames rendered in both themes. No README shows the install pair now,
135
- * and it stays for the landing page redesign to pick from. The hero is a
136
- * landing-page frame, so it stays dark.
137
- */
138
- const LIGHT_TEMPLATES = new Set(['install.html.tmpl'])
139
-
140
- function lightTokens(tokenCss: string): string {
141
- const roles = [...tokenCss.matchAll(/--color-light-([a-z-]+):/g)].map(
142
- (match) => match[1],
143
- )
144
- if (roles.length === 0) {
145
- throw new FrameError(
146
- 'token css carries no --color-light-* roles, refusing to write a light frame',
147
- )
148
- }
149
- return [
150
- tokenCss,
151
- '',
152
- ' :root {',
153
- ...roles.map(
154
- (role) => ` --color-${role}: var(--color-light-${role});`,
155
- ),
156
- ' --frame-shadow: rgb(0 0 0 / 16%);',
157
- ' }',
158
- ].join('\n')
159
- }
160
-
161
- /** The favicon the web page serves, so the hero's tab matches it rather than the accent. */
162
- function faviconUri(markSvg: string): string {
163
- try {
164
- return `data:image/svg+xml,${encodeURIComponent(renderFavicon(markSvg, FAVICON_COLORS))}`
165
- } catch (error) {
166
- throw new FrameError(
167
- `assets/brand/mark.svg cannot be drawn as a favicon: ${error instanceof Error ? error.message : String(error)}`,
168
- )
169
- }
170
- }
171
-
172
- /**
173
- * Builds the value map every template takes and the frame list a template set
174
- * expands to. An empty array is well-formed, and the in-process readers return
175
- * one quietly for a missing tree, so every catalog is refused when empty
176
- * rather than shipping a frame claiming the domain has nothing in it.
177
- */
178
- export function buildFrameValues(catalogs: FrameCatalogs): FrameValues {
179
- const { gov, toolingStacks, skills } = catalogs
180
- const markSvg = catalogs.markSvg.trim()
181
- if (!markSvg) {
182
- throw new FrameError(
183
- 'assets/brand/mark.svg read empty, refusing to write a hero with no mark',
184
- )
185
- }
186
- if (!catalogs.commandCount) {
187
- throw new FrameError(
188
- 'the commands catalog read no commands, refusing to write a zeroed hero',
189
- )
190
- }
191
-
192
- const tokenCss = indentTokens(catalogs.tokenCss)
193
- const rules = gov.rules.map((entry) => entry.name)
194
- // A rule no stack names is opt-in behind `--add`, so featuring one advertises
195
- // an entry no ordinary `canon gov install` delivers. The column samples the
196
- // stack-reached subset while its count and remainder describe the whole.
197
- const stacked = new Set(gov.stacks.flatMap((stack) => stack.rules))
198
- const deliveredEntries = gov.rules.filter((entry) => stacked.has(entry.name))
199
- const deliveredRules = deliveredEntries.map((entry) => entry.name)
200
-
201
- for (const [label, list] of [
202
- ['skills', skills],
203
- ['rules', rules],
204
- ['tooling stacks', toolingStacks],
205
- ['gov stacks', gov.stacks],
206
- ['stack-delivered rules', deliveredRules],
207
- ] as const) {
208
- if (list.length === 0) {
209
- throw new FrameError(
210
- `the ${label} catalog is empty, refusing to write a zeroed hero`,
211
- )
212
- }
213
- }
214
-
215
- const govStackRows = gov.stacks
216
- .map((stack) =>
217
- frameRow(
218
- `<span class="name">${escape(pad(stack.name, 17))}</span>` +
219
- `<span class="muted">${escape(pad(plural(stack.rules.length, 'rule'), 11))}</span>` +
220
- `<span class="muted">${escape(stack.extends ? `extends ${stack.extends}` : '')}</span>`,
221
- ),
222
- )
223
- .join('\n')
224
-
225
- // Each rule sits beside the glob that loads it rather than its description,
226
- // since the glob is the mechanism a reader cannot otherwise see. A rule
227
- // carrying no glob is not unscoped, it loads every session, and saying so is
228
- // the contrast that makes the column mean anything.
229
- const govRuleRows = sample(deliveredEntries)
230
- .map((entry) => {
231
- const globs = entry.paths ?? []
232
- const scope =
233
- globs.length > 0 ? clip(globs.join(' '), GLOB_WIDTH) : 'every session'
234
- return frameRow(
235
- `<span class="name">${escape(pad(entry.name, 24))}</span>` +
236
- `<span class="domain">${escape(pad(`[${entry.domain}]`, 12))}</span>` +
237
- `<span class="muted">${escape(scope)}</span>`,
238
- )
239
- })
240
- .join('\n')
241
-
242
- // A tooling stack is counted rather than listed, because what it lays down is
243
- // dev dependencies, run scripts, and ignore groups rather than named entries.
244
- // The inheritance is the part worth showing, since it is why a stack carrying
245
- // two of its own arrives with far more than two.
246
- const toolingStackRows = toolingStacks
247
- .map((stack) =>
248
- frameRow(
249
- `<span class="name">${escape(pad(stack.name, 14))}</span>` +
250
- `<span class="muted">${escape(pad(plural(stack.devDeps, 'dep'), 10))}</span>` +
251
- `<span class="muted">${escape(pad(plural(stack.scripts, 'script'), 12))}</span>` +
252
- `<span class="muted">${escape(stack.extends ? `extends ${stack.extends}` : '')}</span>`,
253
- ),
254
- )
255
- .join('\n')
256
-
257
- for (const [label, rows] of [
258
- ['governance stack rows', govStackRows],
259
- ['governance rule rows', govRuleRows],
260
- ['tooling stack rows', toolingStackRows],
261
- ] as const) {
262
- if (rows === '') {
263
- throw new FrameError(
264
- `the ${label} rendered empty, refusing to write a blank frame`,
265
- )
266
- }
267
- }
268
-
269
- const values: Record<string, string> = {
270
- TOKENS: tokenCss,
271
- GOV_STACK_ROWS: govStackRows,
272
- GOV_RULE_ROWS: govRuleRows,
273
- TOOLING_STACK_ROWS: toolingStackRows,
274
- SKILL_COUNT: String(skills.length),
275
- RULE_COUNT: String(rules.length),
276
- GOV_STACK_COUNT: String(gov.stacks.length),
277
- TOOLING_STACK_COUNT: String(toolingStacks.length),
278
- COMMAND_COUNT: String(catalogs.commandCount),
279
- MARK_SVG: markSvg,
280
- MARK_PIP: FAVICON_COLORS.pip.dark,
281
- FAVICON: faviconUri(markSvg),
282
- SKILL_ENTRIES: markup(featured(skills, FEATURED_SKILLS)),
283
- RULE_ENTRIES: markup(sample(deliveredRules)),
284
- SKILL_MORE: remaining(skills),
285
- RULE_MORE: remaining(rules),
286
- }
287
-
288
- const light = lightTokens(tokenCss)
289
-
290
- return {
291
- values,
292
- frames: (templates) =>
293
- templates.flatMap((template) => {
294
- const base = template.replace(/\.html\.tmpl$/, '')
295
- const dark = { template, out: `${base}.html`, tokens: tokenCss }
296
- return LIGHT_TEMPLATES.has(template)
297
- ? [dark, { template, out: `${base}-light.html`, tokens: light }]
298
- : [dark]
299
- }),
300
- }
301
- }
302
-
303
- /**
304
- * Every template takes the same value map, so one carrying no count
305
- * placeholder simply resolves none of them. What a template must not do is name
306
- * a placeholder nothing fills.
307
- */
308
- export function fillTemplate(
309
- template: string,
310
- source: string,
311
- values: Readonly<Record<string, string>>,
312
- tokens: string,
313
- ): string {
314
- let html = source
315
- for (const [key, value] of Object.entries({ ...values, TOKENS: tokens })) {
316
- html = html.replaceAll(`{{${key}}}`, value)
317
- }
318
-
319
- const unresolved = html.match(/{{[A-Z_]+}}/g)
320
- if (unresolved) {
321
- throw new FrameError(
322
- `${template} carries unresolved placeholders ${[...new Set(unresolved)].join(', ')}`,
323
- )
324
- }
325
- return html
326
- }