@erclx/canon 4.101.0 → 4.103.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/auto-ship/SKILL.md +5 -6
- package/claude/skills/canon-cli/SKILL.md +4 -1
- package/claude/skills/canon-rollout/SKILL.md +7 -6
- package/claude/skills/draft-readme/REQUIREMENT.md +2 -0
- package/claude/skills/draft-readme/SKILL.md +9 -1
- package/claude/skills/git-ship/SKILL.md +3 -7
- package/claude/skills/memory-capture/SKILL.md +0 -6
- package/claude/skills/memory-review/REQUIREMENT.md +1 -1
- package/claude/skills/memory-review/SKILL.md +2 -3
- package/claude/skills/plan-groundwork/SKILL.md +3 -2
- package/claude/skills/target-check/REQUIREMENT.md +42 -0
- package/claude/skills/target-check/SKILL.md +91 -0
- package/claude/skills/target-check/references/domains.md +88 -0
- package/claude/skills/ui-checklist/SKILL.md +17 -0
- package/docs/agents/commands.md +1 -1
- package/docs/agents/install-and-sync.md +7 -1
- package/docs/agents/scripting.md +1 -1
- package/docs/target-projects.md +3 -1
- package/docs/workflow/ai-workflow.md +16 -13
- package/governance/rules/ci/700-ci-workflow.md +11 -0
- package/package.json +1 -1
- package/scripts/core/regen-web-favicon.ts +9 -40
- package/scripts/core/regen-web-previews.ts +1 -0
- package/src/claude/cases/setup.ts +8 -0
- package/src/commands/design.ts +8 -2
- package/src/commands/tooling.ts +92 -19
- package/src/design/css.ts +5 -2
- package/src/design/favicon.ts +33 -0
- package/src/design/render.ts +46 -6
- package/src/design/tokens.ts +1 -1
- package/standards/groundwork.md +8 -2
- package/standards/readme.md +22 -3
- package/tooling/astro/manifest.toml +1 -0
- package/tooling/base/manifest.toml +2 -2
- package/tooling/base/reference.md +1 -1
- package/tooling/nextjs/manifest.toml +1 -0
- package/tooling/web/configs/.github/workflows/readme-screenshot.yml +110 -0
- package/tooling/web/configs/e2e/screenshot.ts +69 -15
- package/tooling/web/configs/scripts/lib/preview-server.sh +31 -0
- package/tooling/web/configs/scripts/readme-screenshot.sh +26 -0
- package/tooling/web/configs/scripts/screenshot.sh +1 -24
- package/tooling/web/manifest.toml +1 -0
- package/tooling/web/reference.md +6 -1
|
@@ -65,7 +65,9 @@ The routing test is whether the repository can answer an item today. A session g
|
|
|
65
65
|
|
|
66
66
|
A groundwork track may run experiments to settle a question, writing a fixture it reads itself under `.canon/tmp/runs/groundwork-fixtures/<slug>/` and spawning up to three billed headless runs before it asks. A fixture a headless run is pointed at sits outside the repository, since a session started under the project root inherits that project's `CLAUDE.md` and rules and would measure them instead of the arm.
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
A spike starts against a sample, bounded on input size, duration, and spend, and scales up once the sample shows the method works. Nothing checks this, so it holds while a session reads the standard.
|
|
69
|
+
|
|
70
|
+
What a spike leaves behind splits on whether a later reader needs the file to check a claim. Bulk input, such as a large fixture, is re-runnable and cited by nothing, so the scratch path above is the right lifetime for it. Everything a reader opens to verify a result lives inside the track in one of three folders: `evidence/` for a recording or a render, `scripts/` for the arm scripts, and `clones/` for checkouts and copies of outside material a claim rests on. The scratch tree holds only what can be deleted without loss, which none of those is.
|
|
69
71
|
|
|
70
72
|
#### Session 2
|
|
71
73
|
|
|
@@ -166,7 +168,7 @@ The list stays written in the skill body as the fallback for a target whose inst
|
|
|
166
168
|
|
|
167
169
|
Capture leads rather than trails because a routed fact edits a tracked file, which has to reach the branch before the commit steps run.
|
|
168
170
|
|
|
169
|
-
|
|
171
|
+
The chain ends at capture, and review is run on its own.
|
|
170
172
|
|
|
171
173
|
Run `memory-review` standalone to curate the whole pen. An entry it retires moves to `.canon/memory/archive/` rather than being deleted, since a bulk pass has no undo.
|
|
172
174
|
|
|
@@ -260,7 +262,7 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
260
262
|
| `canon:git-commit` | When the staged set is one concern, or was staged hunk by hand |
|
|
261
263
|
| `canon:git-branch` | When a branch name needs generating or renaming to conventional form |
|
|
262
264
|
| `canon:git-pr` | When a pull request needs a title and body written from the diff |
|
|
263
|
-
| `canon:memory-review` |
|
|
265
|
+
| `canon:memory-review` | When the pen has grown, to propose where each entry belongs |
|
|
264
266
|
|
|
265
267
|
### After the pull request opens
|
|
266
268
|
|
|
@@ -287,16 +289,17 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
287
289
|
|
|
288
290
|
### Keep the project current with the toolkit
|
|
289
291
|
|
|
290
|
-
| Skill | When to use
|
|
291
|
-
| -------------------------------- |
|
|
292
|
-
| `canon:
|
|
293
|
-
| `canon:
|
|
294
|
-
| `canon:migration-
|
|
295
|
-
| `canon:migration-
|
|
296
|
-
| `canon:migration-
|
|
297
|
-
| `canon:
|
|
298
|
-
| `canon:canon-feedback-
|
|
299
|
-
| `canon:canon-
|
|
292
|
+
| Skill | When to use |
|
|
293
|
+
| -------------------------------- | -------------------------------------------------------------------------------------- |
|
|
294
|
+
| `canon:target-check` | In a target project, to report per domain what it holds against what the toolkit ships |
|
|
295
|
+
| `canon:seed-sync` | After a toolkit update, to reconcile installed seeds without losing customizations |
|
|
296
|
+
| `canon:migration-claude-md` | When `CLAUDE.md` grew past what always-load context should carry |
|
|
297
|
+
| `canon:migration-context` | When `docs/` holds agent-flavored files belonging in `canon/context/` |
|
|
298
|
+
| `canon:migration-superseded` | When a drift report names a `.claude/` file a folder has replaced |
|
|
299
|
+
| `canon:migration-standards-drop` | When the project still holds an installed `.claude/standards/` tree |
|
|
300
|
+
| `canon:canon-feedback-file` | When something in the toolkit is broken, missing, or off |
|
|
301
|
+
| `canon:canon-feedback-triage` | In the toolkit repo, to work through the open feedback issues |
|
|
302
|
+
| `canon:canon-rollout` | In the toolkit repo, to take one change out to every consuming project at once |
|
|
300
303
|
|
|
301
304
|
### Generate an artifact on demand
|
|
302
305
|
|
|
@@ -39,6 +39,17 @@ paths:
|
|
|
39
39
|
- Install with `bun install --frozen-lockfile`.
|
|
40
40
|
- Key the Playwright browser cache on the Playwright version string, never a static key.
|
|
41
41
|
|
|
42
|
+
## Commit-back
|
|
43
|
+
|
|
44
|
+
- Commit a refreshed file back to the pull request branch only when the branch's own change moved it. Report the diff without committing when something outside the branch can move it, such as a count read at build time or a font that renders differently per machine, since that commit lands on a branch that never asked for it.
|
|
45
|
+
- Commit only what a deterministic step regenerates from source, and run the diff check before the commit step so an unchanged capture pushes nothing.
|
|
46
|
+
- Skip a pull request from a fork with `github.event.pull_request.head.repo.full_name == github.repository`, since the default token cannot push to a fork's branch.
|
|
47
|
+
- Set the concurrency group on the ref so a newer push cancels the run committing onto a stale head.
|
|
48
|
+
- Set `HUSKY: 0` on the commit step when the runner lacks a tool a hook shells out to.
|
|
49
|
+
- Never commit back to the trunk. A commit with no pull request and no reviewer needs an operator's decision, not a workflow's.
|
|
50
|
+
- Leave `workflow_dispatch` off a commit-back workflow. A manual run has no branch to commit onto, so the trigger would pass and do nothing.
|
|
51
|
+
- Fail a template's first step while a path list still holds its placeholder, so an unfinished install fails loudly rather than never firing.
|
|
52
|
+
|
|
42
53
|
## Authority
|
|
43
54
|
|
|
44
55
|
- Load the `canon:ci-workflow` skill for the workflow template and the per-project adaptation. Report it rather than proceeding silently when the skill does not resolve.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Writes web/public/favicon.svg from assets/brand/mark.svg and the
|
|
3
|
-
*
|
|
2
|
+
* Writes web/public/favicon.svg from assets/brand/mark.svg and the favicon's
|
|
3
|
+
* own colors in src/design/favicon.ts, apart from the page accent tokens.
|
|
4
4
|
*
|
|
5
5
|
* The page is the fourth surface to carry the mark as a favicon and the only
|
|
6
6
|
* one that can answer for itself. `regen-hero.sh` and `src/design/render.ts`
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { writeFileSync } from 'node:fs'
|
|
19
19
|
|
|
20
|
+
import { FAVICON_COLORS, renderFavicon } from '../../src/design/favicon'
|
|
21
|
+
|
|
20
22
|
const root = new URL('../..', import.meta.url).pathname
|
|
21
23
|
|
|
22
24
|
const mark = (await Bun.file(`${root}assets/brand/mark.svg`).text()).trim()
|
|
@@ -25,43 +27,10 @@ if (!mark) {
|
|
|
25
27
|
process.exit(1)
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
if (!match) {
|
|
34
|
-
console.error(
|
|
35
|
-
`regen-web-favicon: tokens.css carries no --color-${name}, refusing to write a colorless favicon`,
|
|
36
|
-
)
|
|
37
|
-
process.exit(1)
|
|
38
|
-
}
|
|
39
|
-
return match[1] as string
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const dark = read('accent')
|
|
43
|
-
const light = read('light-accent')
|
|
44
|
-
|
|
45
|
-
// The shapes only, with the source's authoring comment dropped. Both fills are
|
|
46
|
-
// replaced by a rule rather than an attribute so one branch can flip both.
|
|
47
|
-
const shapes = mark
|
|
48
|
-
.replace(/<!--[\s\S]*?-->/, '')
|
|
49
|
-
.trim()
|
|
50
|
-
.replace(/^<svg[^>]*>/, '')
|
|
51
|
-
.replace(/<\/svg>$/, '')
|
|
52
|
-
.replaceAll(' fill="currentColor"', '')
|
|
53
|
-
.trim()
|
|
54
|
-
|
|
55
|
-
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 10 80 80">
|
|
56
|
-
<style>
|
|
57
|
-
path, rect { fill: ${light}; }
|
|
58
|
-
@media (prefers-color-scheme: dark) { path, rect { fill: ${dark}; } }
|
|
59
|
-
</style>
|
|
60
|
-
${shapes.replace(/\n\s*/g, '\n ')}
|
|
61
|
-
</svg>
|
|
62
|
-
`
|
|
63
|
-
|
|
64
|
-
writeFileSync(`${root}web/public/favicon.svg`, svg)
|
|
30
|
+
writeFileSync(
|
|
31
|
+
`${root}web/public/favicon.svg`,
|
|
32
|
+
renderFavicon(mark, FAVICON_COLORS),
|
|
33
|
+
)
|
|
65
34
|
console.log(
|
|
66
|
-
`regen-web-favicon: wrote web/public/favicon.svg (${light} / ${dark})`,
|
|
35
|
+
`regen-web-favicon: wrote web/public/favicon.svg (${FAVICON_COLORS.light} / ${FAVICON_COLORS.dark})`,
|
|
67
36
|
)
|
|
@@ -91,6 +91,14 @@ export const SETUP_CASES: readonly SkillCase[] = [
|
|
|
91
91
|
'Take this change out to every project that installed the toolkit and get each one to a pull request.',
|
|
92
92
|
expect: 'canon-rollout',
|
|
93
93
|
},
|
|
94
|
+
// Reporting per domain is what separates this from canon-operator, which
|
|
95
|
+
// diagnoses on the way to running the operation an intent named. This answers
|
|
96
|
+
// one fixed question across a fixed domain list and repairs nothing.
|
|
97
|
+
{
|
|
98
|
+
prompt:
|
|
99
|
+
'Report what this project has fallen behind the toolkit on, domain by domain, and fix nothing.',
|
|
100
|
+
expect: 'target-check',
|
|
101
|
+
},
|
|
94
102
|
{
|
|
95
103
|
prompt:
|
|
96
104
|
"This project needs its own coding rule that the toolkit doesn't ship.",
|
package/src/commands/design.ts
CHANGED
|
@@ -119,7 +119,11 @@ export function register(program: Command): void {
|
|
|
119
119
|
'Output directory',
|
|
120
120
|
creationRel(process.cwd(), SCRATCH, 'render', 'design'),
|
|
121
121
|
)
|
|
122
|
-
.
|
|
122
|
+
.option(
|
|
123
|
+
'--embed-fonts',
|
|
124
|
+
'Declare each vendored face the typography table names, and set the page body in the body stack',
|
|
125
|
+
)
|
|
126
|
+
.action((opts: { source: string; out: string; embedFonts?: boolean }) => {
|
|
123
127
|
const sourcePath = resolve(process.cwd(), opts.source)
|
|
124
128
|
const outDir = resolve(process.cwd(), opts.out)
|
|
125
129
|
const { GREEN, GREY, NC, RED, WHITE } = palette(process.stderr)
|
|
@@ -133,7 +137,9 @@ export function register(program: Command): void {
|
|
|
133
137
|
process.stderr.write(
|
|
134
138
|
`${GREY}┌${NC}\n${GREY}│${NC} ${WHITE}Render design tokens${NC}\n`,
|
|
135
139
|
)
|
|
136
|
-
const result = renderDesignDoc(sourcePath, outDir
|
|
140
|
+
const result = renderDesignDoc(sourcePath, outDir, {
|
|
141
|
+
embedFonts: opts.embedFonts,
|
|
142
|
+
})
|
|
137
143
|
process.stderr.write(
|
|
138
144
|
`${GREY}│${NC} ${GREEN}✓${NC} ${result.htmlPath}\n${GREY}│${NC} ${GREEN}✓${NC} ${result.cssPath}\n${GREY}└${NC}\n`,
|
|
139
145
|
)
|
package/src/commands/tooling.ts
CHANGED
|
@@ -39,6 +39,12 @@ interface SyncOptions {
|
|
|
39
39
|
readonly skip?: string
|
|
40
40
|
readonly check?: boolean
|
|
41
41
|
readonly write?: boolean
|
|
42
|
+
readonly diff?: { readonly json: boolean }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface DiffOptions {
|
|
46
|
+
readonly skip?: string
|
|
47
|
+
readonly json?: boolean
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
interface InjectOptions {
|
|
@@ -59,7 +65,7 @@ interface ListOptions {
|
|
|
59
65
|
|
|
60
66
|
type Prepared =
|
|
61
67
|
| { readonly ok: true; readonly chain: Manifest[]; readonly target: string }
|
|
62
|
-
| { readonly ok: false; readonly error: string }
|
|
68
|
+
| { readonly ok: false; readonly reason: string; readonly error: string }
|
|
63
69
|
|
|
64
70
|
export function register(program: Command): void {
|
|
65
71
|
const tooling = program
|
|
@@ -74,7 +80,10 @@ export function register(program: Command): void {
|
|
|
74
80
|
.argument('[target]', 'Target directory', '.')
|
|
75
81
|
.helpOption('-h, --help', 'Show this help message')
|
|
76
82
|
.option('--skip <stack>', 'Drop a layer from the extends chain')
|
|
77
|
-
.option(
|
|
83
|
+
.option(
|
|
84
|
+
'--check',
|
|
85
|
+
'Report what would change and write nothing, always exiting 0. Prefer `canon tooling diff`',
|
|
86
|
+
)
|
|
78
87
|
.option('--write', 'Apply every change without prompting')
|
|
79
88
|
.addHelpText(
|
|
80
89
|
'after',
|
|
@@ -98,6 +107,42 @@ export function register(program: Command): void {
|
|
|
98
107
|
},
|
|
99
108
|
)
|
|
100
109
|
|
|
110
|
+
tooling
|
|
111
|
+
.command('diff')
|
|
112
|
+
.description('Report how a target differs from a stack and write nothing')
|
|
113
|
+
.argument('[stack]', 'Tooling stack name (e.g. base, vite-react)')
|
|
114
|
+
.argument('[target]', 'Target directory', '.')
|
|
115
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
116
|
+
.option('--skip <stack>', 'Drop a layer from the extends chain')
|
|
117
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
118
|
+
.addHelpText(
|
|
119
|
+
'after',
|
|
120
|
+
[
|
|
121
|
+
'',
|
|
122
|
+
'The comparison `canon tooling sync --check` reports, under a name that',
|
|
123
|
+
'says it only asks. Exit codes:',
|
|
124
|
+
' 0 the target matches the stack',
|
|
125
|
+
' 1 a file, script, dependency, or gitignore entry differs, or a refusal',
|
|
126
|
+
'',
|
|
127
|
+
'`sync --check` reports the same list and always exits 0. Use `diff` to',
|
|
128
|
+
'gate CI, and keep `--check` for scripts that already call it.',
|
|
129
|
+
'',
|
|
130
|
+
'Examples:',
|
|
131
|
+
' canon tooling diff base',
|
|
132
|
+
' canon tooling diff base --json',
|
|
133
|
+
'',
|
|
134
|
+
].join('\n'),
|
|
135
|
+
)
|
|
136
|
+
.action(
|
|
137
|
+
async (stack: string | undefined, target: string, opts: DiffOptions) => {
|
|
138
|
+
process.exitCode = await runSync(stack, target, {
|
|
139
|
+
skip: opts.skip,
|
|
140
|
+
check: true,
|
|
141
|
+
diff: { json: opts.json === true },
|
|
142
|
+
})
|
|
143
|
+
},
|
|
144
|
+
)
|
|
145
|
+
|
|
101
146
|
tooling
|
|
102
147
|
.command('inject')
|
|
103
148
|
.description('Apply one stack to a target without scanning or prompting')
|
|
@@ -239,18 +284,27 @@ function prepare(stack: string, target: string, skip?: string): Prepared {
|
|
|
239
284
|
if (mismatch !== undefined) logWarn(mismatch)
|
|
240
285
|
|
|
241
286
|
if (!stackExists(PROJECT_ROOT, stack)) {
|
|
242
|
-
return {
|
|
287
|
+
return {
|
|
288
|
+
ok: false,
|
|
289
|
+
reason: 'unknown-stack',
|
|
290
|
+
error: `Stack not found: ${stack}`,
|
|
291
|
+
}
|
|
243
292
|
}
|
|
244
293
|
|
|
245
294
|
if (skip !== undefined) {
|
|
246
295
|
if (skip === stack) {
|
|
247
296
|
return {
|
|
248
297
|
ok: false,
|
|
298
|
+
reason: 'bad-skip',
|
|
249
299
|
error: `Cannot --skip the stack being synced: ${skip}`,
|
|
250
300
|
}
|
|
251
301
|
}
|
|
252
302
|
if (!stackExists(PROJECT_ROOT, skip)) {
|
|
253
|
-
return {
|
|
303
|
+
return {
|
|
304
|
+
ok: false,
|
|
305
|
+
reason: 'unknown-skip',
|
|
306
|
+
error: `Stack to skip not found: ${skip}`,
|
|
307
|
+
}
|
|
254
308
|
}
|
|
255
309
|
}
|
|
256
310
|
|
|
@@ -258,6 +312,7 @@ function prepare(stack: string, target: string, skip?: string): Prepared {
|
|
|
258
312
|
if (resolved === PROJECT_ROOT) {
|
|
259
313
|
return {
|
|
260
314
|
ok: false,
|
|
315
|
+
reason: 'toolkit-root',
|
|
261
316
|
error:
|
|
262
317
|
'Cannot run against toolkit root. Files here are the source of truth.',
|
|
263
318
|
}
|
|
@@ -275,40 +330,44 @@ async function runSync(
|
|
|
275
330
|
target: string,
|
|
276
331
|
opts: SyncOptions,
|
|
277
332
|
): Promise<number> {
|
|
278
|
-
intro('canon tooling sync')
|
|
333
|
+
intro(opts.diff === undefined ? 'canon tooling sync' : 'canon tooling diff')
|
|
279
334
|
|
|
280
335
|
if (opts.check === true && opts.write === true) {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
336
|
+
return refuse(
|
|
337
|
+
opts,
|
|
338
|
+
'conflicting-flags',
|
|
339
|
+
'Pass --check or --write, not both.',
|
|
340
|
+
)
|
|
284
341
|
}
|
|
285
342
|
|
|
286
343
|
const selected = stack ?? (await promptForStack())
|
|
287
344
|
if (selected === undefined) {
|
|
288
|
-
|
|
289
|
-
outro()
|
|
290
|
-
return 1
|
|
345
|
+
return refuse(opts, 'no-stacks', 'No tooling stacks found')
|
|
291
346
|
}
|
|
292
347
|
|
|
293
348
|
if (isStackExcluded(selected)) {
|
|
294
|
-
|
|
349
|
+
return refuse(
|
|
350
|
+
opts,
|
|
351
|
+
'excluded-stack',
|
|
295
352
|
'Claude is managed by `canon claude`, not `canon tooling`. Run `canon claude sync` instead.',
|
|
296
353
|
)
|
|
297
|
-
outro()
|
|
298
|
-
return 1
|
|
299
354
|
}
|
|
300
355
|
|
|
301
356
|
const prepared = prepare(selected, target, opts.skip)
|
|
302
|
-
if (!prepared.ok)
|
|
303
|
-
logWarn(prepared.error)
|
|
304
|
-
outro()
|
|
305
|
-
return 1
|
|
306
|
-
}
|
|
357
|
+
if (!prepared.ok) return refuse(opts, prepared.reason, prepared.error)
|
|
307
358
|
|
|
308
359
|
const result = scan(prepared.chain, prepared.target)
|
|
309
360
|
|
|
310
361
|
report(result)
|
|
311
362
|
|
|
363
|
+
if (opts.diff !== undefined) {
|
|
364
|
+
if (opts.diff.json) {
|
|
365
|
+
process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`)
|
|
366
|
+
}
|
|
367
|
+
outro()
|
|
368
|
+
return result.totalChanges === 0 ? 0 : 1
|
|
369
|
+
}
|
|
370
|
+
|
|
312
371
|
const mode = resolveWriteMode(opts)
|
|
313
372
|
const { GREEN, NC } = palette(process.stderr)
|
|
314
373
|
|
|
@@ -353,6 +412,20 @@ async function runSync(
|
|
|
353
412
|
return 0
|
|
354
413
|
}
|
|
355
414
|
|
|
415
|
+
/**
|
|
416
|
+
* A diff caller branches on the record rather than the exit, since a shell
|
|
417
|
+
* wrapper can flatten the status. Every refusal therefore answers with a reason
|
|
418
|
+
* on stdout under `--json`, beside the exit 1 that keeps a gate failing.
|
|
419
|
+
*/
|
|
420
|
+
function refuse(opts: SyncOptions, reason: string, message: string): number {
|
|
421
|
+
logWarn(message)
|
|
422
|
+
if (opts.diff?.json) {
|
|
423
|
+
process.stdout.write(`${JSON.stringify({ ok: false, reason, message })}\n`)
|
|
424
|
+
}
|
|
425
|
+
outro()
|
|
426
|
+
return 1
|
|
427
|
+
}
|
|
428
|
+
|
|
356
429
|
type WriteMode = 'apply' | 'prompt' | 'report' | 'unauthorized'
|
|
357
430
|
|
|
358
431
|
type ApplyDecision = 'apply' | 'cancelled' | 'reported' | 'unauthorized'
|
package/src/design/css.ts
CHANGED
|
@@ -136,14 +136,17 @@ ${component.rules}`,
|
|
|
136
136
|
* installed. Defaults to the mono stack's primary family; teach passes its own
|
|
137
137
|
* three faces instead of widening this default for every consumer.
|
|
138
138
|
*/
|
|
139
|
-
function fontFaceBlock(
|
|
139
|
+
export function fontFaceBlock(
|
|
140
|
+
faces: readonly FontFace[],
|
|
141
|
+
display: 'swap' | 'block' = 'swap',
|
|
142
|
+
): string {
|
|
140
143
|
return faces
|
|
141
144
|
.map(
|
|
142
145
|
(face) => `@font-face {
|
|
143
146
|
font-family: '${face.family}';
|
|
144
147
|
font-weight: ${face.weight};
|
|
145
148
|
font-style: normal;
|
|
146
|
-
font-display:
|
|
149
|
+
font-display: ${display};
|
|
147
150
|
src: url(data:font/woff2;base64,${face.base64}) format('woff2');
|
|
148
151
|
}`,
|
|
149
152
|
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface FaviconColors {
|
|
2
|
+
readonly light: string
|
|
3
|
+
readonly dark: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** The favicon's own fills, chosen for tab size and apart from the page accent tokens. */
|
|
7
|
+
export const FAVICON_COLORS: FaviconColors = {
|
|
8
|
+
light: '#c42938',
|
|
9
|
+
dark: '#e54e40',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The shapes only, with the source's authoring comment dropped. Both fills are
|
|
14
|
+
* replaced by a rule rather than an attribute so one branch can flip both.
|
|
15
|
+
*/
|
|
16
|
+
export function renderFavicon(mark: string, colors: FaviconColors): string {
|
|
17
|
+
const shapes = mark
|
|
18
|
+
.replace(/<!--[\s\S]*?-->/, '')
|
|
19
|
+
.trim()
|
|
20
|
+
.replace(/^<svg[^>]*>/, '')
|
|
21
|
+
.replace(/<\/svg>$/, '')
|
|
22
|
+
.replaceAll(' fill="currentColor"', '')
|
|
23
|
+
.trim()
|
|
24
|
+
|
|
25
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 10 80 80">
|
|
26
|
+
<style>
|
|
27
|
+
path, rect { fill: ${colors.light}; }
|
|
28
|
+
@media (prefers-color-scheme: dark) { path, rect { fill: ${colors.dark}; } }
|
|
29
|
+
</style>
|
|
30
|
+
${shapes.replace(/\n\s*/g, '\n ')}
|
|
31
|
+
</svg>
|
|
32
|
+
`
|
|
33
|
+
}
|
package/src/design/render.ts
CHANGED
|
@@ -2,6 +2,9 @@ import { mkdirSync, writeFileSync } from 'node:fs'
|
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import type { Cell, DesignDoc, Row } from '@/design/parse'
|
|
4
4
|
import { parseDesignDoc } from '@/design/parse'
|
|
5
|
+
import { fontFaceBlock } from '@/design/css'
|
|
6
|
+
import type { FontFace } from '@/design/fonts'
|
|
7
|
+
import { FONT_FACES } from '@/design/fonts'
|
|
5
8
|
import { colorValue } from '@/design/tokens'
|
|
6
9
|
|
|
7
10
|
export interface RenderResult {
|
|
@@ -19,19 +22,55 @@ const FAVICON_HREF = `data:image/svg+xml,${encodeURIComponent(
|
|
|
19
22
|
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 10 80 80"><path d="M34,20 L15,28 L15,72 L34,80 Z M66,20 L85,28 L85,72 L66,80 Z" fill="${FAVICON_COLOR}" /><rect x="44" y="15" width="12" height="70" rx="2" fill="${FAVICON_COLOR}" /></svg>`,
|
|
20
23
|
)}`
|
|
21
24
|
|
|
25
|
+
export interface RenderOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Declare each vendored face the typography table names, and let the page
|
|
28
|
+
* body take the body role's stack. Off by default, since a face is a large
|
|
29
|
+
* base64 block a target's render would otherwise grow by unasked.
|
|
30
|
+
*/
|
|
31
|
+
readonly embedFonts?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
export function renderDesignDoc(
|
|
23
35
|
sourcePath: string,
|
|
24
36
|
outDir: string,
|
|
37
|
+
options: RenderOptions = {},
|
|
25
38
|
): RenderResult {
|
|
26
39
|
const doc = parseDesignDoc(sourcePath)
|
|
27
40
|
mkdirSync(outDir, { recursive: true })
|
|
28
41
|
const cssPath = join(outDir, 'design.css')
|
|
29
42
|
const htmlPath = join(outDir, 'index.html')
|
|
30
|
-
|
|
31
|
-
writeFileSync(
|
|
43
|
+
const faces = options.embedFonts ? namedFaces(doc) : []
|
|
44
|
+
writeFileSync(cssPath, buildCss(doc, faces))
|
|
45
|
+
writeFileSync(htmlPath, buildHtml(doc, bodyFamily(doc, faces)))
|
|
32
46
|
return { htmlPath, cssPath }
|
|
33
47
|
}
|
|
34
48
|
|
|
49
|
+
/** The vendored faces whose family appears in a typography `Family` cell. */
|
|
50
|
+
function namedFaces(doc: DesignDoc): FontFace[] {
|
|
51
|
+
const named = new Set(
|
|
52
|
+
doc.typography.flatMap((row) =>
|
|
53
|
+
val(row, 'Family')
|
|
54
|
+
.split(',')
|
|
55
|
+
.map((family) => family.trim().replace(/^['"]|['"]$/g, '')),
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
return FONT_FACES.filter((face) => named.has(face.family))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A stack lands raw in a `<style>` block, where HTML escaping does nothing. */
|
|
62
|
+
const SAFE_STACK = /^[\w\s,'"-]+$/
|
|
63
|
+
|
|
64
|
+
/** The stack the body role names, once a face for it is embedded. */
|
|
65
|
+
function bodyFamily(doc: DesignDoc, faces: readonly FontFace[]): string {
|
|
66
|
+
if (!faces.length) return 'system-ui, sans-serif'
|
|
67
|
+
const row =
|
|
68
|
+
doc.typography.find((r) => slug(val(r, 'Role')) === 'body') ??
|
|
69
|
+
doc.typography[0]
|
|
70
|
+
const family = val(row, 'Family')
|
|
71
|
+
return SAFE_STACK.test(family) ? family : 'system-ui, sans-serif'
|
|
72
|
+
}
|
|
73
|
+
|
|
35
74
|
function slug(s: string): string {
|
|
36
75
|
return s
|
|
37
76
|
.toLowerCase()
|
|
@@ -105,7 +144,7 @@ function confidence(doc: DesignDoc): Confidence {
|
|
|
105
144
|
return { tagged, total }
|
|
106
145
|
}
|
|
107
146
|
|
|
108
|
-
function buildCss(doc: DesignDoc): string {
|
|
147
|
+
function buildCss(doc: DesignDoc, faces: readonly FontFace[]): string {
|
|
109
148
|
const lines: string[] = [':root {']
|
|
110
149
|
for (const row of doc.color) {
|
|
111
150
|
if (val(row, 'Value')) {
|
|
@@ -135,7 +174,8 @@ function buildCss(doc: DesignDoc): string {
|
|
|
135
174
|
}
|
|
136
175
|
}
|
|
137
176
|
lines.push('}')
|
|
138
|
-
|
|
177
|
+
const root = lines.join('\n') + '\n'
|
|
178
|
+
return faces.length ? `${fontFaceBlock(faces, 'block')}\n\n${root}` : root
|
|
139
179
|
}
|
|
140
180
|
|
|
141
181
|
function escape(s: string): string {
|
|
@@ -178,7 +218,7 @@ function previewChrome(): string {
|
|
|
178
218
|
return [' :root {', ...lines, ' }'].join('\n')
|
|
179
219
|
}
|
|
180
220
|
|
|
181
|
-
function buildHtml(doc: DesignDoc): string {
|
|
221
|
+
function buildHtml(doc: DesignDoc, bodyStack: string): string {
|
|
182
222
|
const sections = [
|
|
183
223
|
sectionPersonality(doc.personality),
|
|
184
224
|
sectionColor(doc.color),
|
|
@@ -205,7 +245,7 @@ function buildHtml(doc: DesignDoc): string {
|
|
|
205
245
|
<link rel="stylesheet" href="design.css">
|
|
206
246
|
<style>
|
|
207
247
|
${previewChrome()}
|
|
208
|
-
body { font-family:
|
|
248
|
+
body { font-family: ${bodyStack}; margin: 2rem; max-width: 960px; color: var(--preview-ink); background: var(--preview-paper); }
|
|
209
249
|
h1 { margin-top: 0; }
|
|
210
250
|
h2 { margin-top: 2rem; border-bottom: 1px solid var(--preview-rule); padding-bottom: 0.25rem; }
|
|
211
251
|
table { border-collapse: collapse; width: 100%; margin-top: 0.5rem; }
|
package/src/design/tokens.ts
CHANGED
|
@@ -377,7 +377,7 @@ export const TOKENS: DesignTokens = {
|
|
|
377
377
|
iconography: [
|
|
378
378
|
'No icon library is installed. `assets/brand/mark.svg` is the one authored icon, embedded inline in the hero topbar, and the surfaces otherwise draw literal glyph characters: `│ ├ ✓ ! ✗ + - ◆ ◇ ❯` for the terminal framing.',
|
|
379
379
|
'',
|
|
380
|
-
'The same mark ships
|
|
380
|
+
'The same mark ships in six independently-maintained copies that carry different colors by design rather than by drift, each fitted to the chrome it renders on: the dark accent for a dark-chrome surface, the light accent for a light-chrome one, and the pair the web favicon takes from `src/design/favicon.ts`. Unifying them or repairing the one that looks drifted would break the fit each was chosen for. `canon/context/design.md` carries which file holds each copy.',
|
|
381
381
|
].join('\n'),
|
|
382
382
|
}
|
|
383
383
|
|
package/standards/groundwork.md
CHANGED
|
@@ -130,9 +130,15 @@ Each spike carries four things:
|
|
|
130
130
|
|
|
131
131
|
Cost is a report rather than a limit, and it is what makes the next spike estimable before anyone commits to it. Record it even when it comes to a single read.
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
Start a spike against a sample and scale up only once the sample shows the method works. Bound the sample on all three of input size, duration, and spend, since fifty minutes of audio is a runtime and a bill before it is a file size, and three minutes of it usually shows whether the pipeline runs at all. The same holds for a dataset, an image set, or a batch of queries. Record the sample's bounds and the figure for the full input in the spike, so a reader can tell what the result was measured on and what the scale-up costs. The rule reaches only a track, since that is the surface that reads this standard, and nothing checks that a spike ran small first, so it holds only while a session reads it.
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
A spike also leaves files behind, and they split on whether a later reader needs the file to check a claim rather than on whether it is markdown or re-runnable. A file a reader opens to verify how a result was produced or what it showed lives inside the track. A file that is only bulk input, such as a large fixture or a copied asset, stays outside it wherever the surface driving the spike puts it. Three subfolders inside the track carry the first kind, and none takes a number, since numbering is the read order over the files a reader opens in sequence and these are reached from the claim that cites them instead:
|
|
136
|
+
|
|
137
|
+
- `evidence/`: what a run produced and the record cites, being a recording, a render, or a frame pulled from one, beside the file citing it.
|
|
138
|
+
- `scripts/`: the arm scripts and harnesses a spike ran. An arm script is re-runnable, and it is also what a reader opens to check the method, so this refines the earlier split, which sorted on re-runnable alone, and does not contradict it.
|
|
139
|
+
- `clones/`: checkouts and copies of outside material a claim rests on, kept small enough to hold in the folder. A checkout too large to keep is cited by its address and commit instead.
|
|
140
|
+
|
|
141
|
+
`spikes/`, `web/`, and `research/` were considered and folded into these, so do not reopen them. A spike's files sit under the three above, and a fetched page belongs in `clones/`. The `web/` case is the arguable one, since a screenshot of a reference site could sit apart, and it folds into `evidence/` because that is where a cited artifact already lives.
|
|
136
142
|
|
|
137
143
|
Reach for a test harness the project already carries before building one. A track needing an experiment no existing harness can express has found a finding, and it belongs in the folder rather than in a new abstraction.
|
|
138
144
|
|
package/standards/readme.md
CHANGED
|
@@ -11,7 +11,7 @@ The reader is what changes. Reference prose serves someone who already committed
|
|
|
11
11
|
|
|
12
12
|
## Scope
|
|
13
13
|
|
|
14
|
-
Governs every `README.md`: voice, heading structure, required and optional sections, badge selection, and what the page links out to instead of carrying.
|
|
14
|
+
Governs every `README.md`: voice, heading structure, required and optional sections, the header block, badge selection, and what the page links out to instead of carrying.
|
|
15
15
|
|
|
16
16
|
Does not govern:
|
|
17
17
|
|
|
@@ -34,14 +34,14 @@ Scoped to the README at a repository root. A nested README documenting a folder,
|
|
|
34
34
|
|
|
35
35
|
- H1 title, H2 major sections, H3 subsections. Maintain proper hierarchy for GitHub's auto-generated table of contents.
|
|
36
36
|
- Use sentence case for all headings (proper nouns and product names retain their casing)
|
|
37
|
-
- Project description in plain text directly under H1. Keep it to 2-3 sentences.
|
|
37
|
+
- Project description in plain text below the header block, or directly under the H1 when the block holds nothing beneath the title. Keep it to 2-3 sentences.
|
|
38
38
|
- Do not create deeply nested heading structures that harm scannability
|
|
39
39
|
- Do not use horizontal rules or dividers (`---`)
|
|
40
40
|
|
|
41
41
|
## Sections
|
|
42
42
|
|
|
43
43
|
- Required: project description, installation/setup, usage examples, support/help resources
|
|
44
|
-
- Optional:
|
|
44
|
+
- Optional: the header block below, features, contributing (link to `CONTRIBUTING.md`), license (link to `LICENSE`)
|
|
45
45
|
- Do not include full API documentation. Link to separate docs instead.
|
|
46
46
|
- Do not include license text. Reference the `LICENSE` file.
|
|
47
47
|
- Do not include detailed contribution guidelines. Reference `CONTRIBUTING.md`.
|
|
@@ -49,6 +49,25 @@ Scoped to the README at a repository root. A nested README documenting a folder,
|
|
|
49
49
|
- A README that points to the project's own context entries instead of restating them satisfies the required list by routing rather than carrying. A repository documented in `docs/` or `canon/context/` names the entry point and the listing command, then stops. The reader is one hop away, the same way `## Badges` treats zero as complete.
|
|
50
50
|
- A README missing an H1, or whose headings restate the tool that scaffolded it rather than the project, is unedited generator output rather than a page anyone wrote. Draft over it. Nothing here asks a session to preserve a section a scaffold wrote and nobody replaced.
|
|
51
51
|
|
|
52
|
+
### Header block
|
|
53
|
+
|
|
54
|
+
The opening of a root README is a block of up to six elements in a fixed order. Every element is read off the repository rather than written from scratch, and each one is a slot: a project that holds nothing for it leaves it out.
|
|
55
|
+
|
|
56
|
+
1. Mark: the project's logo or icon, from an image the repository already commits. Omit it when the project has none.
|
|
57
|
+
2. Title: the H1, naming the project.
|
|
58
|
+
3. Badges: the block `### Badges` below governs, placed directly under the title.
|
|
59
|
+
4. Claim: one line saying what the project does, taken from the manifest description or the repository description.
|
|
60
|
+
5. Live link: the URL of the running surface, from a `homepage` field or a deploy config. Omit it when the project has no page or no known URL.
|
|
61
|
+
6. Product screenshot: an image the project already commits and references, shown with alt text naming what it depicts. Present only when the project has a page. Omit it when no such image exists rather than writing a placeholder path.
|
|
62
|
+
|
|
63
|
+
The description of 2-3 sentences follows the block and expands the claim rather than repeating it.
|
|
64
|
+
|
|
65
|
+
- A project with no page omits the link and the screenshot. A library or a CLI reads as a complete block with a title, badges, and a claim.
|
|
66
|
+
- The order is the block's rather than the page's. The screenshot sits last inside the block, which puts it in the first row of content a reader meets below the title, badges, and claim.
|
|
67
|
+
- A themed light and dark pair counts as one element in the slot rather than two. The block does not require a pair and leaves the handling of themes to the project.
|
|
68
|
+
- Centering the block in an HTML wrapper is optional. Plain markdown reaches every host a README renders on, and a wrapper is styling a project can add.
|
|
69
|
+
- Never invent an element. A mark or a screenshot the project does not hold stays out of the page.
|
|
70
|
+
|
|
52
71
|
### Badges
|
|
53
72
|
|
|
54
73
|
A badge earns its place by carrying a fact the reader cannot get from the page they are already on. Three classes pass that test.
|