@erclx/canon 5.76.0 → 5.78.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 +33 -0
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/auto-ship/references/ui-checklist.md +9 -1
- package/claude/skills/ci-workflow/REQUIREMENT.md +8 -2
- package/claude/skills/ci-workflow/SKILL.md +3 -0
- 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/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-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/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/SKILL.md +2 -2
- 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-worker/SKILL.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/agents/targets.md +3 -1
- package/docs/workflow/operating-model.md +6 -5
- 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 +5 -4
- package/src/commands/pr.ts +117 -31
- package/src/commands/take.ts +12 -1
- package/src/commands/targets.ts +11 -1
- 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/pr/evidence.ts +43 -4
- package/src/pr/frames.ts +1 -6
- package/src/pr/lift.ts +77 -9
- 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/take/camera.ts +337 -208
- package/src/take/film.ts +7 -1
- 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 +110 -3
- package/src/take/stepper.ts +24 -4
- package/src/targets/registry.ts +99 -14
- package/src/targets/resolve.ts +3 -1
- package/tooling/astro/configs/card.config.mjs +4 -0
- package/tooling/astro/configs/scripts/check-card-exclusion.sh +7 -2
- package/tooling/astro/reference.md +2 -1
- package/tooling/web/configs/.github/workflows/readme-screenshot.yml +3 -1
- package/tooling/web/configs/.github/workflows/verify.yml +5 -2
- package/tooling/web/configs/scripts/install-playwright-deps.sh +52 -0
- package/tooling/web/reference.md +5 -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/targets/registry.ts
CHANGED
|
@@ -2,11 +2,12 @@ import {
|
|
|
2
2
|
existsSync,
|
|
3
3
|
mkdirSync,
|
|
4
4
|
readFileSync,
|
|
5
|
+
realpathSync,
|
|
5
6
|
renameSync,
|
|
6
7
|
writeFileSync,
|
|
7
8
|
} from 'node:fs'
|
|
8
|
-
import { homedir } from 'node:os'
|
|
9
|
-
import { dirname, join, resolve } from 'node:path'
|
|
9
|
+
import { homedir, tmpdir } from 'node:os'
|
|
10
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* One project the toolkit has installed into, as the install recorded it.
|
|
@@ -116,15 +117,90 @@ function isRecord(value: Partial<TargetRecord>): value is TargetRecord {
|
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
/**
|
|
119
|
-
*
|
|
120
|
+
* The folders whose contents are never a project, resolved at call time so a
|
|
121
|
+
* test's `CANON_STATE_DIR` override moves the state root with it.
|
|
122
|
+
*
|
|
123
|
+
* Sandbox runs and replays live under the state folder, background jobs keep
|
|
124
|
+
* their scratch under `~/.claude/jobs/`, and unit tests stamp into the OS temp
|
|
125
|
+
* folder. Each one passes through a stamping sync, so without this list every
|
|
126
|
+
* run leaves a row behind for a folder that is gone a minute later.
|
|
127
|
+
*/
|
|
128
|
+
export function scratchRoots(): readonly string[] {
|
|
129
|
+
return [tmpdir(), stateDir(), join(homedir(), '.claude', 'jobs')]
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Whether a path sits under a scratch root or inside any `.canon/tmp/` folder.
|
|
134
|
+
*
|
|
135
|
+
* Both sides are compared in their given and their real form, since macOS
|
|
136
|
+
* hands out its temp folder as `/var/folders/...` while a path that went
|
|
137
|
+
* through a symlink resolution reads `/private/var/folders/...`. A
|
|
138
|
+
* `.claude/worktrees/` checkout is not scratch: it is a real checkout of a real
|
|
139
|
+
* project, and only a root on the list or a `.canon/tmp/` segment refuses it.
|
|
140
|
+
*/
|
|
141
|
+
export function isScratchPath(
|
|
142
|
+
target: string,
|
|
143
|
+
roots: readonly string[] = scratchRoots(),
|
|
144
|
+
): boolean {
|
|
145
|
+
const forms = bothForms(resolve(target))
|
|
146
|
+
const rootForms = roots.flatMap((root) => bothForms(resolve(root)))
|
|
147
|
+
|
|
148
|
+
return forms.some(
|
|
149
|
+
(form) =>
|
|
150
|
+
hasCanonTmpSegment(form) ||
|
|
151
|
+
rootForms.some((root) => isWithin(form, root)),
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function bothForms(path: string): readonly string[] {
|
|
156
|
+
try {
|
|
157
|
+
const real = realpathSync(path)
|
|
158
|
+
return real === path ? [path] : [path, real]
|
|
159
|
+
} catch {
|
|
160
|
+
return [path]
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function hasCanonTmpSegment(path: string): boolean {
|
|
165
|
+
const segments = path.split(sep)
|
|
166
|
+
return segments.some(
|
|
167
|
+
(segment, index) => segment === '.canon' && segments[index + 1] === 'tmp',
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isWithin(path: string, root: string): boolean {
|
|
172
|
+
const rel = relative(root, path)
|
|
173
|
+
return (
|
|
174
|
+
rel === '' ||
|
|
175
|
+
(rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel))
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Whether a row still names a project worth listing: a folder that exists and
|
|
181
|
+
* sits under no scratch root. The read filter and the write prune both call
|
|
182
|
+
* this, so the two can never disagree on a row.
|
|
183
|
+
*/
|
|
184
|
+
function isLive(row: TargetRecord, roots: readonly string[]): boolean {
|
|
185
|
+
return existsSync(row.path) && !isScratchPath(row.path, roots)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Reads every recorded target that is still live, sorted by path.
|
|
120
190
|
*
|
|
121
191
|
* A row missing either field is dropped rather than reported. This module is
|
|
122
192
|
* the file's only writer, so a malformed row is a hand edit or a truncated
|
|
123
193
|
* write and neither is a finding the caller can act on. What a caller can act
|
|
124
194
|
* on is the file being absent, which is its own kind above.
|
|
195
|
+
*
|
|
196
|
+
* A row whose folder is gone or under a scratch root is left out too, so the
|
|
197
|
+
* list is right on a machine whose file predates the prune, before any sync
|
|
198
|
+
* rewrites it. A target on a drive that is unmounted reads as gone here, and
|
|
199
|
+
* its next sync records it again.
|
|
125
200
|
*/
|
|
126
201
|
export function readTargetRegistry(
|
|
127
202
|
path: string = registryPath(),
|
|
203
|
+
roots: readonly string[] = scratchRoots(),
|
|
128
204
|
): TargetRegistry {
|
|
129
205
|
let text: string
|
|
130
206
|
try {
|
|
@@ -150,38 +226,46 @@ export function readTargetRegistry(
|
|
|
150
226
|
.filter((row): row is TargetRecord =>
|
|
151
227
|
isRecord(row as Partial<TargetRecord>),
|
|
152
228
|
)
|
|
229
|
+
.filter((row) => isLive(row, roots))
|
|
153
230
|
.sort((a, b) => a.path.localeCompare(b.path))
|
|
154
231
|
|
|
155
232
|
return { kind: 'read', path, targets }
|
|
156
233
|
}
|
|
157
234
|
|
|
158
|
-
/**
|
|
159
|
-
|
|
235
|
+
/**
|
|
236
|
+
* Why a record attempt did not land, so a caller can say so rather than assume
|
|
237
|
+
* it did. `scratch` is a deliberate refusal of a path under a scratch root,
|
|
238
|
+
* not a failure.
|
|
239
|
+
*/
|
|
240
|
+
export type RecordOutcome = 'recorded' | 'unwritten' | 'scratch'
|
|
160
241
|
|
|
161
242
|
/**
|
|
162
243
|
* Records one target, keyed by its resolved path and replacing any row already
|
|
163
|
-
* held for it.
|
|
244
|
+
* held for it, and refuses a path under a scratch root.
|
|
164
245
|
*
|
|
165
246
|
* The write is a temp file plus a rename, so a reader never meets a half
|
|
166
247
|
* written file. Two syncs finishing together still resolve last-writer-wins on
|
|
167
248
|
* the merged set, which can drop the row the loser added. That is left rather
|
|
168
249
|
* than locked: the authoritative record of an install is the stamp inside the
|
|
169
250
|
* target, this index is a cache over those, and the next sync of the dropped
|
|
170
|
-
* target restores its row.
|
|
251
|
+
* target restores its row. The prune reads the same merged set the write
|
|
252
|
+
* already reads, so it adds no second read and does not widen that window.
|
|
171
253
|
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* never opens a recorded path, and the count is its whole output.
|
|
254
|
+
* Every write also drops the rows `readTargetRegistry` leaves out, being a
|
|
255
|
+
* folder that is gone or one under a scratch root, so the file shrinks back to
|
|
256
|
+
* the live projects rather than growing with every sandbox run. A target on a
|
|
257
|
+
* drive that is unmounted drops with them, and its next sync restores it.
|
|
177
258
|
*/
|
|
178
259
|
export function recordTarget(
|
|
179
260
|
target: string,
|
|
180
261
|
now: Date,
|
|
181
262
|
path: string = registryPath(),
|
|
263
|
+
roots: readonly string[] = scratchRoots(),
|
|
182
264
|
): RecordOutcome {
|
|
183
265
|
const resolved = resolve(target)
|
|
184
|
-
|
|
266
|
+
if (isScratchPath(resolved, roots)) return 'scratch'
|
|
267
|
+
|
|
268
|
+
const current = readTargetRegistry(path, roots)
|
|
185
269
|
const existing = current.kind === 'read' ? current.targets : []
|
|
186
270
|
|
|
187
271
|
const targets = [
|
|
@@ -220,7 +304,8 @@ export type BackfillOutcome = RecordOutcome | 'no-stamp'
|
|
|
220
304
|
export function backfillTarget(
|
|
221
305
|
target: BackfillTarget,
|
|
222
306
|
path: string = registryPath(),
|
|
307
|
+
roots: readonly string[] = scratchRoots(),
|
|
223
308
|
): BackfillOutcome {
|
|
224
309
|
if (target.stampedAt === null) return 'no-stamp'
|
|
225
|
-
return recordTarget(target.path, new Date(target.stampedAt), path)
|
|
310
|
+
return recordTarget(target.path, new Date(target.stampedAt), path, roots)
|
|
226
311
|
}
|
package/src/targets/resolve.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface ResolveTargetsOptions extends SweepOptions {
|
|
|
45
45
|
/** Roots to walk, supplementing the record rather than replacing it. */
|
|
46
46
|
readonly sweep?: readonly string[]
|
|
47
47
|
readonly registryFile?: string
|
|
48
|
+
/** Folders whose recorded rows are left out, defaulting to `scratchRoots()`. */
|
|
49
|
+
readonly scratchRoots?: readonly string[]
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
/**
|
|
@@ -71,7 +73,7 @@ export async function resolveTargets(
|
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
const file = opts.registryFile ?? registryPath()
|
|
74
|
-
const registry = readTargetRegistry(file)
|
|
76
|
+
const registry = readTargetRegistry(file, opts.scratchRoots)
|
|
75
77
|
|
|
76
78
|
const recorded: KnownTarget[] =
|
|
77
79
|
registry.kind === 'read'
|
|
@@ -14,7 +14,11 @@ const portOffset = Number(process.env.WORKTREE_PORT_OFFSET) || 0
|
|
|
14
14
|
// The base port sits outside the band `astro.config.mjs` serves from, so a card
|
|
15
15
|
// server and a dev server of one worktree never contend, and two worktrees
|
|
16
16
|
// separate here the way they separate there.
|
|
17
|
+
//
|
|
18
|
+
// The dev toolbar is off because this server exists to be captured, and the
|
|
19
|
+
// toolbar paints over the viewport the capture cuts the card from.
|
|
17
20
|
export default defineConfig({
|
|
21
|
+
devToolbar: { enabled: false },
|
|
18
22
|
srcDir: './card-src',
|
|
19
23
|
outDir: './card-dist',
|
|
20
24
|
publicDir: './public',
|
|
@@ -50,8 +50,13 @@ if grep -rl "$MARKER" "$DIST" >/dev/null 2>&1; then
|
|
|
50
50
|
exit 1
|
|
51
51
|
fi
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
# Built pages only, since an image or font may carry the route's name without
|
|
54
|
+
# being the route. The path test covers both formats Astro emits, being
|
|
55
|
+
# og-card/index.html under build.format 'directory' and og-card.html under
|
|
56
|
+
# 'file'. The search runs from inside dist so a project checked out under a
|
|
57
|
+
# directory like blog-card-site cannot match on its own prefix.
|
|
58
|
+
if (cd "$DIST" && find . -ipath '*og-card*' -name '*.html' | grep -q .); then
|
|
59
|
+
echo "A page named for the card route reached $DIST. See card.config.mjs's srcDir." >&2
|
|
55
60
|
exit 1
|
|
56
61
|
fi
|
|
57
62
|
|
|
@@ -64,7 +64,7 @@ The astro stack covers Astro + TypeScript projects, being content sites, marketi
|
|
|
64
64
|
## Social card route
|
|
65
65
|
|
|
66
66
|
- Keep the social card as a route the project owns, so a re-render after a rename is one string edit and a re-capture rather than another `design-identity` pick loop
|
|
67
|
-
- Expect `canon:design-identity` to write the picked composition into the route and capture it through the card server
|
|
67
|
+
- Expect `canon:design-identity` to write the picked composition into the route and capture it through the card server, which runs with Astro's dev toolbar off so it never paints over the card
|
|
68
68
|
- `card.config.mjs` is a second Astro config whose `srcDir` points at `./card-src`, which the main page router never reads, so the published build excludes the card structurally
|
|
69
69
|
- It serves on base port `4421` plus `WORKTREE_PORT_OFFSET`, outside the band `astro.config.mjs` serves from, so a worktree's card and dev servers never contend
|
|
70
70
|
- `bun run card:dev` serves the route and `bun run card:check` runs the exclusion check, which base's `scripts/verify.ts` also runs after its build stage
|
|
@@ -72,6 +72,7 @@ The astro stack covers Astro + TypeScript projects, being content sites, marketi
|
|
|
72
72
|
## Card exclusion and size
|
|
73
73
|
|
|
74
74
|
- `scripts/check-card-exclusion.sh` fails when the route's `<meta name="og-card-marker">` tag reaches `dist/`, and when the route dropped the tag the leak test reads
|
|
75
|
+
- The filename test reads built HTML pages only, under an `og-card` path in either build format, so an image or font may carry the route's name
|
|
75
76
|
- Keep the exclusion a check rather than a convention, since the card server and the capture read the route and the published build must not carry it
|
|
76
77
|
- Declare `.card` at 600x315, half the 1200x630 target, since `canon capture` shoots at a fixed 2x device scale and 1200x630 would capture at 2400x1260
|
|
77
78
|
|
|
@@ -74,10 +74,12 @@ jobs:
|
|
|
74
74
|
# Unconditional: the cache holds browser binaries and nothing apt
|
|
75
75
|
# wrote, so gating this on a cache hit leaves a warm run launching
|
|
76
76
|
# chromium without the libraries it needs.
|
|
77
|
-
|
|
77
|
+
timeout-minutes: 10
|
|
78
|
+
run: bash scripts/install-playwright-deps.sh chromium
|
|
78
79
|
|
|
79
80
|
- name: Install Playwright Browser
|
|
80
81
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
82
|
+
timeout-minutes: 5
|
|
81
83
|
run: bunx playwright install chromium
|
|
82
84
|
|
|
83
85
|
- name: Capture README Screenshot
|
|
@@ -72,7 +72,7 @@ jobs:
|
|
|
72
72
|
- name: Install Playwright System Dependencies
|
|
73
73
|
if: steps.browser.outputs.enabled == 'true'
|
|
74
74
|
timeout-minutes: 10
|
|
75
|
-
run:
|
|
75
|
+
run: bash scripts/install-playwright-deps.sh chromium
|
|
76
76
|
|
|
77
77
|
- name: Cache Playwright Browsers
|
|
78
78
|
id: playwright-cache
|
|
@@ -84,6 +84,7 @@ jobs:
|
|
|
84
84
|
|
|
85
85
|
- name: Install Playwright Chromium
|
|
86
86
|
if: steps.browser.outputs.enabled == 'true' && steps.playwright-cache.outputs.cache-hit != 'true'
|
|
87
|
+
timeout-minutes: 5
|
|
87
88
|
run: bunx playwright install chromium
|
|
88
89
|
|
|
89
90
|
- name: Run Tests
|
|
@@ -151,7 +152,8 @@ jobs:
|
|
|
151
152
|
path: dist/
|
|
152
153
|
|
|
153
154
|
- name: Install Playwright System Dependencies
|
|
154
|
-
|
|
155
|
+
timeout-minutes: 10
|
|
156
|
+
run: bash scripts/install-playwright-deps.sh chromium
|
|
155
157
|
|
|
156
158
|
- name: Cache Playwright Browsers
|
|
157
159
|
id: playwright-cache
|
|
@@ -162,6 +164,7 @@ jobs:
|
|
|
162
164
|
|
|
163
165
|
- name: Install Playwright Chromium
|
|
164
166
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
167
|
+
timeout-minutes: 5
|
|
165
168
|
run: bunx playwright install chromium
|
|
166
169
|
|
|
167
170
|
- name: Run E2E Tests
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Installs the Playwright system packages for the named browsers, killing an
|
|
3
|
+
# attempt that outlives its bound and retrying it. A degraded apt mirror hangs
|
|
4
|
+
# one connection rather than failing it, so a step timeout alone turns the
|
|
5
|
+
# hang into a red check and never recovers it, while a fresh attempt usually
|
|
6
|
+
# lands on a healthy backend.
|
|
7
|
+
#
|
|
8
|
+
# Usage: install-playwright-deps.sh [--package <spec>] <browser>...
|
|
9
|
+
# Exits 0 on the first success, 2 when no browser is named, and the last
|
|
10
|
+
# attempt's code when every attempt fails.
|
|
11
|
+
set -e
|
|
12
|
+
|
|
13
|
+
package=playwright
|
|
14
|
+
if [ "$1" = "--package" ]; then
|
|
15
|
+
package=$2
|
|
16
|
+
shift 2
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
if [ $# -eq 0 ]; then
|
|
20
|
+
echo "Usage: install-playwright-deps.sh [--package <spec>] <browser>..." >&2
|
|
21
|
+
exit 2
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
attempts=3
|
|
25
|
+
bound=${INSTALL_ATTEMPT_SECONDS:-150}
|
|
26
|
+
|
|
27
|
+
# The runner image sets these, yet a stalled log shows gaps of minutes, so
|
|
28
|
+
# print what apt actually reads to settle whether they took effect
|
|
29
|
+
apt-config dump | grep -E '^Acquire::(Retries|http::Timeout) ' || true
|
|
30
|
+
|
|
31
|
+
status=0
|
|
32
|
+
for attempt in $(seq 1 "$attempts"); do
|
|
33
|
+
if [ "$attempt" -gt 1 ]; then
|
|
34
|
+
# timeout runs as the runner user, and Linux drops its signal to the root
|
|
35
|
+
# apt-get playwright spawns through sudo. sudo relays it only to its own
|
|
36
|
+
# child shell, so a root apt-get or dpkg can outlive the kill and hold the
|
|
37
|
+
# lock the next attempt needs.
|
|
38
|
+
sudo pkill -KILL -x 'apt-get|dpkg' || true
|
|
39
|
+
# A kill during unpack leaves dpkg interrupted, which refuses the next
|
|
40
|
+
# install. Its own failure must not end the loop.
|
|
41
|
+
sudo dpkg --configure -a || echo "dpkg --configure -a failed before attempt ${attempt}" >&2
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
status=0
|
|
45
|
+
timeout --kill-after=10 "$bound" bunx "$package" install-deps "$@" || status=$?
|
|
46
|
+
if [ "$status" -eq 0 ]; then
|
|
47
|
+
exit 0
|
|
48
|
+
fi
|
|
49
|
+
echo "attempt ${attempt}/${attempts} failed with exit ${status}" >&2
|
|
50
|
+
done
|
|
51
|
+
|
|
52
|
+
exit "$status"
|
package/tooling/web/reference.md
CHANGED
|
@@ -88,6 +88,11 @@ The starter `e2e/home.spec.ts` is seeded once. Sync copies it into a project tha
|
|
|
88
88
|
- `.github/workflows/verify.yml` runs `static-checks`, `unit-tests`, `build-verify`, and `e2e-tests`. `unit-tests` installs Chromium and runs `test:browser` only when the project defines that script.
|
|
89
89
|
- Read `governance/rules/tooling/ci-workflow.md` for when a workflow's commit-back is right and when a report is
|
|
90
90
|
|
|
91
|
+
## Playwright installs in CI
|
|
92
|
+
|
|
93
|
+
- Both workflows install Playwright's system packages through `scripts/install-playwright-deps.sh`, which sits beside `worktree-port.sh` and retries an attempt a stalled apt mirror hangs
|
|
94
|
+
- Expect each system install bounded at 10 minutes and each browser download at 5, while the jobs carry no bound, since test and build time differs per project
|
|
95
|
+
|
|
91
96
|
## Design references
|
|
92
97
|
|
|
93
98
|
- Expect a seeded `canon/design/index.md` stub carrying `auto: false`, copied only when missing, so a project's own index is never replaced
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Fake gh for src/commands/pr.test.ts. Reads FAKE_GH_FX, FAKE_GH_COMMENTS, and FAKE_GH_API_LOG.
|
|
3
|
-
set -o pipefail
|
|
4
|
-
fx="$FAKE_GH_FX"
|
|
5
|
-
f=.
|
|
6
|
-
p=
|
|
7
|
-
for a in "$@"; do
|
|
8
|
-
[ "$p" = --jq ] && f=$a
|
|
9
|
-
p=$a
|
|
10
|
-
done
|
|
11
|
-
case "$*" in
|
|
12
|
-
*graphql* | "pr "* | "repo view"*) exit 1 ;;
|
|
13
|
-
api*pulls/7/files*)
|
|
14
|
-
echo "$*" >>"$fx/files-calls.log"
|
|
15
|
-
[ -f "$fx/files-fail" ] && exit 1
|
|
16
|
-
{
|
|
17
|
-
if [ -f "$fx/files.tsv" ]; then
|
|
18
|
-
cat "$fx/files.tsv"
|
|
19
|
-
else
|
|
20
|
-
git diff --name-status --no-renames main...HEAD | sed -e 's/^A/added/' -e 's/^M/modified/' -e 's/^D/removed/'
|
|
21
|
-
fi
|
|
22
|
-
} | jq -Rn '[inputs | select(length > 0) | split("\t") | {status: .[0], filename: .[1]}]' | jq -r "$f"
|
|
23
|
-
;;
|
|
24
|
-
api*git/trees/*)
|
|
25
|
-
[ -f "$fx/base-tree.json" ] || exit 1
|
|
26
|
-
jq -r "$f" "$fx/base-tree.json"
|
|
27
|
-
;;
|
|
28
|
-
api*contents/*)
|
|
29
|
-
a="$*"
|
|
30
|
-
p=${a##*contents/}
|
|
31
|
-
p=${p%%\?*}
|
|
32
|
-
r=${a##*ref=}
|
|
33
|
-
[ -f "$fx/img/$r-$(basename "$p")" ] && cat "$fx/img/$r-$(basename "$p")" || exit 1
|
|
34
|
-
;;
|
|
35
|
-
api*compare/*)
|
|
36
|
-
if [ -f "$fx/merge-base" ]; then cat "$fx/merge-base"; else git merge-base main HEAD; fi
|
|
37
|
-
;;
|
|
38
|
-
"api -X PUT repos/{owner}/{repo}/pulls/7/reviews/"*)
|
|
39
|
-
id=${4##*/}
|
|
40
|
-
echo "$id" >>"$fx/review-puts.log"
|
|
41
|
-
[ -f "$fx/review-put-fail-$id" ] && exit 1
|
|
42
|
-
printf '%s\n' "${@: -1}" >>"$fx/review-bodies.log"
|
|
43
|
-
echo "{}"
|
|
44
|
-
;;
|
|
45
|
-
api*pulls/7/reviews*) [ -f "$fx/ui-reviews.json" ] && jq -r "$f" "$fx/ui-reviews.json" ;;
|
|
46
|
-
api*issues/7/comments*) jq '[.comments[] | {html_url: .url, body}]' "$FAKE_GH_COMMENTS" | jq -r "$f" ;;
|
|
47
|
-
"api -X PATCH repos/{owner}/{repo}/pulls/7 "*)
|
|
48
|
-
[ -f "$fx/body-fail" ] && exit 1
|
|
49
|
-
printf '%s' "${@: -1}" >"$fx/body-write.log"
|
|
50
|
-
echo "{}"
|
|
51
|
-
;;
|
|
52
|
-
"api repos/{owner}/{repo}/pulls/7")
|
|
53
|
-
if [ -f "$fx/head" ]; then h=$(cat "$fx/head"); else h=$(git rev-parse HEAD); fi
|
|
54
|
-
b=
|
|
55
|
-
[ -f "$fx/pr-body" ] && b=$(cat "$fx/pr-body")
|
|
56
|
-
jq -n --arg h "$h" --arg b "$b" '{number:7,head:{ref:"feat/x",sha:$h},base:{ref:"main"},mergeable_state:"clean",body:$b}'
|
|
57
|
-
;;
|
|
58
|
-
"api repos/{owner}/{repo}") echo "{\"full_name\":\"o/r\"}" ;;
|
|
59
|
-
api*)
|
|
60
|
-
printf '%s' "${@: -1}" >"$FAKE_GH_API_LOG"
|
|
61
|
-
echo "{}"
|
|
62
|
-
;;
|
|
63
|
-
*) exit 1 ;;
|
|
64
|
-
esac
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<title>Busy</title>
|
|
6
|
-
<!--
|
|
7
|
-
A hero canvas that redraws every frame and spends busy=<ms> of main
|
|
8
|
-
thread work on each, so a frame takes about that long to render. A card
|
|
9
|
-
sits about 2900px down, below the window, and the rail link scrolls to it.
|
|
10
|
-
With timed=1 each frame spins on performance.now() instead, as the
|
|
11
|
-
groundwork page did, which never ends once the stepper pauses virtual
|
|
12
|
-
time.
|
|
13
|
-
-->
|
|
14
|
-
<style>
|
|
15
|
-
html {
|
|
16
|
-
scroll-behavior: smooth;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
body {
|
|
20
|
-
margin: 0;
|
|
21
|
-
font-family: sans-serif;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
nav {
|
|
25
|
-
position: fixed;
|
|
26
|
-
top: 10px;
|
|
27
|
-
right: 10px;
|
|
28
|
-
z-index: 2;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
nav a {
|
|
32
|
-
display: block;
|
|
33
|
-
padding: 12px 20px;
|
|
34
|
-
background: #fff;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
canvas {
|
|
38
|
-
display: block;
|
|
39
|
-
width: 100vw;
|
|
40
|
-
height: 100vh;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
section {
|
|
44
|
-
height: 900px;
|
|
45
|
-
border-top: 1px solid #ccc;
|
|
46
|
-
padding: 40px;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
.card {
|
|
50
|
-
width: 400px;
|
|
51
|
-
height: 240px;
|
|
52
|
-
background: #ddd;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.card:hover {
|
|
56
|
-
background: #f60;
|
|
57
|
-
}
|
|
58
|
-
</style>
|
|
59
|
-
</head>
|
|
60
|
-
<body>
|
|
61
|
-
<nav><a id="rail" href="#cards">Cards</a></nav>
|
|
62
|
-
<canvas id="hero"></canvas>
|
|
63
|
-
<section>one</section>
|
|
64
|
-
<section>two</section>
|
|
65
|
-
<section id="cards"><div class="card" id="card">card</div></section>
|
|
66
|
-
<section>four</section>
|
|
67
|
-
<script>
|
|
68
|
-
const params = new URLSearchParams(location.search)
|
|
69
|
-
const busy = Number(params.get('busy') || 0)
|
|
70
|
-
const timed = params.get('timed') === '1'
|
|
71
|
-
const hero = document.getElementById('hero')
|
|
72
|
-
hero.width = innerWidth
|
|
73
|
-
hero.height = innerHeight
|
|
74
|
-
const context = hero.getContext('2d')
|
|
75
|
-
// The work is a count calibrated in real time on load, since a loop
|
|
76
|
-
// timed by performance.now() never ends under a paused virtual clock.
|
|
77
|
-
let sink = 0
|
|
78
|
-
const spin = (count) => {
|
|
79
|
-
for (let i = 0; i < count; i += 1) sink += Math.sqrt(i)
|
|
80
|
-
}
|
|
81
|
-
const measure = (ms) => {
|
|
82
|
-
let count = 0
|
|
83
|
-
const start = performance.now()
|
|
84
|
-
while (performance.now() - start < ms) {
|
|
85
|
-
spin(10000)
|
|
86
|
-
count += 10000
|
|
87
|
-
}
|
|
88
|
-
return count / ms
|
|
89
|
-
}
|
|
90
|
-
// Warmed first, so the count matches the optimized loop a frame runs.
|
|
91
|
-
measure(100)
|
|
92
|
-
const work = Math.round(busy * measure(200))
|
|
93
|
-
const draw = (time) => {
|
|
94
|
-
if (timed) {
|
|
95
|
-
const start = performance.now()
|
|
96
|
-
while (performance.now() - start < busy) {}
|
|
97
|
-
} else {
|
|
98
|
-
spin(work)
|
|
99
|
-
}
|
|
100
|
-
context.fillStyle = `hsl(${(time / 10) % 360}, 60%, 50%)`
|
|
101
|
-
context.fillRect(0, 0, hero.width, hero.height)
|
|
102
|
-
requestAnimationFrame(draw)
|
|
103
|
-
}
|
|
104
|
-
requestAnimationFrame(draw)
|
|
105
|
-
</script>
|
|
106
|
-
</body>
|
|
107
|
-
</html>
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<title>Fade</title>
|
|
6
|
-
<!--
|
|
7
|
-
A band that fades from black to white over exactly one second of page
|
|
8
|
-
time once the button is pressed, so its brightness frame by frame reads
|
|
9
|
-
how much page time each frame of a take advanced.
|
|
10
|
-
-->
|
|
11
|
-
<style>
|
|
12
|
-
body {
|
|
13
|
-
margin: 0;
|
|
14
|
-
background: #808080;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
#go {
|
|
18
|
-
position: absolute;
|
|
19
|
-
left: 100px;
|
|
20
|
-
top: 100px;
|
|
21
|
-
width: 200px;
|
|
22
|
-
height: 60px;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
#band {
|
|
26
|
-
position: absolute;
|
|
27
|
-
left: 0;
|
|
28
|
-
top: 300px;
|
|
29
|
-
width: 1920px;
|
|
30
|
-
height: 780px;
|
|
31
|
-
background-color: #000;
|
|
32
|
-
transition: background-color 1000ms linear;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
#band.lit {
|
|
36
|
-
background-color: #fff;
|
|
37
|
-
}
|
|
38
|
-
</style>
|
|
39
|
-
</head>
|
|
40
|
-
<body>
|
|
41
|
-
<button id="go">Go</button>
|
|
42
|
-
<div id="band"></div>
|
|
43
|
-
<script>
|
|
44
|
-
document.getElementById('go').addEventListener('click', () => {
|
|
45
|
-
document.getElementById('band').classList.add('lit')
|
|
46
|
-
})
|
|
47
|
-
</script>
|
|
48
|
-
</body>
|
|
49
|
-
</html>
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<title>Fetch</title>
|
|
6
|
-
<!--
|
|
7
|
-
A button whose press starts a fetch the server answers slowly, the way
|
|
8
|
-
a page switch in an app loads what the next view shows. A take must keep
|
|
9
|
-
stepping frames while the fetch is in flight.
|
|
10
|
-
-->
|
|
11
|
-
<style>
|
|
12
|
-
body {
|
|
13
|
-
margin: 0;
|
|
14
|
-
background: #808080;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
#go {
|
|
18
|
-
position: absolute;
|
|
19
|
-
left: 100px;
|
|
20
|
-
top: 100px;
|
|
21
|
-
width: 200px;
|
|
22
|
-
height: 60px;
|
|
23
|
-
}
|
|
24
|
-
</style>
|
|
25
|
-
</head>
|
|
26
|
-
<body>
|
|
27
|
-
<button id="go">Load</button>
|
|
28
|
-
<p id="status">idle</p>
|
|
29
|
-
<script>
|
|
30
|
-
document.getElementById('go').addEventListener('click', () => {
|
|
31
|
-
document.getElementById('status').textContent = 'loading'
|
|
32
|
-
fetch('/slow').then(() => {
|
|
33
|
-
document.getElementById('status').textContent = 'loaded'
|
|
34
|
-
})
|
|
35
|
-
})
|
|
36
|
-
</script>
|
|
37
|
-
</body>
|
|
38
|
-
</html>
|