@erclx/canon 5.20.0 → 5.21.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 (38) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/auto-ship/references/verb-absent.md +1 -1
  3. package/claude/skills/docs-sync/REQUIREMENT.md +1 -1
  4. package/claude/skills/document-health/REQUIREMENT.md +1 -1
  5. package/claude/skills/draft-ready/REQUIREMENT.md +2 -1
  6. package/claude/skills/draft-ready/SKILL.md +6 -4
  7. package/claude/skills/markdown-propose/REQUIREMENT.md +3 -3
  8. package/claude/skills/markdown-propose/SKILL.md +1 -1
  9. package/docs/agents/commands.md +1 -0
  10. package/docs/agents/index.md +1 -0
  11. package/docs/agents/markdown-audit.md +1 -3
  12. package/docs/agents/slides.md +120 -0
  13. package/docs/agents/tasks-validate.md +1 -1
  14. package/docs/agents/tasks.md +1 -1
  15. package/docs/target/migrations.md +1 -1
  16. package/docs/workflow/ai-workflow.md +1 -1
  17. package/docs/workflow/skill-map.md +11 -13
  18. package/governance/rules/lib/330-testing-py.md +1 -1
  19. package/governance/rules/lib/336-testing-php.md +1 -1
  20. package/package.json +1 -1
  21. package/src/claude/cases/workflow.ts +4 -20
  22. package/src/commands/slides.ts +13 -3
  23. package/src/commands/tasks.ts +46 -2
  24. package/src/migrate/skill-names.ts +0 -7
  25. package/src/slides/convert/chart.ts +165 -0
  26. package/src/slides/convert/deck.ts +164 -0
  27. package/src/slides/convert/export.ts +217 -7
  28. package/src/slides/convert/master.ts +195 -0
  29. package/src/slides/convert/shapes.ts +1 -1
  30. package/src/slides/convert/walk.ts +187 -2
  31. package/src/tasks/record.ts +11 -1
  32. package/src/tasks/validate.ts +14 -0
  33. package/standards/tasks.md +1 -1
  34. package/tooling/astro/configs/card-src/pages/og-card.astro +2 -2
  35. package/claude/skills/docs-fold/REQUIREMENT.md +0 -25
  36. package/claude/skills/docs-fold/SKILL.md +0 -9
  37. package/claude/skills/standards-audit/REQUIREMENT.md +0 -33
  38. package/claude/skills/standards-audit/SKILL.md +0 -99
@@ -0,0 +1,195 @@
1
+ import type PptxGenJS from 'pptxgenjs'
2
+ import {
3
+ BAND_SLOTS,
4
+ type Band,
5
+ type BandSlot,
6
+ type DeckConfig,
7
+ } from '@/slides/convert/deck'
8
+ import { fontFace } from '@/slides/convert/shapes'
9
+
10
+ /**
11
+ * The deck's master, built from the project's own tokens as the page computed
12
+ * them rather than from the toolkit's design module. A slide that hides or
13
+ * overrides a band takes a master without that band, so editing the footer
14
+ * once in PowerPoint reaches every slide still on the master's own.
15
+ */
16
+
17
+ export interface DeckTheme {
18
+ readonly background: string
19
+ readonly ink: string
20
+ readonly muted: string
21
+ readonly accent: string
22
+ readonly face: string
23
+ }
24
+
25
+ /** What the first slide computed, read in the page by `readTheme`. */
26
+ export interface ThemeReading {
27
+ /** Each role's `--color-*` value as hex, absent where none is declared. */
28
+ readonly tokens: {
29
+ readonly background?: string
30
+ readonly text?: string
31
+ readonly muted?: string
32
+ readonly accent?: string
33
+ }
34
+ readonly body: { readonly background: string; readonly color: string }
35
+ readonly fontFamily: string
36
+ /** Every declared `--color-*` property in sheet order, for chart series. */
37
+ readonly roles: readonly { readonly name: string; readonly hex: string }[]
38
+ }
39
+
40
+ export interface MasterBands {
41
+ readonly header: boolean
42
+ readonly footer: boolean
43
+ }
44
+
45
+ /** Intrinsic pixel size, which sets the mark's aspect inside its box. */
46
+ export interface MarkSize {
47
+ readonly width: number
48
+ readonly height: number
49
+ }
50
+
51
+ const SLIDE_W = 13.333
52
+ const MARGIN = 0.5
53
+ const BAND_H = 0.4
54
+ const HEADER_Y = 0.2
55
+ const FOOTER_Y = 7.5 - 0.2 - BAND_H
56
+ const SLOT_W = (SLIDE_W - 2 * MARGIN) / 3
57
+ const NUMBER_W = 1
58
+ const MARK_BOX = { w: 1.2, h: BAND_H }
59
+ const BAND_PT = 10
60
+
61
+ /**
62
+ * A missing role falls back to what the slide's body computed, never to the
63
+ * toolkit's palette, which would put Canon's colors on another project's deck.
64
+ */
65
+ export function deckTheme(reading: ThemeReading): {
66
+ readonly theme: DeckTheme
67
+ readonly notices: readonly string[]
68
+ } {
69
+ const { tokens, body } = reading
70
+ const missing = (
71
+ [
72
+ ['background', tokens.background],
73
+ ['text', tokens.text],
74
+ ['muted', tokens.muted],
75
+ ['accent', tokens.accent],
76
+ ] as const
77
+ )
78
+ .filter(([, value]) => value === undefined)
79
+ .map(([role]) => `--color-${role}`)
80
+ return {
81
+ theme: {
82
+ background: tokens.background ?? body.background,
83
+ ink: tokens.text ?? body.color,
84
+ muted: tokens.muted ?? body.color,
85
+ accent: tokens.accent ?? body.color,
86
+ face: fontFace(reading.fontFamily),
87
+ },
88
+ notices:
89
+ missing.length === 0
90
+ ? []
91
+ : [
92
+ `no ${missing.join(' or ')} token, so the deck master takes the slide body ${missing.length === 1 ? 'color for it' : 'color for them'}`,
93
+ ],
94
+ }
95
+ }
96
+
97
+ export function masterName(bands: MasterBands): string {
98
+ const parts = [bands.header && 'header', bands.footer && 'footer'].filter(
99
+ Boolean,
100
+ )
101
+ return parts.length === 0 ? 'canon-bare' : `canon-${parts.join('-')}`
102
+ }
103
+
104
+ const SLOT_ALIGN: Record<BandSlot, PptxGenJS.HAlign> = {
105
+ left: 'left',
106
+ center: 'center',
107
+ right: 'right',
108
+ }
109
+
110
+ /**
111
+ * One text object per filled slot. The master draws these for a band it
112
+ * carries, and a slide overriding a band draws its own through the same call.
113
+ */
114
+ export function bandTexts(
115
+ band: Omit<Band, 'show'>,
116
+ kind: 'header' | 'footer',
117
+ theme: DeckTheme,
118
+ options: { readonly hasMark?: boolean } = {},
119
+ ): { readonly text: string; readonly options: PptxGenJS.TextPropsOptions }[] {
120
+ return BAND_SLOTS.flatMap((slot, index) => {
121
+ const text = band[slot]
122
+ if (!text) return []
123
+ const isMarkSlot = kind === 'header' && slot === 'right' && options.hasMark
124
+ return [
125
+ {
126
+ text,
127
+ options: {
128
+ x: MARGIN + index * SLOT_W,
129
+ y: kind === 'header' ? HEADER_Y : FOOTER_Y,
130
+ w: SLOT_W - (isMarkSlot ? MARK_BOX.w + 0.1 : 0),
131
+ h: BAND_H,
132
+ margin: 0,
133
+ align: SLOT_ALIGN[slot],
134
+ valign: 'middle',
135
+ fontFace: theme.face,
136
+ fontSize: BAND_PT,
137
+ color: kind === 'header' ? theme.ink : theme.muted,
138
+ },
139
+ },
140
+ ]
141
+ })
142
+ }
143
+
144
+ /** The right of the footer, which `deck.json` keeps free while numbers show. */
145
+ export function slideNumberProps(theme: DeckTheme): PptxGenJS.SlideNumberProps {
146
+ return {
147
+ x: SLIDE_W - MARGIN - NUMBER_W,
148
+ y: FOOTER_Y,
149
+ w: NUMBER_W,
150
+ h: BAND_H,
151
+ margin: 0,
152
+ align: 'right',
153
+ valign: 'middle',
154
+ fontFace: theme.face,
155
+ fontSize: BAND_PT,
156
+ color: theme.accent,
157
+ }
158
+ }
159
+
160
+ function markImage(path: string, size: MarkSize): PptxGenJS.ImageProps {
161
+ const scale = Math.min(MARK_BOX.w / size.width, MARK_BOX.h / size.height)
162
+ const w = Math.round(size.width * scale * 1000) / 1000
163
+ const h = Math.round(size.height * scale * 1000) / 1000
164
+ return { path, x: SLIDE_W - MARGIN - w, y: HEADER_Y, w, h }
165
+ }
166
+
167
+ export function buildMaster(
168
+ deck: DeckConfig,
169
+ theme: DeckTheme,
170
+ bands: MasterBands,
171
+ markSize: MarkSize = { width: 1, height: 1 },
172
+ ): PptxGenJS.SlideMasterProps {
173
+ const hasHeader = bands.header && deck.header.show
174
+ const hasFooter = bands.footer && deck.footer.show
175
+ const hasMark = deck.mark !== undefined
176
+ const objects: NonNullable<PptxGenJS.SlideMasterProps['objects']> = [
177
+ ...(deck.mark ? [{ image: markImage(deck.mark, markSize) }] : []),
178
+ ...(hasHeader
179
+ ? bandTexts(deck.header, 'header', theme, { hasMark }).map((text) => ({
180
+ text,
181
+ }))
182
+ : []),
183
+ ...(hasFooter
184
+ ? bandTexts(deck.footer, 'footer', theme).map((text) => ({ text }))
185
+ : []),
186
+ ]
187
+ return {
188
+ title: masterName(bands),
189
+ background: { color: theme.background },
190
+ objects,
191
+ ...(hasFooter && deck.slideNumbers
192
+ ? { slideNumber: slideNumberProps(theme) }
193
+ : {}),
194
+ }
195
+ }
@@ -150,7 +150,7 @@ const PT_PER_PX = 0.75
150
150
  const inches = (px: number): number => px / PX_PER_INCH
151
151
  const points = (px: number): number => px * PT_PER_PX
152
152
 
153
- function position(box: Rect): PptxGenJS.PositionProps {
153
+ export function position(box: Rect): PptxGenJS.PositionProps {
154
154
  return {
155
155
  x: inches(box.x),
156
156
  y: inches(box.y),
@@ -1,3 +1,5 @@
1
+ import type { ChartRecord } from '@/slides/convert/chart'
2
+ import type { ThemeReading } from '@/slides/convert/master'
1
3
  import type {
2
4
  BoxStyle,
3
5
  ElementRecord,
@@ -9,10 +11,34 @@ import type {
9
11
  TextRun,
10
12
  } from '@/slides/convert/shapes'
11
13
 
14
+ /** How one slide treats a band the deck master draws. */
15
+ export type BandOverride =
16
+ | { readonly kind: 'master' }
17
+ | { readonly kind: 'off' }
18
+ | {
19
+ readonly kind: 'override'
20
+ readonly left?: string
21
+ readonly center?: string
22
+ readonly right?: string
23
+ }
24
+
25
+ /** What a slide declares about itself on `<body>` and in its notes. */
26
+ export interface SlideMeta {
27
+ readonly notes?: string
28
+ readonly section?: string
29
+ readonly isHidden: boolean
30
+ readonly header: BandOverride
31
+ readonly footer: BandOverride
32
+ /** The `--color-text` the slide's `<body>` computes, as hex. */
33
+ readonly textToken?: string
34
+ }
35
+
12
36
  /** What one laid-out slide hands back across `page.evaluate`. */
13
37
  export interface WalkedSlide {
14
38
  readonly background: Rgba
15
39
  readonly records: ElementRecord[]
40
+ readonly charts: ChartRecord[]
41
+ readonly meta: SlideMeta
16
42
  }
17
43
 
18
44
  /** Set on every walked element, so a screenshot finds the one a record names. */
@@ -27,7 +53,8 @@ export const ID_ATTRIBUTE = 'data-canon-id'
27
53
  * Text gathers along the inline flow. A block holding text takes every run its
28
54
  * inline descendants carry, so `<strong>` inside `<p>` is one shape with two
29
55
  * runs, while a block-level child is walked on its own. Images, SVGs, and
30
- * tables consume their whole subtree.
56
+ * tables consume their whole subtree. Speaker notes in `<aside class="notes">`
57
+ * and a chart's `<figure data-chart>` are read whole and never drawn as shapes.
31
58
  *
32
59
  * Each walked element is tagged with `idAttribute` set to its record id.
33
60
  */
@@ -293,10 +320,41 @@ export function walkSlide(idAttribute: string): WalkedSlide {
293
320
 
294
321
  const SKIPPED = new Set(['script', 'style', 'template', 'noscript', 'link'])
295
322
 
323
+ const cellText = (element: Element): string =>
324
+ (element.textContent ?? '').replace(/\s+/g, ' ').trim()
325
+
326
+ /** One line per block, so a paragraph break survives and no tag does. */
327
+ const notesText = (element: Element): string => {
328
+ const clone = element.cloneNode(true)
329
+ if (!(clone instanceof Element)) return ''
330
+ for (const br of Array.from(clone.querySelectorAll('br'))) {
331
+ br.replaceWith('\n')
332
+ }
333
+ for (const block of Array.from(
334
+ clone.querySelectorAll('p, li, div, blockquote, h1, h2, h3, h4, h5, h6'),
335
+ )) {
336
+ block.append('\n')
337
+ }
338
+ return (clone.textContent ?? '')
339
+ .split('\n')
340
+ .map((line) => line.replace(/\s+/g, ' ').trim())
341
+ .filter(Boolean)
342
+ .join('\n')
343
+ }
344
+
345
+ const notes: string[] = []
346
+ const charts: ChartRecord[] = []
347
+
296
348
  for (const element of Array.from(document.body.querySelectorAll('*'))) {
297
349
  if (consumed.has(element)) continue
298
350
  const tag = element.tagName.toLowerCase()
299
351
  if (SKIPPED.has(tag)) continue
352
+ if (tag === 'aside' && element.classList.contains('notes')) {
353
+ consumeSubtree(element)
354
+ const text = notesText(element)
355
+ if (text) notes.push(text)
356
+ continue
357
+ }
300
358
  if (!element.checkVisibility({ visibilityProperty: true })) continue
301
359
 
302
360
  const id = records.length + 1
@@ -313,6 +371,27 @@ export function walkSlide(idAttribute: string): WalkedSlide {
313
371
  ids.set(element, id)
314
372
  element.setAttribute(idAttribute, String(id))
315
373
 
374
+ if (tag === 'figure' && element.hasAttribute('data-chart')) {
375
+ consumeSubtree(element)
376
+ records.push({ ...base, kind: 'box' })
377
+ const table = element.querySelector('table')
378
+ const caption = element.querySelector('figcaption')
379
+ const title = caption ? cellText(caption) : ''
380
+ charts.push({
381
+ selector: base.selector,
382
+ box: bounds,
383
+ type: element.getAttribute('data-chart') ?? '',
384
+ hasLabels: element.hasAttribute('data-labels'),
385
+ ...(title ? { title } : {}),
386
+ rows: table
387
+ ? Array.from(table.rows).map((row) =>
388
+ Array.from(row.cells).map(cellText),
389
+ )
390
+ : [],
391
+ })
392
+ continue
393
+ }
394
+
316
395
  if (element instanceof HTMLImageElement) {
317
396
  records.push({
318
397
  ...base,
@@ -440,5 +519,111 @@ export function walkSlide(idAttribute: string): WalkedSlide {
440
519
  ? htmlGround
441
520
  : { hex: 'FFFFFF', alpha: 1 }
442
521
 
443
- return { background, records }
522
+ const body = document.body
523
+ const band = (kind: 'header' | 'footer'): BandOverride => {
524
+ if (body.getAttribute(`data-${kind}`) === 'off') return { kind: 'off' }
525
+ const slots: { left?: string; center?: string; right?: string } = {}
526
+ for (const slot of ['left', 'center', 'right'] as const) {
527
+ const value = body.getAttribute(`data-${kind}-${slot}`)
528
+ if (value !== null) slots[slot] = value
529
+ }
530
+ return Object.keys(slots).length > 0
531
+ ? { kind: 'override', ...slots }
532
+ : { kind: 'master' }
533
+ }
534
+ const section = body.getAttribute('data-section')?.trim()
535
+ const textValue = getComputedStyle(body)
536
+ .getPropertyValue('--color-text')
537
+ .trim()
538
+ const meta: SlideMeta = {
539
+ ...(textValue ? { textToken: rgba(textValue).hex } : {}),
540
+ ...(notes.length > 0 ? { notes: notes.join('\n\n') } : {}),
541
+ ...(section ? { section } : {}),
542
+ isHidden:
543
+ body.hasAttribute('data-hidden') &&
544
+ body.getAttribute('data-hidden') !== 'false',
545
+ header: band('header'),
546
+ footer: band('footer'),
547
+ }
548
+
549
+ return { background, records, charts, meta }
550
+ }
551
+
552
+ /**
553
+ * Runs inside the laid-out page and reads the colors and face the deck master
554
+ * takes. Tokens come from `<html>`'s computed style, so a slide that switches
555
+ * theme on its own `<body>`, such as a dark cover, leaves the master on the
556
+ * project's root theme. The body supplies the fallback colors and the face.
557
+ * Serialized to source like `walkSlide`, so its helpers sit in its own body.
558
+ */
559
+ export function readTheme(): ThemeReading {
560
+ const canvas = document.createElement('canvas')
561
+ canvas.width = 1
562
+ canvas.height = 1
563
+ const context = canvas.getContext('2d', { willReadFrequently: true })
564
+ const hex = (value: string): string | undefined => {
565
+ if (!context || !value || value === 'transparent') return undefined
566
+ context.clearRect(0, 0, 1, 1)
567
+ context.fillStyle = '#000000'
568
+ context.fillStyle = value
569
+ context.fillRect(0, 0, 1, 1)
570
+ const [r = 0, g = 0, b = 0, a = 0] = context.getImageData(0, 0, 1, 1).data
571
+ if (a === 0) return undefined
572
+ return [r, g, b]
573
+ .map((channel) => channel.toString(16).padStart(2, '0'))
574
+ .join('')
575
+ .toUpperCase()
576
+ }
577
+
578
+ const style = getComputedStyle(document.body)
579
+ const rootStyle = getComputedStyle(document.documentElement)
580
+ const token = (name: string): string | undefined => {
581
+ const value = rootStyle.getPropertyValue(name).trim()
582
+ return value ? hex(value) : undefined
583
+ }
584
+
585
+ const names: string[] = []
586
+ for (const sheet of Array.from(document.styleSheets)) {
587
+ let rules: CSSRuleList
588
+ try {
589
+ rules = sheet.cssRules
590
+ } catch {
591
+ continue
592
+ }
593
+ for (const rule of Array.from(rules)) {
594
+ if (!(rule instanceof CSSStyleRule)) continue
595
+ for (const name of Array.from(rule.style)) {
596
+ if (name.startsWith('--color-') && !names.includes(name)) {
597
+ names.push(name)
598
+ }
599
+ }
600
+ }
601
+ }
602
+
603
+ const tokens: {
604
+ background?: string
605
+ text?: string
606
+ muted?: string
607
+ accent?: string
608
+ } = {}
609
+ for (const role of ['background', 'text', 'muted', 'accent'] as const) {
610
+ const value = token(`--color-${role}`)
611
+ if (value) tokens[role] = value
612
+ }
613
+
614
+ return {
615
+ tokens,
616
+ body: {
617
+ background:
618
+ hex(style.backgroundColor) ??
619
+ hex(getComputedStyle(document.documentElement).backgroundColor) ??
620
+ 'FFFFFF',
621
+ color: hex(style.color) ?? '000000',
622
+ },
623
+ fontFamily: style.fontFamily,
624
+ roles: names.flatMap((name) => {
625
+ const value = token(name)
626
+ return value ? [{ name, hex: value }] : []
627
+ }),
628
+ }
444
629
  }
@@ -512,12 +512,18 @@ export async function recordPlan(
512
512
  * line, so the merge of an earlier slice cannot archive the task in the window
513
513
  * before this branch's own number is recorded. A rerun that ticks nothing adds
514
514
  * no marker, since its branch has already recorded or is already listed.
515
+ *
516
+ * `branchPulls` holds the pull request numbers the branch's own head carries.
517
+ * One already on the task's `Pull request:` line means the branch recorded its
518
+ * number, so a marker would outlive the branch with nothing left to clear it.
519
+ * An absent or empty list keeps the marker, which is the first slice's case.
515
520
  */
516
521
  export async function closeOutcomes(
517
522
  root: string,
518
523
  selector: RecordSelector,
519
524
  positions: readonly number[],
520
525
  branch?: string,
526
+ branchPulls: readonly number[] = [],
521
527
  ): Promise<CloseOutcome> {
522
528
  const opened = await openTask(root, selector)
523
529
  if ('ok' in opened) return opened
@@ -541,8 +547,12 @@ export async function closeOutcomes(
541
547
  )
542
548
  }
543
549
 
550
+ const recorded = readPullRequest(result.text).some((number) =>
551
+ branchPulls.includes(number),
552
+ )
553
+
544
554
  const text =
545
- result.closed.length > 0 && branch
555
+ result.closed.length > 0 && branch && !recorded
546
556
  ? writePendingBranch(result.text, branch).text
547
557
  : result.text
548
558
 
@@ -6,6 +6,7 @@ import {
6
6
  declinedDir,
7
7
  isReservedStem,
8
8
  readOutcomes,
9
+ readPendingBranches,
9
10
  readPlanTargets,
10
11
  readPullRequest,
11
12
  resolveLivePlan,
@@ -1065,6 +1066,10 @@ function nothing(): CitedResult {
1065
1066
  * about, and a task naming none leaves the row untested rather than settled,
1066
1067
  * because the only local signal left is the checkbox that produced the defect.
1067
1068
  *
1069
+ * A task still carrying a pending branch is untested whatever its recorded
1070
+ * pull request says, since a slice is in flight that has recorded no number yet
1071
+ * and the landed one may belong to an earlier slice.
1072
+ *
1068
1073
  * The outcome list comes off `readOutcomes` rather than a pattern of its own,
1069
1074
  * so this check cannot disagree with the archive and outcome verbs about which
1070
1075
  * checkboxes are outcomes and which sit inside a block a task displays.
@@ -1114,6 +1119,15 @@ async function checkCitedTask(
1114
1119
  const { open, closed } = readOutcomes(text)
1115
1120
  if (open.length > 0 || closed.length === 0) return nothing()
1116
1121
 
1122
+ const pending = readPendingBranches(text)
1123
+ if (pending.length > 0) {
1124
+ return untestedRow(
1125
+ group,
1126
+ subject,
1127
+ `waits on ${cited}, which closed every outcome but holds a pending branch (${pending.join(', ')}) that has yet to record its pull request.`,
1128
+ )
1129
+ }
1130
+
1117
1131
  // The last number is the newest slice, and an earlier one closed without
1118
1132
  // merging never lands, so only the last says whether the work reached trunk.
1119
1133
  const pullRequest = readPullRequest(text).at(-1)
@@ -141,7 +141,7 @@ One plan per task, or one per slice, which the board check reads as distinct pla
141
141
 
142
142
  `Pull request:` records which pull requests carry the task's work, each as a bare `#NNN` the way `Issue:` does. It lists every pull request that shipped part of the task, oldest first and separated by commas, as in `Pull request: #NNN, #NNN`, and a task shipped whole lists one. It is not an origin, so a task without one is well-formed. `git-pr` appends to it when a pull request opens.
143
143
 
144
- The line is what lets a merge close its own task. `canon tasks archive --pull-request <n>` finds the task by any number the line lists and archives it only on the last. One pull request, one task: two tasks naming the same number refuse to archive rather than both moving. `Pending branch: <branch>` holds that merge back while a branch that ticked an outcome waits on its own number, since `canon tasks outcome` writes it and `canon tasks pull-request` clears it.
144
+ The line is what lets a merge close its own task. `canon tasks archive --pull-request <n>` finds the task by any number the line lists and archives it only on the last. One pull request, one task: two tasks naming the same number refuse to archive rather than both moving. `Pending branch: <branch>` holds that merge back while a branch that ticked an outcome waits on its own number, since `canon tasks outcome` writes it and `canon tasks pull-request` clears it. `canon tasks outcome` skips the write when the branch's own pull request is already on the line.
145
145
 
146
146
  ## What goes in
147
147
 
@@ -20,12 +20,12 @@
20
20
  * `scripts/check-card-exclusion.sh` fails a build that carries the marker
21
21
  * below, which is what keeps the route out of the published site.
22
22
  */
23
- import '@/styles/global.css'
24
23
 
25
- // Retarget the import above when this project keeps its global stylesheet
24
+ // Point the import below at this project's global stylesheet when it lives
26
25
  // somewhere else. An unresolved path refuses the card server by name, which is
27
26
  // the loud direction: a card captured against no stylesheet renders as an
28
27
  // unstyled box and reports success.
28
+ import '@/styles/global.css'
29
29
  ---
30
30
 
31
31
  <html lang="en">
@@ -1,25 +0,0 @@
1
- ---
2
- name: docs-fold
3
- description: Why a pointer answers under the fold skill's old name, and the test that retires it
4
- ---
5
-
6
- # Docs fold requirement
7
-
8
- ## Gap
9
-
10
- Without this skill, a person typing the old name after the rename to `context-fold` meets an unknown command with nothing naming where the skill went. A target that has not run `canon migrate skill-names` still holds files telling someone to type it, and a live gitignored record written before the rename still names it, since the sweep reads the tracked listing only.
11
-
12
- ## Must
13
-
14
- - Name `context-fold` as the skill to run, so a typed invocation of the old name reaches the new one in one step
15
- - Carry `disable-model-invocation: true`, so routing never matches the old description against a request the new skill owns
16
-
17
- ## Must not
18
-
19
- - Carry any part of the fold procedure, which would give one behavior two bodies that drift apart
20
- - Retire on a release number. Retire it when `canon migrate skill-names --json` reports zero citations in every target the rollout reaches and no live `.canon/` record names the old name, since a release number retires it on a date nothing measured.
21
-
22
- ## Out of scope
23
-
24
- - The fold itself, which `context-fold` owns
25
- - Rewriting a stale citation, which `canon migrate skill-names` owns in a tracked file and the controlling session owns on the board
@@ -1,9 +0,0 @@
1
- ---
2
- name: docs-fold
3
- description: Renamed to `context-fold`. Answers a typed invocation of the old name by pointing at the new one.
4
- disable-model-invocation: true
5
- ---
6
-
7
- # Docs fold
8
-
9
- This skill is now `context-fold`. Run `/canon:context-fold` instead, and change any file that still names this one.
@@ -1,33 +0,0 @@
1
- ---
2
- name: standards-audit
3
- description: What the standards audit is for, the gaps it closes, and the fixing it deliberately refuses
4
- ---
5
-
6
- # Standards audit requirement
7
-
8
- ## Gap
9
-
10
- Without this skill, markdown ships unchecked against the authoring standards it was written under. A session fresh from writing prose does not reopen the standard it was meant to follow, banned punctuation survives because it reads fine, and the mapping from a file to the standards governing it gets re-derived by guess on every pass.
11
-
12
- ## Must
13
-
14
- - Scope the audit to markdown the project hand-authors, so a generated file is not reported against a standard nobody applies to it
15
- - Decide which standards govern each changed file, rather than auditing every file against every standard
16
- - Grep for the violations a standard makes mechanically detectable, because reading alone misses occurrences
17
- - Report each finding with a file and a line, grouped so one file is one fix pass
18
-
19
- ## Must not
20
-
21
- - Fix a violation, or propose the fix inline. The cheap swap satisfies the grep and breaks the rule the grep was standing in for.
22
- - Enumerate specific rules or tokens from a standard. The standard is the list, and a copy of it here drifts the moment the standard changes.
23
-
24
- ## Guards
25
-
26
- - No markdown changed since main: stop with a pass, not an error
27
- - No installed standards to audit against: stop and say where they come from
28
-
29
- ## Out of scope
30
-
31
- - Code style and structure, which `.claude/rules/` governs and Claude Code loads directly
32
- - Whether a standard is itself correct. A finding against the corpus is an argument to change the standard, and that is a standards edit rather than an audit result.
33
- - Persisting a report. The diff is the living state and the audit is a check against it at a moment.