@erclx/canon 5.23.0 → 5.25.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/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/backlog-triage/REQUIREMENT.md +2 -2
- package/claude/skills/backlog-triage/SKILL.md +19 -6
- package/claude/skills/canon-operator/REQUIREMENT.md +1 -0
- package/claude/skills/canon-operator/SKILL.md +5 -1
- package/claude/skills/canvas/REQUIREMENT.md +49 -0
- package/claude/skills/canvas/SKILL.md +106 -0
- package/claude/skills/design-extract/REQUIREMENT.md +1 -1
- package/claude/skills/draft-doc/REQUIREMENT.md +2 -1
- package/claude/skills/draft-doc/references/readme/agent-facing.md +59 -0
- package/claude/skills/draft-doc/references/readme/application.md +66 -0
- package/claude/skills/draft-doc/references/readme/cli.md +64 -0
- package/claude/skills/draft-doc/references/readme/library.md +57 -0
- package/claude/skills/draft-doc/references/readme/plugin.md +66 -0
- package/claude/skills/draft-doc/references/readme.md +33 -4
- package/claude/skills/draft-figure/REQUIREMENT.md +14 -3
- package/claude/skills/draft-figure/SKILL.md +24 -51
- package/claude/skills/draft-figure/references/architecture-set.md +35 -0
- package/claude/skills/draft-figure/references/hand-drawn.md +31 -0
- package/claude/skills/draft-figure/references/mermaid.md +55 -0
- package/claude/skills/git-followup/SKILL.md +17 -6
- package/claude/skills/git-pr/references/evidence.md +4 -2
- package/claude/skills/git-split/SKILL.md +1 -1
- package/claude/skills/index-lookup/SKILL.md +2 -2
- package/claude/skills/review-address/SKILL.md +6 -13
- package/claude/skills/review-address/references/rest-reads.md +43 -0
- package/claude/skills/review-craft/references/ui.md +7 -1
- package/claude/skills/review-pr/REQUIREMENT.md +1 -0
- package/claude/skills/review-pr/SKILL.md +9 -3
- package/claude/skills/review-pr/references/close-out.md +2 -2
- package/claude/skills/review-pr/references/post.md +17 -5
- package/claude/skills/review-pr/references/scope.md +10 -8
- package/claude/skills/review-ui/SKILL.md +12 -4
- package/claude/skills/role-orchestrator/references/orchestrator-cloud-launch.md +30 -1
- package/claude/skills/role-orchestrator/references/orchestrator-launch.md +4 -0
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +4 -4
- package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +6 -0
- package/claude/skills/role-orchestrator/scripts/poll.ts +90 -27
- package/claude/skills/role-orchestrator/scripts/repo.ts +3 -5
- package/claude/skills/role-orchestrator/scripts/watch.ts +4 -7
- package/docs/agents/canvas.md +6 -2
- package/docs/agents/commands.md +45 -45
- package/docs/agents/context-audit-checks.md +2 -2
- package/docs/agents/index.md +0 -1
- package/docs/agents/key-changes.md +3 -3
- package/docs/agents/pr-evidence.md +11 -2
- package/docs/agents/pr-reads.md +5 -4
- package/docs/target/projects.md +4 -4
- package/docs/workflow/ai-workflow.md +0 -1
- package/docs/workflow/skill-map.md +3 -3
- package/docs/workflow/visual-design-workflow.md +2 -1
- package/governance/renames.toml +0 -4
- package/governance/rules/standards/markdown.md +1 -0
- package/package.json +1 -2
- package/scripts/core/install-check.sh +1 -1
- package/src/audits/catalog.ts +2 -2
- package/src/canvas/client/inspector/field.tsx +200 -0
- package/src/canvas/client/inspector/fields.css +151 -0
- package/src/canvas/client/inspector/section.tsx +27 -0
- package/src/canvas/client/inspector/values.ts +60 -0
- package/src/canvas/client/inspector.tsx +192 -111
- package/src/canvas/client/shell.css +0 -94
- package/src/canvas/tokens.ts +9 -1
- package/src/claude/cases/misc.ts +5 -0
- package/src/claude/cases/workflow.ts +1 -1
- package/src/claude/seeds.ts +0 -1
- package/src/commands/canvas.ts +29 -0
- package/src/commands/design.ts +2 -64
- package/src/commands/pr.ts +219 -202
- package/src/context/audit.ts +7 -6
- package/src/design/budget.ts +149 -0
- package/src/design/document.ts +22 -10
- package/src/design/render.ts +11 -1
- package/src/design/tokens.ts +40 -61
- package/src/gate/measures.ts +49 -0
- package/src/gate/stages.ts +5 -1
- package/src/help.ts +2 -2
- package/src/migrate/skill-names.ts +7 -1
- package/src/pr/evidence.ts +25 -5
- package/src/pr/rest.ts +137 -0
- package/standards/canvas.md +112 -0
- package/standards/context.md +2 -1
- package/standards/design.md +13 -3
- package/standards/figures.md +0 -1
- package/standards/index.md +1 -1
- package/standards/markdown.md +1 -0
- package/standards/mermaid.md +1 -7
- package/tooling/claude/reference.md +0 -5
- package/tooling/claude/seeds/.claude/hooks/scratch-guard.sh +1 -1
- package/claude/skills/draft-diagram/REQUIREMENT.md +0 -45
- package/claude/skills/draft-diagram/SKILL.md +0 -177
- package/docs/agents/design-board.md +0 -33
- package/governance/rules/standards/diagrams.md +0 -14
- package/governance/rules/standards/mermaid.md +0 -15
- package/scripts/core/check-gallery-exclusion.sh +0 -50
- package/src/design/board.ts +0 -576
- package/standards/diagrams.md +0 -132
- package/tooling/claude/seeds/.claude/diagrams/index.md +0 -8
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The prose budget `standards/design.md` sets for a design record, and the
|
|
3
|
+
* counter the `design` gate stage reads it through.
|
|
4
|
+
*
|
|
5
|
+
* The limits live here and in the standard, never in the stage, so the stage
|
|
6
|
+
* names a failure without restating what it measured against.
|
|
7
|
+
*
|
|
8
|
+
* A row is excluded on the same test `table()` in `@/design/parse` reads one
|
|
9
|
+
* by, a line opening with a pipe, so a code span carrying a pipe or a cell
|
|
10
|
+
* carrying the uncertainty tag never reaches the count. A line opening with
|
|
11
|
+
* `<` is dropped the way `prose()` drops it, and so is a fenced block or an
|
|
12
|
+
* HTML comment spanning several lines, which `prose()` would read in part. A
|
|
13
|
+
* `###` subsection counts toward the `##` section holding it, since the
|
|
14
|
+
* standard fixes the sections and a subheading is no way past the cap.
|
|
15
|
+
*/
|
|
16
|
+
export const DESIGN_BUDGET = {
|
|
17
|
+
/** Words before the first `##` section, the title excluded. */
|
|
18
|
+
leadWords: 40,
|
|
19
|
+
/** Words in `## Personality`, held to one paragraph. */
|
|
20
|
+
personalityWords: 60,
|
|
21
|
+
personalityParagraphs: 1,
|
|
22
|
+
/** Bullets or paragraphs in any other section, beside its table. */
|
|
23
|
+
rules: 3,
|
|
24
|
+
/** Words in one of those rules, the proxy for one line. */
|
|
25
|
+
ruleWords: 25,
|
|
26
|
+
} as const
|
|
27
|
+
|
|
28
|
+
const PERSONALITY = 'Personality'
|
|
29
|
+
|
|
30
|
+
/** A list item at any depth, which starts a rule of its own. */
|
|
31
|
+
const BULLET = /^\s*(?:[-*+]|\d+[.)])\s+/
|
|
32
|
+
|
|
33
|
+
interface Section {
|
|
34
|
+
readonly heading: string | undefined
|
|
35
|
+
readonly lines: readonly string[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Every place the record exceeds the budget, one sentence each, in document
|
|
40
|
+
* order. An empty list is a record within budget, including an empty one.
|
|
41
|
+
*/
|
|
42
|
+
export function measureDesignProse(source: string): string[] {
|
|
43
|
+
const findings: string[] = []
|
|
44
|
+
|
|
45
|
+
for (const section of splitSections(source)) {
|
|
46
|
+
const rules = readRules(section.lines)
|
|
47
|
+
const total = rules.reduce((sum, rule) => sum + rule, 0)
|
|
48
|
+
|
|
49
|
+
if (section.heading === undefined) {
|
|
50
|
+
if (total > DESIGN_BUDGET.leadWords) {
|
|
51
|
+
findings.push(
|
|
52
|
+
`The lead holds ${total} words against a cap of ${DESIGN_BUDGET.leadWords}`,
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
} else if (section.heading === PERSONALITY) {
|
|
56
|
+
if (rules.length > DESIGN_BUDGET.personalityParagraphs) {
|
|
57
|
+
findings.push(
|
|
58
|
+
`${PERSONALITY} holds ${rules.length} paragraphs against a cap of ${DESIGN_BUDGET.personalityParagraphs}`,
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
if (total > DESIGN_BUDGET.personalityWords) {
|
|
62
|
+
findings.push(
|
|
63
|
+
`${PERSONALITY} holds ${total} words against a cap of ${DESIGN_BUDGET.personalityWords}`,
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
if (rules.length > DESIGN_BUDGET.rules) {
|
|
68
|
+
findings.push(
|
|
69
|
+
`${section.heading} holds ${rules.length} rules against a cap of ${DESIGN_BUDGET.rules}`,
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
rules.forEach((words, index) => {
|
|
73
|
+
if (words > DESIGN_BUDGET.ruleWords) {
|
|
74
|
+
findings.push(
|
|
75
|
+
`${section.heading} rule ${index + 1} holds ${words} words against a cap of ${DESIGN_BUDGET.ruleWords}`,
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return findings
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The lead under an undefined heading, then each `##` section in order. */
|
|
86
|
+
function splitSections(source: string): Section[] {
|
|
87
|
+
const sections: { heading: string | undefined; lines: string[] }[] = [
|
|
88
|
+
{ heading: undefined, lines: [] },
|
|
89
|
+
]
|
|
90
|
+
for (const line of source.split('\n')) {
|
|
91
|
+
const match = line.match(/^##\s+(.+?)\s*$/)
|
|
92
|
+
if (match) {
|
|
93
|
+
sections.push({ heading: match[1], lines: [] })
|
|
94
|
+
} else if (!/^#\s/.test(line)) {
|
|
95
|
+
sections[sections.length - 1].lines.push(line)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return sections
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The word count of each rule in a section. A rule opens on a bullet or on
|
|
103
|
+
* the first prose line after a break, and every following prose line joins it
|
|
104
|
+
* until a blank line, a table row, a heading, or the next bullet.
|
|
105
|
+
*/
|
|
106
|
+
function readRules(lines: readonly string[]): number[] {
|
|
107
|
+
const rules: number[] = []
|
|
108
|
+
let isOpen = false
|
|
109
|
+
let isFenced = false
|
|
110
|
+
let isCommented = false
|
|
111
|
+
|
|
112
|
+
for (const line of lines) {
|
|
113
|
+
const trimmed = line.trim()
|
|
114
|
+
if (isCommented) {
|
|
115
|
+
isCommented = !trimmed.includes('-->')
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
if (trimmed.startsWith('<!--')) {
|
|
119
|
+
isCommented = !trimmed.includes('-->', 4)
|
|
120
|
+
isOpen = false
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
if (trimmed.startsWith('```')) {
|
|
124
|
+
isFenced = !isFenced
|
|
125
|
+
isOpen = false
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
if (
|
|
129
|
+
isFenced ||
|
|
130
|
+
trimmed === '' ||
|
|
131
|
+
trimmed.startsWith('|') ||
|
|
132
|
+
trimmed.startsWith('#') ||
|
|
133
|
+
trimmed.startsWith('<')
|
|
134
|
+
) {
|
|
135
|
+
isOpen = false
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
const text = trimmed.replace(BULLET, '')
|
|
139
|
+
const count = text.split(/\s+/).filter(Boolean).length
|
|
140
|
+
if (BULLET.test(line) || !isOpen) {
|
|
141
|
+
rules.push(count)
|
|
142
|
+
isOpen = true
|
|
143
|
+
} else {
|
|
144
|
+
rules[rules.length - 1] += count
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return rules
|
|
149
|
+
}
|
package/src/design/document.ts
CHANGED
|
@@ -88,6 +88,20 @@ function borderRows(tokens: readonly BorderToken[]): string[][] {
|
|
|
88
88
|
})
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* The lead a reader meets before the first section. It points at the module
|
|
93
|
+
* and at the context entry, since the record carries values and one-line rules
|
|
94
|
+
* and every reason behind them lives in the entry.
|
|
95
|
+
*/
|
|
96
|
+
const LEAD = [
|
|
97
|
+
'Authoring guidance: `standards/design.md`. Rendered from `src/design/tokens.ts` by `canon design regen`, so edit the module and never this file.',
|
|
98
|
+
'Why each value was chosen lives in `canon/context/design/tokens.md`.',
|
|
99
|
+
].join(' ')
|
|
100
|
+
|
|
101
|
+
function bullets(rules: readonly string[]): string {
|
|
102
|
+
return rules.map((rule) => `- ${rule}`).join('\n')
|
|
103
|
+
}
|
|
104
|
+
|
|
91
105
|
function section(heading: string, ...blocks: string[]): string {
|
|
92
106
|
return [`## ${heading}`, ...blocks.filter((block) => block !== '')].join(
|
|
93
107
|
'\n\n',
|
|
@@ -98,21 +112,19 @@ export function renderDesignDocument(tokens: DesignTokens = TOKENS): string {
|
|
|
98
112
|
const body = [
|
|
99
113
|
'# Design',
|
|
100
114
|
'',
|
|
101
|
-
|
|
102
|
-
'',
|
|
103
|
-
tokens.preamble,
|
|
115
|
+
LEAD,
|
|
104
116
|
'',
|
|
105
117
|
section('Personality', tokens.personality),
|
|
106
118
|
'',
|
|
107
119
|
section(
|
|
108
120
|
'Color',
|
|
109
|
-
tokens.
|
|
121
|
+
bullets(tokens.colorRules),
|
|
110
122
|
formatTable(['Role', 'Intent', 'Value'], colorRows(tokens.color)),
|
|
111
123
|
),
|
|
112
124
|
'',
|
|
113
125
|
section(
|
|
114
126
|
'Typography',
|
|
115
|
-
tokens.
|
|
127
|
+
bullets(tokens.typographyRules),
|
|
116
128
|
formatTable(
|
|
117
129
|
['Role', 'Family', 'Weight', 'Size', 'Line height'],
|
|
118
130
|
typeRows(tokens.typography),
|
|
@@ -121,24 +133,24 @@ export function renderDesignDocument(tokens: DesignTokens = TOKENS): string {
|
|
|
121
133
|
'',
|
|
122
134
|
section(
|
|
123
135
|
'Spacing',
|
|
124
|
-
tokens.
|
|
136
|
+
bullets(tokens.spacingRules),
|
|
125
137
|
formatTable(['Step', 'Multiplier', 'Value'], spaceRows(tokens.spacing)),
|
|
126
138
|
),
|
|
127
139
|
'',
|
|
128
140
|
section(
|
|
129
141
|
'Borders',
|
|
130
|
-
tokens.
|
|
142
|
+
bullets(tokens.bordersRules),
|
|
131
143
|
formatTable(
|
|
132
144
|
['Role', 'Radius', 'Width', 'When used'],
|
|
133
145
|
borderRows(tokens.borders),
|
|
134
146
|
),
|
|
135
147
|
),
|
|
136
148
|
'',
|
|
137
|
-
section('Layout', tokens.layout),
|
|
149
|
+
section('Layout', bullets(tokens.layout)),
|
|
138
150
|
'',
|
|
139
|
-
section('Motion', tokens.motion),
|
|
151
|
+
section('Motion', bullets(tokens.motion)),
|
|
140
152
|
'',
|
|
141
|
-
section('Iconography', tokens.iconography),
|
|
153
|
+
section('Iconography', bullets(tokens.iconography)),
|
|
142
154
|
]
|
|
143
155
|
|
|
144
156
|
return `${body.join('\n')}\n`
|
package/src/design/render.ts
CHANGED
|
@@ -328,7 +328,17 @@ function sectionBorders(rows: Row[]): string {
|
|
|
328
328
|
return `<h2>Borders</h2>\n<table><thead><tr><th>Role</th><th>Radius</th><th>Width</th><th>When used</th><th>Sample</th></tr></thead><tbody>${body}</tbody></table>`
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
+
/** A section the record writes as one-line rules draws them as a list. */
|
|
332
|
+
const BULLET = /^[-*+]\s+/
|
|
333
|
+
|
|
331
334
|
function sectionLine(title: string, text: string): string {
|
|
332
335
|
if (!text) return ''
|
|
333
|
-
|
|
336
|
+
const lines = text.split('\n')
|
|
337
|
+
if (!lines.every((line) => BULLET.test(line))) {
|
|
338
|
+
return `<h2>${title}</h2>\n<p>${escape(text)}</p>`
|
|
339
|
+
}
|
|
340
|
+
const items = lines
|
|
341
|
+
.map((line) => `<li>${escape(line.replace(BULLET, ''))}</li>`)
|
|
342
|
+
.join('')
|
|
343
|
+
return `<h2>${title}</h2>\n<ul>${items}</ul>`
|
|
334
344
|
}
|
package/src/design/tokens.ts
CHANGED
|
@@ -59,21 +59,25 @@ export interface BorderToken {
|
|
|
59
59
|
readonly verify?: readonly ('radius' | 'width')[]
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Every prose slot is a list of one-line rules rather than a string, so a
|
|
64
|
+
* bullet is the unit the renderer emits and the prose budget counts, and a
|
|
65
|
+
* value needing a paragraph of why goes to `canon/context/design/tokens.md`.
|
|
66
|
+
*/
|
|
62
67
|
export interface DesignTokens {
|
|
63
68
|
readonly personality: string
|
|
64
69
|
readonly color: readonly ColorToken[]
|
|
65
|
-
readonly
|
|
70
|
+
readonly colorRules: readonly string[]
|
|
66
71
|
readonly typeScale: readonly TypeStep[]
|
|
67
72
|
readonly typography: readonly TypeToken[]
|
|
68
|
-
readonly
|
|
73
|
+
readonly typographyRules: readonly string[]
|
|
69
74
|
readonly spacing: readonly SpaceToken[]
|
|
70
|
-
readonly
|
|
75
|
+
readonly spacingRules: readonly string[]
|
|
71
76
|
readonly borders: readonly BorderToken[]
|
|
72
|
-
readonly
|
|
73
|
-
readonly layout: string
|
|
74
|
-
readonly motion: string
|
|
75
|
-
readonly iconography: string
|
|
76
|
-
readonly preamble: string
|
|
77
|
+
readonly bordersRules: readonly string[]
|
|
78
|
+
readonly layout: readonly string[]
|
|
79
|
+
readonly motion: readonly string[]
|
|
80
|
+
readonly iconography: readonly string[]
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
/** The monospace stack, taken by code and by the terminal frames a page embeds. */
|
|
@@ -94,35 +98,14 @@ const DARK_GROUNDS = ['background', 'surface'] as const
|
|
|
94
98
|
const LIGHT_GROUNDS = ['light-background', 'light-surface'] as const
|
|
95
99
|
|
|
96
100
|
export const TOKENS: DesignTokens = {
|
|
97
|
-
|
|
98
|
-
'
|
|
99
|
-
'',
|
|
100
|
-
'The values below are the system rather than a reading of one. Until 2026-09-01 this record transcribed two surfaces and agreed with nothing else, which is what made a change to it reach nobody. The slide theme, the token preview, and a teach workspace stylesheet now read the module this file is rendered from, so a value changed there changes what all three render.',
|
|
101
|
-
'',
|
|
102
|
-
'The two rendered captures read it as well. `scripts/core/regen-hero.ts` fills `assets/captures/hero.html.tmpl` and `assets/captures/install.html.tmpl` with what `canon design css --no-components` emits, so both frames now carry the custom properties rather than their own copies of the hex, and a value moved here moves what the next capture renders.',
|
|
103
|
-
'',
|
|
104
|
-
'The terminal framing is the one surface left holding its own values, and that is a decision rather than a gap. `scripts/lib/ui.sh` and `src/ui.ts` each spell six escape constants, and `canon/context/scripts/framing.md` records one color source per language with a check behind each, so generating a third spelling from here would break the rule those two checks enforce. What the record is still incomplete about is the other half of those six: `WHITE` and `GREY` name no role below, so the terminal palette is described here in part rather than in whole.',
|
|
105
|
-
].join('\n'),
|
|
106
|
-
|
|
107
|
-
personality: [
|
|
108
|
-
'Warm neutrals carry the frame under a single rust accent, set in Geist. The subject is a toolkit for people who read documents and diffs as much as they run commands, so the voice is a proportional one and the page reads as prose. One accent carries every count, link, and primary action. Promoting a second and third into structural roles is what reads as a generated interface, so the palette stays at one.',
|
|
109
|
-
'',
|
|
110
|
-
'Monospace is a role the page uses rather than its voice. The `code` role takes it, and so does any terminal frame a surface embeds, since those show what the shell printed and matching the shell is what makes them legible. Every other role is Geist.',
|
|
111
|
-
].join('\n'),
|
|
101
|
+
personality:
|
|
102
|
+
'Warm neutrals under one rust accent, set in Geist. The page reads as prose, and monospace appears only where a surface shows what a shell printed. One accent carries every count, link, and primary action.',
|
|
112
103
|
|
|
113
|
-
|
|
114
|
-
'
|
|
115
|
-
'',
|
|
116
|
-
'
|
|
117
|
-
|
|
118
|
-
'Success is the one of the three that does have a rendered equivalent, which is why it carries a hex. `assets/captures/install.html` marks every confirmed step with it, and the shell writes `ANSI 32` for the same role, so the two are one role in two registers rather than one value in two spellings. The hex is what the rendered surface picked and no reading claims the terminal renders that value. It declares `background` alone as its ground, since that is the only role it is drawn on, where every other dark text role is drawn on both. It is the one role below that is not derived.',
|
|
119
|
-
'',
|
|
120
|
-
"Every other role is derived rather than picked, solved in OKLCH by binary search for the lightness that hits a target contrast against its ground, using this module's own anchors. Six anchors are the whole system: ground lightness 0.985 light and 0.165 dark, neutral chroma 0.003 light and 0.004 dark, neutral hue 90 both, accent hue 22 light and 28 dark, accent chroma 0.13 light and 0.12 dark for a mark and 0.095 light and 0.09 dark for a fill. Only the mark step is rendered below, in `accent`. That token already stands in for a fill in practice: `assets/captures/hero.html`'s `.cmd` rule paints its whole background with `var(--color-accent)` and sets text on top, which is a fill use rather than a mark one. The fill chroma step has no token of its own yet, so a lower-saturation `accent-fill` would recolor that button rather than introduce a new consumer. Targets, which are inputs rather than results: text 13.5, body 8.6, secondary 5.6, muted 4.6, accent 5.2. Dark is its own anchor set rather than an inversion of light, and every arm measured in the groundwork behind this landed on identical neutral ratios, which is what made the accent choice a question about hue alone.",
|
|
121
|
-
'',
|
|
122
|
-
'`muted` and `light-muted` solve against `surface` rather than `background`. Both declare two grounds and the groundwork solved its target against one, so the recorded hex cleared 4.6 against `background` and read 4.40 dark, 4.24 light against `surface`, the tighter of the two since surface sits a step closer to its text color. The values here are re-solved for the same 4.6 target read against `surface` instead, which clears both: 4.58 dark, 4.56 light against `surface`, and 4.80 dark, 4.95 light against `background`.',
|
|
123
|
-
'',
|
|
124
|
-
'The accent is a quiet red at hue 22 light, 28 dark, chosen over a vivid red that read as an error state against a page reporting success, over indigo which carries less distinctiveness at hue 280 in tooling already dominated by that hue, and over rust at hue 42, a larger temperature shift than the problem required. The debt this accepts: `error` and `warning` now share a register with the most repeated element on every rendered surface, so both need differentiating by lightness or by an icon rather than by hue, and that work is unscheduled.',
|
|
125
|
-
].join('\n'),
|
|
104
|
+
colorRules: [
|
|
105
|
+
'One accent. Never promote a second color into a structural role.',
|
|
106
|
+
'Every text role clears WCAG AA at 4.5:1 against each ground it is drawn on.',
|
|
107
|
+
'`warning` and `error` exist only in the terminal, as ANSI codes.',
|
|
108
|
+
],
|
|
126
109
|
|
|
127
110
|
color: [
|
|
128
111
|
{
|
|
@@ -242,15 +225,11 @@ export const TOKENS: DesignTokens = {
|
|
|
242
225
|
},
|
|
243
226
|
],
|
|
244
227
|
|
|
245
|
-
|
|
246
|
-
'
|
|
247
|
-
'',
|
|
248
|
-
'
|
|
249
|
-
|
|
250
|
-
'A tagged cell is one no rendering surface exercises yet, which is a declaration the system has not tested rather than one it has.',
|
|
251
|
-
'',
|
|
252
|
-
'Two rules set tracking and no others touch it. The label role carries `0.05em`, and the display role tightens to `-0.01em`.',
|
|
253
|
-
].join('\n'),
|
|
228
|
+
typographyRules: [
|
|
229
|
+
'Geist for every role except `code`, which is monospace.',
|
|
230
|
+
'Tracking: `label` at `0.05em`, `display` at `-0.01em`, nothing else.',
|
|
231
|
+
'Sizes come from seven steps, `t0` to `t6`. A role takes one step.',
|
|
232
|
+
],
|
|
254
233
|
|
|
255
234
|
typeScale: [
|
|
256
235
|
{ step: 't0', size: '3.175rem' },
|
|
@@ -312,11 +291,9 @@ export const TOKENS: DesignTokens = {
|
|
|
312
291
|
},
|
|
313
292
|
],
|
|
314
293
|
|
|
315
|
-
|
|
316
|
-
'Seven steps
|
|
317
|
-
|
|
318
|
-
'The outer window padding of the capture frames, which the previous record carried as a three-value frame register, is not part of the scale. Those frames set their own padding in the template and read no step, so the register is retired here rather than mapped onto steps no frame uses.',
|
|
319
|
-
].join('\n'),
|
|
294
|
+
spacingRules: [
|
|
295
|
+
'Seven steps. `md` sits at 3.5 because a control needs 14 pixels.',
|
|
296
|
+
],
|
|
320
297
|
|
|
321
298
|
spacing: [
|
|
322
299
|
{ step: 'xs', multiplier: '1', value: '0.25rem' },
|
|
@@ -328,9 +305,9 @@ export const TOKENS: DesignTokens = {
|
|
|
328
305
|
{ step: '3xl', multiplier: '24', value: '6rem' },
|
|
329
306
|
],
|
|
330
307
|
|
|
331
|
-
|
|
332
|
-
'Every border is one pixel solid
|
|
333
|
-
]
|
|
308
|
+
bordersRules: [
|
|
309
|
+
'Every border is one pixel solid in `border`. Radius and width are independent.',
|
|
310
|
+
],
|
|
334
311
|
|
|
335
312
|
borders: [
|
|
336
313
|
{
|
|
@@ -372,17 +349,19 @@ export const TOKENS: DesignTokens = {
|
|
|
372
349
|
},
|
|
373
350
|
],
|
|
374
351
|
|
|
375
|
-
layout:
|
|
376
|
-
'Capture widths
|
|
352
|
+
layout: [
|
|
353
|
+
'Capture widths: 320, 768, 1280, 1536. 320 is the reflow floor.',
|
|
354
|
+
'Every width query in the generated stylesheets opens a range one of these widths reaches.',
|
|
355
|
+
],
|
|
377
356
|
|
|
378
|
-
motion:
|
|
379
|
-
'
|
|
357
|
+
motion: [
|
|
358
|
+
'None. No transition, animation, or keyframe on any rendered surface.',
|
|
359
|
+
],
|
|
380
360
|
|
|
381
361
|
iconography: [
|
|
382
|
-
'No icon library
|
|
383
|
-
'',
|
|
384
|
-
|
|
385
|
-
].join('\n'),
|
|
362
|
+
'No icon library. `assets/brand/mark.svg` is the one authored icon.',
|
|
363
|
+
'The terminal draws glyphs: `│ ├ ✓ ! ✗ + - ◆ ◇ ❯`.',
|
|
364
|
+
],
|
|
386
365
|
}
|
|
387
366
|
|
|
388
367
|
/** A role's value, or `undefined` where the record declares no such role. */
|
package/src/gate/measures.ts
CHANGED
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
PRACTICE_SKILLS,
|
|
23
23
|
SHIPPED_CORPUS,
|
|
24
24
|
} from '@/claude/skills-audit'
|
|
25
|
+
import { measureDesignProse } from '@/design/budget'
|
|
26
|
+
import { DESIGN_DOCUMENT } from '@/design/regen'
|
|
25
27
|
import { listRepositoryFiles } from '@/git-files'
|
|
26
28
|
import { ceilingFindings } from '@/markdown/ceiling'
|
|
27
29
|
import { resolveMarkdown } from '@/markdown/files'
|
|
@@ -325,6 +327,53 @@ export const canonicalRecords: Measure = async (ctx) => {
|
|
|
325
327
|
return { emissions }
|
|
326
328
|
}
|
|
327
329
|
|
|
330
|
+
/**
|
|
331
|
+
* The design records this repository holds, each read against the prose
|
|
332
|
+
* budget in `@/design/budget`. The worked example is read beside the record,
|
|
333
|
+
* since it is the one a target copies its shape from.
|
|
334
|
+
*/
|
|
335
|
+
const DESIGN_RECORDS = [
|
|
336
|
+
DESIGN_DOCUMENT,
|
|
337
|
+
join('examples', 'design', 'DESIGN.md'),
|
|
338
|
+
] as const
|
|
339
|
+
|
|
340
|
+
export const designProse: Measure = async (ctx) => {
|
|
341
|
+
const failures: string[] = []
|
|
342
|
+
const emissions: Emission[] = []
|
|
343
|
+
|
|
344
|
+
for (const rel of DESIGN_RECORDS) {
|
|
345
|
+
const path = join(ctx.root, rel)
|
|
346
|
+
if (!existsSync(path)) continue
|
|
347
|
+
let source: string
|
|
348
|
+
try {
|
|
349
|
+
source = readFileSync(path, 'utf8')
|
|
350
|
+
} catch {
|
|
351
|
+
return {
|
|
352
|
+
emissions: [],
|
|
353
|
+
unmeasured: `${rel} could not be read, so its prose was not counted.`,
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const findings = measureDesignProse(source)
|
|
357
|
+
if (findings.length > 0) {
|
|
358
|
+
failures.push(
|
|
359
|
+
`${findings.map((finding) => `${finding} in ${rel}`).join('. ')}.`,
|
|
360
|
+
)
|
|
361
|
+
} else {
|
|
362
|
+
emissions.push(info(`${rel} within the prose budget`))
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (failures.length > 0) {
|
|
367
|
+
return {
|
|
368
|
+
emissions: [],
|
|
369
|
+
failure: `${failures.join(' ')} Cut to the budget in standards/design.md, moving the why to the context entry.`,
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (emissions.length === 0)
|
|
373
|
+
emissions.push(info('No design record to measure'))
|
|
374
|
+
return { emissions }
|
|
375
|
+
}
|
|
376
|
+
|
|
328
377
|
/**
|
|
329
378
|
* Every skill body and reference against the no-dated-provenance rule, across
|
|
330
379
|
* both corpora `auditSkills` reads.
|
package/src/gate/stages.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
auditSet,
|
|
11
11
|
captureStamps,
|
|
12
12
|
clientCommandCitations,
|
|
13
|
+
designProse,
|
|
13
14
|
documentCeiling,
|
|
14
15
|
folderEchoedNames,
|
|
15
16
|
markdownBans,
|
|
@@ -234,7 +235,9 @@ export const STAGES: readonly Stage[] = [
|
|
|
234
235
|
// from `src/design/tokens.ts`, the base stylesheet from
|
|
235
236
|
// `src/design/neutral.ts`, and none is edited by hand. Four artifacts
|
|
236
237
|
// from two token sources is the cost of the token move, and a render step that has to run is only safe while something fails
|
|
237
|
-
// when it did not, which is this.
|
|
238
|
+
// when it did not, which is this. The record and the worked example are
|
|
239
|
+
// then counted against the prose budget, which the drift asserts cannot
|
|
240
|
+
// see since a regenerated paragraph agrees with its source.
|
|
238
241
|
id: 'design',
|
|
239
242
|
label: 'Design',
|
|
240
243
|
checks: [
|
|
@@ -277,6 +280,7 @@ export const STAGES: readonly Stage[] = [
|
|
|
277
280
|
failure:
|
|
278
281
|
'The tab icon drifted from the token source. Run bun run check and commit web/public/favicon.svg.',
|
|
279
282
|
},
|
|
283
|
+
{ kind: 'measure', measure: designProse },
|
|
280
284
|
],
|
|
281
285
|
success: 'Design source clean',
|
|
282
286
|
},
|
package/src/help.ts
CHANGED
|
@@ -36,7 +36,7 @@ export const COMMAND_GROUPS: { title: string; rows: CommandRow[] }[] = [
|
|
|
36
36
|
{
|
|
37
37
|
title: 'Author and render',
|
|
38
38
|
rows: [
|
|
39
|
-
['design [cmd]', 'Design system (render,
|
|
39
|
+
['design [cmd]', 'Design system (render, regen)'],
|
|
40
40
|
['slides [cmd]', 'Slide decks (render, list)'],
|
|
41
41
|
['capture [source]', 'Render HTML capture sources to PNG'],
|
|
42
42
|
['serve [dir]', 'Serve a directory on localhost, print the link'],
|
|
@@ -102,7 +102,7 @@ const EXAMPLES = [
|
|
|
102
102
|
'canon docs list --json',
|
|
103
103
|
'canon docs agents',
|
|
104
104
|
'canon design render',
|
|
105
|
-
'canon
|
|
105
|
+
'canon canvas list --json',
|
|
106
106
|
'canon slides render',
|
|
107
107
|
'canon slides list --json',
|
|
108
108
|
'canon capture assets/captures/install.html --selector .window --out assets/evidence',
|
|
@@ -40,6 +40,11 @@ import { defineRenameRules, type RenameRules } from '@/migrate/rename'
|
|
|
40
40
|
* from where the document lands. Every retired name lands on the one skill, so
|
|
41
41
|
* a target citing any of them resolves after a single sweep.
|
|
42
42
|
*
|
|
43
|
+
* `draft-diagram` merged into `draft-figure` the same way, its architecture
|
|
44
|
+
* views becoming one reference the figure skill reads. `claude-diagram` is
|
|
45
|
+
* retargeted onto `draft-figure` rather than left on the retired name, for the
|
|
46
|
+
* same reason `claude-ui-test` was.
|
|
47
|
+
*
|
|
43
48
|
* Every name takes two words. Ten of these would have landed as a bare single
|
|
44
49
|
* word under a plain strip, and a bare word such as `review` or `docs` is a
|
|
45
50
|
* substring of ordinary prose with no token left for a later sweep to find.
|
|
@@ -58,7 +63,7 @@ export const SKILL_NAME_MAP: Readonly<Record<string, string>> = {
|
|
|
58
63
|
'claude-address-review': 'review-address',
|
|
59
64
|
'claude-autoship': 'auto-ship',
|
|
60
65
|
'claude-design-extract': 'design-extract',
|
|
61
|
-
'claude-diagram': 'draft-
|
|
66
|
+
'claude-diagram': 'draft-figure',
|
|
62
67
|
'claude-docs': 'context-fold',
|
|
63
68
|
'docs-fold': 'context-fold',
|
|
64
69
|
'claude-feature': 'plan-feature',
|
|
@@ -91,6 +96,7 @@ export const SKILL_NAME_MAP: Readonly<Record<string, string>> = {
|
|
|
91
96
|
'draft-wireframes': 'draft-doc',
|
|
92
97
|
'draft-readme': 'draft-doc',
|
|
93
98
|
'draft-wiki': 'draft-doc',
|
|
99
|
+
'draft-diagram': 'draft-figure',
|
|
94
100
|
}
|
|
95
101
|
|
|
96
102
|
/**
|
package/src/pr/evidence.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/** The path segment that marks an image as evidence a pull request should compare. */
|
|
2
2
|
const EVIDENCE_SEGMENT = 'evidence'
|
|
3
3
|
|
|
4
|
+
/** The most images a comment carries and still opens its states, since past it the page is a wall of screenshots. */
|
|
5
|
+
const EVIDENCE_OPEN_LIMIT = 6
|
|
6
|
+
|
|
7
|
+
/** Length of the commit names the visible line shows, the width `git` abbreviates to. */
|
|
8
|
+
const SHORT_SHA_LENGTH = 7
|
|
9
|
+
|
|
4
10
|
/** Prefix of the trailing marker this module writes and reads back. */
|
|
5
11
|
const MARKER_PREFIX = '<!-- pr-evidence:'
|
|
6
12
|
|
|
@@ -120,10 +126,14 @@ export function evidenceMarker(head: string): string {
|
|
|
120
126
|
}
|
|
121
127
|
|
|
122
128
|
/**
|
|
123
|
-
* Renders the whole comment body: one
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
129
|
+
* Renders the whole comment body: one `<details>` block per state, a Base/Head
|
|
130
|
+
* row per case, and the trailing marker naming the head this body describes.
|
|
131
|
+
* Every state opens while the comment carries at most `EVIDENCE_OPEN_LIMIT`
|
|
132
|
+
* images, one per row for the head plus one per row not added, and every state
|
|
133
|
+
* closes past it, so a comment is never half open. A visible line under
|
|
134
|
+
* `## Evidence` names the base and head as short shas. Every image URL is
|
|
135
|
+
* pinned to a commit sha rather than a branch, so the comment keeps showing
|
|
136
|
+
* what it claimed even after the branch moves.
|
|
127
137
|
*
|
|
128
138
|
* A preview address opens the body, and with no states it is the whole body
|
|
129
139
|
* apart from the marker, so a pull request whose screenshots did not change
|
|
@@ -157,6 +167,14 @@ export function renderEvidenceBody(
|
|
|
157
167
|
return [...opening, ...closing, evidenceMarker(head)].join('\n')
|
|
158
168
|
}
|
|
159
169
|
|
|
170
|
+
const imageCount = states.reduce(
|
|
171
|
+
(total, entry) =>
|
|
172
|
+
total + entry.items.reduce((sum, item) => sum + (item.added ? 1 : 2), 0),
|
|
173
|
+
0,
|
|
174
|
+
)
|
|
175
|
+
const opener =
|
|
176
|
+
imageCount <= EVIDENCE_OPEN_LIMIT ? '<details open>' : '<details>'
|
|
177
|
+
|
|
160
178
|
const sections = states.map((entry) => {
|
|
161
179
|
const rows = entry.items.map((item) => {
|
|
162
180
|
const before = item.added
|
|
@@ -167,7 +185,7 @@ export function renderEvidenceBody(
|
|
|
167
185
|
})
|
|
168
186
|
|
|
169
187
|
return [
|
|
170
|
-
|
|
188
|
+
opener,
|
|
171
189
|
`<summary>${entry.state === '' ? 'evidence' : entry.state} (${entry.items.length})</summary>`,
|
|
172
190
|
'',
|
|
173
191
|
'| Case | Base | Head |',
|
|
@@ -182,6 +200,8 @@ export function renderEvidenceBody(
|
|
|
182
200
|
...opening,
|
|
183
201
|
'## Evidence',
|
|
184
202
|
'',
|
|
203
|
+
`**Base:** \`${base.slice(0, SHORT_SHA_LENGTH)}\` · **Head:** \`${head.slice(0, SHORT_SHA_LENGTH)}\``,
|
|
204
|
+
'',
|
|
185
205
|
...sections,
|
|
186
206
|
'',
|
|
187
207
|
...closing,
|