@erclx/canon 5.77.0 → 5.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/app-drive/REQUIREMENT.md +12 -0
- package/claude/skills/app-drive/SKILL.md +44 -8
- package/claude/skills/app-drive/references/config.md +18 -8
- package/claude/skills/app-drive/references/drive-record.md +4 -0
- package/claude/skills/auto-ship/REQUIREMENT.md +1 -1
- package/claude/skills/auto-ship/SKILL.md +5 -4
- package/claude/skills/auto-ship/references/draft-mark.md +33 -15
- package/claude/skills/auto-ship/references/failure-recovery.md +4 -2
- package/claude/skills/auto-ship/references/ui-checklist.md +9 -1
- package/claude/skills/code-craft/REQUIREMENT.md +1 -1
- package/claude/skills/code-craft/SKILL.md +7 -0
- package/claude/skills/code-craft/references/adopted.md +8 -0
- package/claude/skills/git-followup/SKILL.md +1 -0
- package/claude/skills/git-pr/REQUIREMENT.md +2 -0
- package/claude/skills/git-pr/SKILL.md +2 -0
- package/claude/skills/git-pr/references/final-command.md +34 -2
- package/claude/skills/git-pr/references/pr.md +1 -1
- package/claude/skills/git-ship/REQUIREMENT.md +2 -2
- package/claude/skills/git-ship/SKILL.md +16 -11
- package/claude/skills/git-ship/references/ci-watch.md +28 -4
- package/claude/skills/markdown-craft/references/markdown.md +4 -4
- package/claude/skills/plan-feature/references/plan.md +2 -0
- package/claude/skills/pr-evidence/SKILL.md +6 -1
- package/claude/skills/pr-evidence/references/evidence.md +1 -0
- package/claude/skills/pr-evidence/references/reel.md +9 -1
- package/claude/skills/review-address/SKILL.md +2 -1
- package/claude/skills/review-craft/references/ui.md +13 -1
- package/claude/skills/review-pr/references/body-variants.md +2 -0
- package/claude/skills/review-pr/references/close-out.md +2 -0
- package/claude/skills/review-pr/references/key-changes.md +2 -1
- package/claude/skills/review-pr/references/post.md +1 -0
- package/claude/skills/review-ui/REQUIREMENT.md +1 -0
- package/claude/skills/review-ui/SKILL.md +2 -2
- package/claude/skills/review-ui/references/driving.md +32 -4
- package/claude/skills/review-ui/references/post.md +6 -2
- package/claude/skills/role-orchestrator/REQUIREMENT.md +1 -1
- package/claude/skills/role-orchestrator/SKILL.md +4 -4
- package/claude/skills/role-orchestrator/references/orchestrator-hand-launch.md +2 -0
- package/claude/skills/role-orchestrator/references/orchestrator-handback.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-limit-stops.md +47 -0
- package/claude/skills/role-orchestrator/references/orchestrator-output.md +24 -1
- package/claude/skills/role-orchestrator/references/orchestrator-poll.md +3 -2
- package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +35 -4
- package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +3 -1
- package/claude/skills/role-worker/SKILL.md +2 -1
- package/claude/skills/task-board/SKILL.md +1 -0
- package/claude/skills/task-board/references/tasks.md +1 -0
- package/claude/skills/test-craft/SKILL.md +1 -0
- package/claude/skills/ui-checklist/REQUIREMENT.md +1 -1
- package/claude/skills/ui-checklist/SKILL.md +2 -1
- package/claude/skills/ui-craft/references/capture.md +16 -0
- package/claude/skills/ui-film/REQUIREMENT.md +2 -2
- package/claude/skills/ui-film/SKILL.md +6 -1
- package/claude/skills/ui-film/references/compose-craft.md +68 -11
- package/claude/skills/ui-film/references/config.md +2 -2
- package/claude/skills/ui-film/references/handoff.md +1 -1
- package/claude/skills/ui-film/references/harness.md +71 -0
- package/claude/skills/ui-film/references/take-craft.md +42 -17
- package/docs/workflow/ai-workflow.md +1 -1
- package/docs/workflow/operating-model.md +6 -5
- package/docs/workflow/parallel-features.md +2 -1
- package/docs/workflow/visual-design-workflow.md +2 -1
- package/governance/ledger/skills/code-craft/SKILL.md +9 -0
- package/governance/ledger/skills/test-craft/SKILL.md +1 -0
- package/governance/ledger/skills/ui-craft/capture.md +18 -5
- package/package.json +4 -3
- package/src/commands/pr.ts +255 -46
- package/src/commands/sessions.ts +88 -6
- package/src/commands/take.ts +12 -1
- package/src/commands/tasks.ts +41 -2
- package/src/design/components.ts +5 -107
- package/src/design/favicon.ts +21 -17
- package/src/design/render.ts +9 -27
- package/src/design/template.ts +42 -0
- package/src/design/templates/favicon.svg +17 -0
- package/src/design/templates/hand-drawn-figure.css +54 -0
- package/src/design/templates/preview.html +27 -0
- package/src/design/templates/scrollbar.css +29 -0
- package/src/design/templates/status.css +24 -0
- package/src/gate/embedded.ts +400 -0
- package/src/gate/stages.ts +8 -0
- package/src/git/files.ts +5 -2
- package/src/indexes/frontmatter.ts +27 -6
- package/src/indexes/render.ts +21 -3
- package/src/markdown/colons.ts +14 -4
- package/src/markdown/heading-case.ts +9 -2
- package/src/pr/bijection.ts +8 -0
- package/src/pr/evidence.ts +81 -13
- package/src/pr/frames.ts +1 -6
- package/src/pr/health.ts +63 -15
- package/src/pr/lift.ts +77 -9
- package/src/pr/links.ts +160 -0
- package/src/pr/local.ts +17 -4
- package/src/pr/paths.ts +20 -2
- package/src/pr/rest.ts +2 -0
- package/src/pr/review-scope.ts +28 -0
- package/src/pr/review-state.ts +5 -5
- package/src/pr/waiver.ts +45 -0
- package/src/sessions/stop.ts +145 -0
- package/src/sessions/transfer/bundle.ts +1 -1
- package/src/take/camera.ts +337 -208
- package/src/take/encode.ts +16 -42
- package/src/take/film.ts +7 -1
- package/src/take/frame-clock.ts +46 -0
- package/src/take/log.ts +43 -2
- package/src/take/perform.ts +85 -13
- package/src/take/plan.ts +26 -5
- package/src/take/reel.ts +30 -5
- package/src/take/screencast.ts +223 -13
- package/src/take/stepper.ts +24 -4
- package/src/tasks/holders.ts +198 -0
- package/src/tasks/validate.ts +148 -0
- package/src/commands/fixtures/gh.sh +0 -64
- package/src/take/fixtures/busy.html +0 -107
- package/src/take/fixtures/fade.html +0 -49
- package/src/take/fixtures/fetch.html +0 -38
- package/src/take/fixtures/input.html +0 -138
- package/src/take/zoom/NOTICE.md +0 -45
- package/src/take/zoom/math-utils.ts +0 -69
- package/src/take/zoom/region-utils.ts +0 -55
- package/src/take/zoom/spring.ts +0 -133
- /package/src/sessions/{transfer/transcript.ts → transcript.ts} +0 -0
package/src/commands/sessions.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type SessionReport,
|
|
12
12
|
selfOf,
|
|
13
13
|
} from '@/sessions/resolve'
|
|
14
|
+
import { readStop, type StopReading } from '@/sessions/stop'
|
|
14
15
|
import {
|
|
15
16
|
type Behind,
|
|
16
17
|
exportSession,
|
|
@@ -32,8 +33,11 @@ interface ListCommandOptions {
|
|
|
32
33
|
readonly branch?: string
|
|
33
34
|
readonly repository?: string
|
|
34
35
|
readonly self?: boolean
|
|
36
|
+
readonly stopped?: boolean
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
type ListedSession = ResolvedSession & StopReading
|
|
40
|
+
|
|
37
41
|
const REASONS: Record<string, string> = {
|
|
38
42
|
'not-a-repository': 'working outside any git repository',
|
|
39
43
|
'detached-head': 'detached HEAD, so the worktree holds no branch name',
|
|
@@ -67,6 +71,7 @@ export function register(program: Command): void {
|
|
|
67
71
|
'--self',
|
|
68
72
|
"Report the caller's own row, and refuse where the roster holds none",
|
|
69
73
|
)
|
|
74
|
+
.option('--stopped', 'Report only the sessions a usage limit stopped')
|
|
70
75
|
.addHelpText(
|
|
71
76
|
'after',
|
|
72
77
|
[
|
|
@@ -161,12 +166,30 @@ export function register(program: Command): void {
|
|
|
161
166
|
'framed listing renders the same dwell beside the status, at the',
|
|
162
167
|
'coarsest unit that keeps it a whole number.',
|
|
163
168
|
'',
|
|
169
|
+
'Each row also carries "stopped" and "stopResets", read from the tail of',
|
|
170
|
+
'the session transcript. "stopped" is "usage-limit" when the last',
|
|
171
|
+
'assistant entry is a limit stop, passing over a synthetic entry with no',
|
|
172
|
+
'error, such as the one a client restart writes. "stopResets" holds the',
|
|
173
|
+
'text the client wrote after "resets", or null when it wrote none.',
|
|
174
|
+
'"stopped" is "unknown" when the transcript could not be read or a',
|
|
175
|
+
'limit-worded entry has another shape, so the row may be stopped, and',
|
|
176
|
+
'null otherwise. A "busy" row reads',
|
|
177
|
+
'null without opening its transcript, since a turn is running.',
|
|
178
|
+
'',
|
|
179
|
+
'--stopped keeps the "usage-limit" rows after --branch and --self apply.',
|
|
180
|
+
'It drops "unknown" rows, and "stopsUnread" counts them before it does,',
|
|
181
|
+
'so a count above zero means a row here may be stopped and is not listed.',
|
|
182
|
+
'"foreignDead" above zero leaves the same doubt about a session this',
|
|
183
|
+
'reader could not check, and an empty result says so on stderr.',
|
|
184
|
+
'An empty result exits 0. "claimed" still reads the whole roster.',
|
|
185
|
+
'',
|
|
164
186
|
'Examples:',
|
|
165
187
|
' canon sessions list',
|
|
166
188
|
' canon sessions list --json',
|
|
167
189
|
' canon sessions list --branch feat/parser --json',
|
|
168
190
|
' canon sessions list --branch chore/agents --repository ../caret --json',
|
|
169
191
|
' canon sessions list --self --json',
|
|
192
|
+
' canon sessions list --stopped --json',
|
|
170
193
|
'',
|
|
171
194
|
].join('\n'),
|
|
172
195
|
)
|
|
@@ -477,15 +500,37 @@ async function runList(opts: ListCommandOptions): Promise<number> {
|
|
|
477
500
|
? await checkClaim(opts.branch, { cwd: at, resolve: async () => report })
|
|
478
501
|
: null
|
|
479
502
|
|
|
503
|
+
const read: readonly ListedSession[] = shown.map((session) => ({
|
|
504
|
+
...session,
|
|
505
|
+
...readStop(session),
|
|
506
|
+
}))
|
|
507
|
+
const stopsUnread = read.filter(
|
|
508
|
+
(session) => session.stopped === 'unknown',
|
|
509
|
+
).length
|
|
510
|
+
const listed = opts.stopped
|
|
511
|
+
? read.filter((session) => session.stopped === 'usage-limit')
|
|
512
|
+
: read
|
|
513
|
+
|
|
480
514
|
intro('canon sessions list')
|
|
481
|
-
reportConfidence(
|
|
515
|
+
reportConfidence(listed, report)
|
|
482
516
|
reportSessions(
|
|
483
|
-
|
|
517
|
+
listed,
|
|
484
518
|
opts.branch,
|
|
485
519
|
repository,
|
|
486
520
|
own !== null,
|
|
487
521
|
report.foreignDead,
|
|
522
|
+
opts.stopped ? stopsUnread : null,
|
|
488
523
|
)
|
|
524
|
+
if (opts.stopped && stopsUnread > 0) {
|
|
525
|
+
logWarn(
|
|
526
|
+
`${plural(stopsUnread, 'row')} could not be read and may be stopped, so this list can be short.`,
|
|
527
|
+
)
|
|
528
|
+
}
|
|
529
|
+
if (opts.stopped && own === null && report.foreignDead > 0) {
|
|
530
|
+
logWarn(
|
|
531
|
+
`${report.foreignDead} records from another pid namespace have no answering socket, so a stopped session may be missing from this list.`,
|
|
532
|
+
)
|
|
533
|
+
}
|
|
489
534
|
if (claim) reportClaim(claim)
|
|
490
535
|
outro()
|
|
491
536
|
|
|
@@ -494,8 +539,9 @@ async function runList(opts: ListCommandOptions): Promise<number> {
|
|
|
494
539
|
`${JSON.stringify({
|
|
495
540
|
dir: report.dir,
|
|
496
541
|
// Over the rows shown, so an unrelated row --branch dropped cannot weaken it.
|
|
497
|
-
confidence: confidenceOf(
|
|
542
|
+
confidence: confidenceOf(listed),
|
|
498
543
|
foreignDead: report.foreignDead,
|
|
544
|
+
stopsUnread,
|
|
499
545
|
branch: opts.branch ?? null,
|
|
500
546
|
repository,
|
|
501
547
|
worktree: claim?.worktree ?? null,
|
|
@@ -503,7 +549,7 @@ async function runList(opts: ListCommandOptions): Promise<number> {
|
|
|
503
549
|
claimed: claim?.claimed ?? null,
|
|
504
550
|
sessionsReadable: claim?.sessionsReadable ?? null,
|
|
505
551
|
refsReadable: claim?.refsReadable ?? null,
|
|
506
|
-
sessions:
|
|
552
|
+
sessions: listed,
|
|
507
553
|
})}\n`,
|
|
508
554
|
)
|
|
509
555
|
}
|
|
@@ -578,11 +624,12 @@ function reportConfidence(
|
|
|
578
624
|
}
|
|
579
625
|
|
|
580
626
|
function reportSessions(
|
|
581
|
-
sessions: readonly
|
|
627
|
+
sessions: readonly ListedSession[],
|
|
582
628
|
branch: string | undefined,
|
|
583
629
|
repository: string | null,
|
|
584
630
|
scoped: boolean,
|
|
585
631
|
foreignDead: number,
|
|
632
|
+
stopsUnread: number | null,
|
|
586
633
|
): void {
|
|
587
634
|
logStep('Sessions')
|
|
588
635
|
|
|
@@ -596,6 +643,11 @@ function reportSessions(
|
|
|
596
643
|
// was narrowed to one row before the branch filter ran. Reporting the wider
|
|
597
644
|
// answer there would claim a reading this run never took.
|
|
598
645
|
if (sessions.length === 0) {
|
|
646
|
+
if (stopsUnread !== null) {
|
|
647
|
+
logInfo(noneStopped(branch, scoped, foreignDead, stopsUnread))
|
|
648
|
+
return
|
|
649
|
+
}
|
|
650
|
+
|
|
599
651
|
if (branch) {
|
|
600
652
|
logInfo(
|
|
601
653
|
scoped
|
|
@@ -630,12 +682,42 @@ function reportSessions(
|
|
|
630
682
|
`unresolved: ${REASONS[session.unresolved ?? ''] ?? 'unknown'}`
|
|
631
683
|
const dwell = formatDwell(session.statusDwellMs)
|
|
632
684
|
const status = dwell ? `${session.status} ${dwell}` : session.status
|
|
633
|
-
return
|
|
685
|
+
return [
|
|
686
|
+
`${session.name} ${status} ${held}`,
|
|
687
|
+
` ${session.cwd}`,
|
|
688
|
+
...stopLine(session),
|
|
689
|
+
].join('\n')
|
|
634
690
|
})
|
|
635
691
|
.join('\n'),
|
|
636
692
|
)
|
|
637
693
|
}
|
|
638
694
|
|
|
695
|
+
/**
|
|
696
|
+
* Words an empty `--stopped` result by what was read, since "no session is
|
|
697
|
+
* stopped" is a claim about the machine that a row this run could not read, or
|
|
698
|
+
* a record it could not check, leaves open.
|
|
699
|
+
*/
|
|
700
|
+
function noneStopped(
|
|
701
|
+
branch: string | undefined,
|
|
702
|
+
scoped: boolean,
|
|
703
|
+
foreignDead: number,
|
|
704
|
+
stopsUnread: number,
|
|
705
|
+
): string {
|
|
706
|
+
if (stopsUnread > 0 || (!scoped && foreignDead > 0)) {
|
|
707
|
+
return 'No session this reader could check is stopped.'
|
|
708
|
+
}
|
|
709
|
+
if (scoped) return 'This session is not stopped.'
|
|
710
|
+
return branch
|
|
711
|
+
? `No session holding ${branch} is stopped.`
|
|
712
|
+
: 'No session is stopped.'
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function stopLine(session: StopReading): string[] {
|
|
716
|
+
if (session.stopped === null) return []
|
|
717
|
+
const resets = session.stopResets ? `, resets ${session.stopResets}` : ''
|
|
718
|
+
return [` stopped: ${session.stopped}${resets}`]
|
|
719
|
+
}
|
|
720
|
+
|
|
639
721
|
/**
|
|
640
722
|
* Renders the dwell at the coarsest unit that keeps it a whole number, since a
|
|
641
723
|
* reader scanning a roster wants an age at a glance rather than a millisecond
|
package/src/commands/take.ts
CHANGED
|
@@ -114,6 +114,9 @@ export function register(program: Command): void {
|
|
|
114
114
|
'WebP at 960 wide and 20 fps, keeping every third frame, with the cursor',
|
|
115
115
|
'and click ring drawn from each camera track. It carries no zoom, so a',
|
|
116
116
|
"reviewer sees the whole page. clip.mp4 stays each beat's raw take.",
|
|
117
|
+
'The reel is lossless, so a fade-out leaves no ghost, and it falls back',
|
|
118
|
+
'to lossy past 10 MB so a long reel still posts. The record says which',
|
|
119
|
+
'in `lossless`.',
|
|
117
120
|
'',
|
|
118
121
|
' {',
|
|
119
122
|
' "beats": [',
|
|
@@ -127,7 +130,8 @@ export function register(program: Command): void {
|
|
|
127
130
|
' }',
|
|
128
131
|
'',
|
|
129
132
|
'Step kinds:',
|
|
130
|
-
' move <target>|<x,y> glide the pointer to a target or a point',
|
|
133
|
+
' move <target>|<x,y> glide the pointer to a target or a point, and',
|
|
134
|
+
' with hold <ms>, zoom onto its end for that long',
|
|
131
135
|
' click <target> glide to the target and press it',
|
|
132
136
|
' dblclick <target> glide to the target and press it twice',
|
|
133
137
|
' drag <from> <to> press the from target, glide held, release on to',
|
|
@@ -143,6 +147,13 @@ export function register(program: Command): void {
|
|
|
143
147
|
'PageDown, the four arrows as ArrowUp and the like, and F1 to F12. A key',
|
|
144
148
|
'moves no pointer, so the camera neither aims at it nor ends a zoom.',
|
|
145
149
|
'',
|
|
150
|
+
'A take paces each press on its own: the pointer rests 300 ms on the',
|
|
151
|
+
'target before a click, dblclick, or drag, and holds 500 ms on the',
|
|
152
|
+
'result before the next step. A fill waits 400 ms between pressing the',
|
|
153
|
+
'field and its first key, and holds 500 ms after its last. Setup steps',
|
|
154
|
+
'run unpaced. The camera zooms in over the glide reaching a target and',
|
|
155
|
+
'out over the glide leaving it, scaling about the target.',
|
|
156
|
+
'',
|
|
146
157
|
'drag drives pointer events, the kind a canvas board listens for. It',
|
|
147
158
|
'starts no native HTML5 drag of a draggable element on the stepped route,',
|
|
148
159
|
'so a beat needing one films the same only on the screencast route.',
|
package/src/commands/tasks.ts
CHANGED
|
@@ -72,6 +72,7 @@ import {
|
|
|
72
72
|
type ValidateOutcome,
|
|
73
73
|
validateBoard,
|
|
74
74
|
} from '@/tasks/validate'
|
|
75
|
+
import { liveReaders } from '@/tasks/holders'
|
|
75
76
|
import {
|
|
76
77
|
intro,
|
|
77
78
|
logAdd,
|
|
@@ -110,6 +111,7 @@ interface DeclineCommandOptions {
|
|
|
110
111
|
interface ValidateCommandOptions {
|
|
111
112
|
readonly json?: boolean
|
|
112
113
|
readonly root?: string
|
|
114
|
+
readonly live?: boolean
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
interface CitationsCommandOptions {
|
|
@@ -323,6 +325,10 @@ export function register(program: Command): void {
|
|
|
323
325
|
.helpOption('-h, --help', 'Show this help message')
|
|
324
326
|
.option('--json', 'Add a machine-readable record on stdout')
|
|
325
327
|
.option('--root <path>', 'Board root, defaulting to the main worktree')
|
|
328
|
+
.option(
|
|
329
|
+
'--live',
|
|
330
|
+
'Also read the session roster and open pull requests for held rows',
|
|
331
|
+
)
|
|
326
332
|
.addHelpText(
|
|
327
333
|
'after',
|
|
328
334
|
[
|
|
@@ -338,6 +344,12 @@ export function register(program: Command): void {
|
|
|
338
344
|
` no Waiting on cell renders past ${WAITING_LIMIT} characters`,
|
|
339
345
|
' a blocker citing a bare vNNN.N label settles like a linked one',
|
|
340
346
|
' a file two Run now rows share is listed, never failed',
|
|
347
|
+
' with --live, no Up next or Needs a plan row is held by a live session',
|
|
348
|
+
" or an open pull request on its plan's branch",
|
|
349
|
+
'',
|
|
350
|
+
'The `live` field states each --live source as read, unread, or not-asked.',
|
|
351
|
+
'A source that fails reads as unread and moves no exit code, so read that',
|
|
352
|
+
'field rather than taking exit 0 as clean. A run without --live reads neither.',
|
|
341
353
|
'',
|
|
342
354
|
'Refusals, read off `reason`:',
|
|
343
355
|
' no-board the repository holds no `.canon/tasks/` board',
|
|
@@ -356,6 +368,7 @@ export function register(program: Command): void {
|
|
|
356
368
|
'Examples:',
|
|
357
369
|
' canon tasks validate',
|
|
358
370
|
' canon tasks validate --json',
|
|
371
|
+
' canon tasks validate --live --json',
|
|
359
372
|
'',
|
|
360
373
|
].join('\n'),
|
|
361
374
|
)
|
|
@@ -1911,7 +1924,10 @@ function reportNextLabel(
|
|
|
1911
1924
|
|
|
1912
1925
|
async function runValidate(opts: ValidateCommandOptions): Promise<number> {
|
|
1913
1926
|
const root = opts.root ?? (await mainWorktreeRoot())
|
|
1914
|
-
const outcome = await validateBoard(
|
|
1927
|
+
const outcome = await validateBoard(
|
|
1928
|
+
root,
|
|
1929
|
+
opts.live ? { live: liveReaders(root) } : {},
|
|
1930
|
+
)
|
|
1915
1931
|
|
|
1916
1932
|
return reportValidation(outcome, opts.json ?? false, root)
|
|
1917
1933
|
}
|
|
@@ -2390,8 +2406,30 @@ function reportValidation(
|
|
|
2390
2406
|
} else {
|
|
2391
2407
|
for (const task of outcome.unplaced) logWarn(describeUnplaced(task))
|
|
2392
2408
|
}
|
|
2393
|
-
|
|
2409
|
+
|
|
2410
|
+
if (outcome.live.sessions !== 'not-asked') {
|
|
2411
|
+
logStep('Held rows')
|
|
2412
|
+
if (
|
|
2413
|
+
outcome.live.sessions === 'read' &&
|
|
2414
|
+
outcome.live.pulls === 'read' &&
|
|
2415
|
+
!outcome.findings.some((finding) => finding.kind === 'row-held')
|
|
2416
|
+
) {
|
|
2417
|
+
logInfo('no row outside run now is held by a session or a pull request')
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
// Unread is a warning in both modes, since diagnostics reach stderr with --json too.
|
|
2423
|
+
if (outcome.live.sessions === 'unread') {
|
|
2424
|
+
logWarn('the session roster could not be read, so no session was checked')
|
|
2394
2425
|
}
|
|
2426
|
+
if (outcome.live.pulls === 'unread') {
|
|
2427
|
+
logWarn(
|
|
2428
|
+
'pull requests could not be read, so a hold may be missed or already merged',
|
|
2429
|
+
)
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
if (!emitJson) outro()
|
|
2395
2433
|
|
|
2396
2434
|
if (emitJson) {
|
|
2397
2435
|
process.stdout.write(
|
|
@@ -2408,6 +2446,7 @@ function reportValidation(
|
|
|
2408
2446
|
shared: outcome.shared,
|
|
2409
2447
|
wide: outcome.wide,
|
|
2410
2448
|
unplaced: outcome.unplaced,
|
|
2449
|
+
live: outcome.live,
|
|
2411
2450
|
})}\n`,
|
|
2412
2451
|
)
|
|
2413
2452
|
}
|
package/src/design/components.ts
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* names out of that record and says they live in code, which is here.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { readTemplate } from '@/design/template'
|
|
19
|
+
|
|
18
20
|
export interface Component {
|
|
19
21
|
readonly name: string
|
|
20
22
|
/** Why the component exists, carried into the emitted stylesheet as a comment. */
|
|
@@ -43,30 +45,7 @@ const STATUS: Component = {
|
|
|
43
45
|
'--color-muted',
|
|
44
46
|
'--radius-marker',
|
|
45
47
|
],
|
|
46
|
-
rules:
|
|
47
|
-
display: inline-flex;
|
|
48
|
-
align-items: center;
|
|
49
|
-
gap: 0.42rem;
|
|
50
|
-
font-size: 0.75rem;
|
|
51
|
-
font-weight: 500;
|
|
52
|
-
letter-spacing: 0;
|
|
53
|
-
text-transform: none;
|
|
54
|
-
color: var(--color-muted);
|
|
55
|
-
white-space: nowrap;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
.status::before {
|
|
59
|
-
content: '';
|
|
60
|
-
width: 6px;
|
|
61
|
-
height: 6px;
|
|
62
|
-
flex: none;
|
|
63
|
-
border-radius: var(--radius-marker);
|
|
64
|
-
background: var(--color-border);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
.status.is-done::before {
|
|
68
|
-
background: var(--color-accent);
|
|
69
|
-
}`,
|
|
48
|
+
rules: readTemplate('status.css'),
|
|
70
49
|
}
|
|
71
50
|
|
|
72
51
|
/**
|
|
@@ -84,35 +63,7 @@ const SCROLLBAR: Component = {
|
|
|
84
63
|
'so the two engines cannot drift apart.',
|
|
85
64
|
].join('\n '),
|
|
86
65
|
reads: ['--color-border', '--color-muted', '--color-background'],
|
|
87
|
-
rules:
|
|
88
|
-
scrollbar-width: thin;
|
|
89
|
-
scrollbar-color: var(--color-border) transparent;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
*::-webkit-scrollbar {
|
|
93
|
-
width: 10px;
|
|
94
|
-
height: 10px;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
*::-webkit-scrollbar-track {
|
|
98
|
-
background: transparent;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
*::-webkit-scrollbar-thumb {
|
|
102
|
-
border-radius: var(--radius-marker);
|
|
103
|
-
background: var(--color-border);
|
|
104
|
-
/* Inset by painting a border in the page color, which is what keeps the
|
|
105
|
-
thumb off the edges without a second element. */
|
|
106
|
-
border: 3px solid var(--color-background);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
*::-webkit-scrollbar-thumb:hover {
|
|
110
|
-
background: var(--color-muted);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
*::-webkit-scrollbar-corner {
|
|
114
|
-
background: transparent;
|
|
115
|
-
}`,
|
|
66
|
+
rules: readTemplate('scrollbar.css'),
|
|
116
67
|
}
|
|
117
68
|
|
|
118
69
|
/**
|
|
@@ -130,60 +81,7 @@ const HAND_DRAWN_FIGURE: Component = {
|
|
|
130
81
|
'reading measure and cleared past an outline rail on a wide viewport.',
|
|
131
82
|
].join('\n '),
|
|
132
83
|
reads: ['--color-muted', '--figure-hand', '--figure-wide'],
|
|
133
|
-
rules:
|
|
134
|
-
|
|
135
|
-
:root {
|
|
136
|
-
--figure-hand: 'Virgil', 'Excalifont', cursive;
|
|
137
|
-
--figure-wide: 64rem;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
figure {
|
|
141
|
-
margin: 2.75rem 0;
|
|
142
|
-
width: var(--figure-wide);
|
|
143
|
-
max-width: 92vw;
|
|
144
|
-
margin-left: 50%;
|
|
145
|
-
transform: translateX(-50%);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
figure svg {
|
|
149
|
-
width: 100%;
|
|
150
|
-
height: auto;
|
|
151
|
-
display: block;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/* CSS beats an SVG presentation attribute, so the diagrams pick up the
|
|
155
|
-
hand face without editing a single lesson. */
|
|
156
|
-
figure svg text {
|
|
157
|
-
font-family: var(--figure-hand);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
figcaption {
|
|
161
|
-
font-family: var(--figure-hand);
|
|
162
|
-
font-size: 1rem;
|
|
163
|
-
color: var(--color-muted);
|
|
164
|
-
margin: 1rem auto 0;
|
|
165
|
-
line-height: 1.5;
|
|
166
|
-
max-width: 52rem;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/* A figure breaks the measure and must still clear an outline rail. Deriving
|
|
170
|
-
the ceiling from the viewport does not hold, since a rail positioned from
|
|
171
|
-
the centre keeps overlapping as the window widens, so the ceiling is fixed
|
|
172
|
-
instead. */
|
|
173
|
-
@media (min-width: 1421px) {
|
|
174
|
-
figure {
|
|
175
|
-
max-width: 54rem;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
@media (max-width: 640px) {
|
|
180
|
-
figure {
|
|
181
|
-
width: 100%;
|
|
182
|
-
max-width: 100%;
|
|
183
|
-
margin-left: 0;
|
|
184
|
-
transform: none;
|
|
185
|
-
}
|
|
186
|
-
}`,
|
|
84
|
+
rules: readTemplate('hand-drawn-figure.css'),
|
|
187
85
|
}
|
|
188
86
|
|
|
189
87
|
export const COMPONENTS: readonly Component[] = [
|
package/src/design/favicon.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Relative, since web/ bundles this module for FAVICON_COLORS and its `@` means web/src.
|
|
2
|
+
import { fillTemplate, readTemplate } from './template'
|
|
3
|
+
|
|
1
4
|
export interface ThemePair {
|
|
2
5
|
readonly light: string
|
|
3
6
|
readonly dark: string
|
|
@@ -68,23 +71,24 @@ export function renderFavicon(mark: string, colors: FaviconColors): string {
|
|
|
68
71
|
const inset = EDGE_WIDTH / 2
|
|
69
72
|
const shapes = markShapes(mark)
|
|
70
73
|
|
|
71
|
-
return
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
74
|
+
return fillTemplate(readTemplate('favicon.svg'), {
|
|
75
|
+
left: String(left),
|
|
76
|
+
top: String(top),
|
|
77
|
+
side: String(round(side)),
|
|
78
|
+
tileX: String(round(left + inset)),
|
|
79
|
+
tileY: String(round(top + inset)),
|
|
80
|
+
tileSide: String(round(side - EDGE_WIDTH)),
|
|
81
|
+
tileRadius: String(round(side * TILE_RADIUS - inset)),
|
|
82
|
+
edgeWidth: String(EDGE_WIDTH),
|
|
83
|
+
tileDark: colors.tile.dark,
|
|
84
|
+
tileLight: colors.tile.light,
|
|
85
|
+
edge: colors.edge,
|
|
86
|
+
cDark: colors.c.dark,
|
|
87
|
+
cLight: colors.c.light,
|
|
88
|
+
pipDark: colors.pip.dark,
|
|
89
|
+
pipLight: colors.pip.light,
|
|
90
|
+
shapes: shapes.replace(/\n\s*/g, '\n '),
|
|
91
|
+
})
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
/**
|
package/src/design/render.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { fontFaceBlock } from '@/design/css'
|
|
|
6
6
|
import { faviconLink } from '@/design/favicon'
|
|
7
7
|
import type { FontFace } from '@/design/fonts'
|
|
8
8
|
import { FONT_FACES } from '@/design/fonts'
|
|
9
|
+
import { fillTemplate, readTemplate } from '@/design/template'
|
|
9
10
|
import { colorValue } from '@/design/tokens'
|
|
10
11
|
|
|
11
12
|
export interface RenderResult {
|
|
@@ -239,33 +240,14 @@ function buildHtml(
|
|
|
239
240
|
const summary = tagged
|
|
240
241
|
? `\n<p class="note">${total - tagged} of ${total} cells are anchored to a source. The other ${tagged} ${verb} <code>? verify</code>, so nothing anchors them yet.</p>`
|
|
241
242
|
: ''
|
|
242
|
-
return
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
body { font-family: ${bodyStack}; margin: 2rem; max-width: 960px; color: var(--preview-ink); background: var(--preview-paper); }
|
|
251
|
-
h1 { margin-top: 0; }
|
|
252
|
-
h2 { margin-top: 2rem; border-bottom: 1px solid var(--preview-rule); padding-bottom: 0.25rem; }
|
|
253
|
-
table { border-collapse: collapse; width: 100%; margin-top: 0.5rem; }
|
|
254
|
-
th, td { text-align: left; padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--preview-rule); font-size: 14px; }
|
|
255
|
-
th { background: var(--preview-panel); font-weight: 600; }
|
|
256
|
-
.swatch { display: inline-block; width: 1.5rem; height: 1.5rem; border-radius: 4px; border: 1px solid var(--preview-rule); vertical-align: middle; margin-right: 0.5rem; }
|
|
257
|
-
.bar { display: inline-block; height: 1rem; background: var(--preview-muted); border-radius: 2px; vertical-align: middle; }
|
|
258
|
-
.note { color: var(--preview-muted); font-size: 13px; margin-top: 0.5rem; }
|
|
259
|
-
.empty { color: var(--preview-muted); font-style: italic; }${verifyStyle}
|
|
260
|
-
</style>
|
|
261
|
-
</head>
|
|
262
|
-
<body>
|
|
263
|
-
<h1>Design tokens</h1>
|
|
264
|
-
<p class="note">Generated from <code>canon/DESIGN.md</code> by <code>bun run web:previews</code>. Token preview only, not a screen mock.</p>${summary}
|
|
265
|
-
${sections.join('\n')}
|
|
266
|
-
</body>
|
|
267
|
-
</html>
|
|
268
|
-
`
|
|
243
|
+
return fillTemplate(readTemplate('preview.html'), {
|
|
244
|
+
favicon: favicon === undefined ? '' : `\n${faviconLink(favicon)}`,
|
|
245
|
+
chrome: previewChrome(),
|
|
246
|
+
bodyStack,
|
|
247
|
+
verifyStyle,
|
|
248
|
+
summary,
|
|
249
|
+
sections: sections.join('\n'),
|
|
250
|
+
})
|
|
269
251
|
}
|
|
270
252
|
|
|
271
253
|
function sectionPersonality(text: string): string {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A file under `templates/`, resolved from this module so the published CLI
|
|
5
|
+
* finds it wherever the package is installed. Each file ends on one newline
|
|
6
|
+
* that the emitted text does not carry, so that newline is dropped.
|
|
7
|
+
*
|
|
8
|
+
* The folder is outside the formatter, since a reflowed template changes the
|
|
9
|
+
* bytes every generator emits.
|
|
10
|
+
*/
|
|
11
|
+
export function readTemplate(file: string): string {
|
|
12
|
+
return readFileSync(
|
|
13
|
+
new URL(`./templates/${file}`, import.meta.url),
|
|
14
|
+
'utf8',
|
|
15
|
+
).replace(/\n$/, '')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const SLOT = /\{\{(\w+)\}\}/g
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Fills each `{{name}}` slot from `values`. A slot with no value, or a value no
|
|
22
|
+
* slot asks for, throws, so a template and its caller cannot drift apart
|
|
23
|
+
* silently.
|
|
24
|
+
*/
|
|
25
|
+
export function fillTemplate(
|
|
26
|
+
template: string,
|
|
27
|
+
values: Readonly<Record<string, string>>,
|
|
28
|
+
): string {
|
|
29
|
+
const used = new Set<string>()
|
|
30
|
+
const filled = template.replace(SLOT, (_, name: string) => {
|
|
31
|
+
const value = values[name]
|
|
32
|
+
if (value === undefined)
|
|
33
|
+
throw new Error(`template slot {{${name}}} has no value`)
|
|
34
|
+
used.add(name)
|
|
35
|
+
return value
|
|
36
|
+
})
|
|
37
|
+
const unused = Object.keys(values).filter((name) => !used.has(name))
|
|
38
|
+
if (unused.length > 0) {
|
|
39
|
+
throw new Error(`template has no slot for ${unused.join(', ')}`)
|
|
40
|
+
}
|
|
41
|
+
return filled
|
|
42
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="{{left}} {{top}} {{side}} {{side}}">
|
|
2
|
+
<style>
|
|
3
|
+
.tile { fill: {{tileDark}}; stroke: {{tileDark}}; }
|
|
4
|
+
.c { fill: {{cDark}}; }
|
|
5
|
+
.pip { fill: {{pipDark}}; }
|
|
6
|
+
@media (prefers-color-scheme: light) {
|
|
7
|
+
.tile { fill: {{tileLight}}; stroke: {{edge}}; }
|
|
8
|
+
.c { fill: {{cLight}}; }
|
|
9
|
+
.pip { fill: {{pipLight}}; }
|
|
10
|
+
}
|
|
11
|
+
</style>
|
|
12
|
+
<rect class="tile" x="{{tileX}}" y="{{tileY}}" width="{{tileSide}}" height="{{tileSide}}" rx="{{tileRadius}}" stroke-width="{{edgeWidth}}" />
|
|
13
|
+
<g class="c">
|
|
14
|
+
{{shapes}}
|
|
15
|
+
</g>
|
|
16
|
+
</svg>
|
|
17
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/* ---- Figures: hand-drawn, and wider than the measure ---- */
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--figure-hand: 'Virgil', 'Excalifont', cursive;
|
|
5
|
+
--figure-wide: 64rem;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
figure {
|
|
9
|
+
margin: 2.75rem 0;
|
|
10
|
+
width: var(--figure-wide);
|
|
11
|
+
max-width: 92vw;
|
|
12
|
+
margin-left: 50%;
|
|
13
|
+
transform: translateX(-50%);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
figure svg {
|
|
17
|
+
width: 100%;
|
|
18
|
+
height: auto;
|
|
19
|
+
display: block;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* CSS beats an SVG presentation attribute, so the diagrams pick up the
|
|
23
|
+
hand face without editing a single lesson. */
|
|
24
|
+
figure svg text {
|
|
25
|
+
font-family: var(--figure-hand);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
figcaption {
|
|
29
|
+
font-family: var(--figure-hand);
|
|
30
|
+
font-size: 1rem;
|
|
31
|
+
color: var(--color-muted);
|
|
32
|
+
margin: 1rem auto 0;
|
|
33
|
+
line-height: 1.5;
|
|
34
|
+
max-width: 52rem;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* A figure breaks the measure and must still clear an outline rail. Deriving
|
|
38
|
+
the ceiling from the viewport does not hold, since a rail positioned from
|
|
39
|
+
the centre keeps overlapping as the window widens, so the ceiling is fixed
|
|
40
|
+
instead. */
|
|
41
|
+
@media (min-width: 1421px) {
|
|
42
|
+
figure {
|
|
43
|
+
max-width: 54rem;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@media (max-width: 640px) {
|
|
48
|
+
figure {
|
|
49
|
+
width: 100%;
|
|
50
|
+
max-width: 100%;
|
|
51
|
+
margin-left: 0;
|
|
52
|
+
transform: none;
|
|
53
|
+
}
|
|
54
|
+
}
|