@ethlete/agent-rules 0.1.0-next.3 → 0.1.0-next.30
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/AGENTS.md +28 -0
- package/CHANGELOG.md +463 -0
- package/CLAUDE.md +1 -0
- package/README.md +362 -29
- package/THIRD-PARTY-LICENSES.md +35 -0
- package/content/defaults.json +1 -1
- package/content/git-hooks/post-checkout.sh +10 -0
- package/content/git-hooks/pre-push.sh +5 -0
- package/content/hooks/context-warning.py +871 -110
- package/content/hooks/subagent-model-policy.py +194 -0
- package/content/mods/.claude-plugin/marketplace.json +11 -0
- package/content/mods/plugins/e2e-progress/.claude-plugin/plugin.json +6 -0
- package/content/mods/plugins/e2e-progress/hooks/hooks.json +1 -0
- package/content/mods/plugins/e2e-progress/hooks/register.tsx +442 -0
- package/content/mods/plugins/e2e-progress/tsconfig.json +3 -0
- package/content/mods/plugins/e2e-progress/types/index.d.ts +23 -0
- package/content/output-styles/ste-clarity.md +131 -0
- package/content/rules/agent-sessions-wake-up.md +11 -0
- package/content/rules/app-styling.md +66 -0
- package/content/rules/comments.md +4 -1
- package/content/rules/lint-and-format.md +7 -5
- package/content/rules/nx-layout.md +50 -0
- package/content/rules/reactive-state.md +1 -1
- package/content/rules/styling.md +9 -7
- package/content/rules/subagent-models.md +39 -0
- package/content/skills/agent-sessions/SKILL.md +81 -0
- package/content/skills/agent-sessions/agent-sessions.mjs +492 -0
- package/content/skills/angular-patterns/SKILL.md +30 -7
- package/content/skills/api-source/SKILL.md +156 -0
- package/content/skills/app-testing/SKILL.md +141 -0
- package/content/skills/codex-subagent/SKILL.md +90 -0
- package/content/skills/codex-subagent/codex-agent.mjs +229 -0
- package/content/skills/design-exploration/SKILL.md +657 -0
- package/content/skills/design-exploration/check-story.mjs +139 -0
- package/content/skills/design-exploration/shoot-template.mjs +61 -0
- package/content/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/content/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/content/skills/domain-modeling/SKILL.md +80 -0
- package/content/skills/e2e-triage/SKILL.md +80 -0
- package/content/skills/figma-export/SKILL.md +191 -0
- package/content/skills/figma-export/dump-figma-layers.py +83 -0
- package/content/skills/figma-export/dump-figma-svg.py +235 -0
- package/content/skills/figma-export/measure-template.mjs +87 -0
- package/content/skills/git-commit/SKILL.md +11 -7
- package/content/skills/git-flow/SKILL.md +87 -0
- package/content/skills/grill-with-docs/SKILL.md +13 -0
- package/content/skills/grilling/SKILL.md +51 -0
- package/content/skills/handoff/SKILL.md +125 -5
- package/content/skills/query/SKILL.md +165 -64
- package/content/skills/rxjs-signals/SKILL.md +33 -4
- package/content/skills/sdk-docs/SKILL.md +50 -14
- package/content/skills/sdk-local-build/SKILL.md +26 -8
- package/content/skills/sdk-local-build/sdk-local-baseline.mjs +64 -0
- package/content/skills/sdk-source/SKILL.md +37 -34
- package/content/skills/sdk-update/SKILL.md +103 -0
- package/content/skills/story-styling/SKILL.md +10 -8
- package/content/skills/styleguide/SKILL.md +12 -7
- package/content/skills/styleguide/assets.md +54 -0
- package/content/skills/styleguide/changesets.md +17 -0
- package/content/skills/styleguide/file-structure.md +73 -0
- package/content/skills/styleguide/lint-rule-lookup.md +51 -0
- package/content/skills/styleguide/storybook-structure.md +25 -0
- package/content/skills/theming/SKILL.md +50 -28
- package/content/skills/timetrack/SKILL.md +397 -0
- package/content/skills/verify-in-app/SKILL.md +107 -0
- package/migrations/app-styling-utilities.md +63 -0
- package/migrations/list-state-query-form.md +20 -0
- package/migrations/nx-layout.md +29 -0
- package/migrations/sdk-components-over-hand-built-ui.md +27 -0
- package/migrations/search-query-field.md +52 -0
- package/migrations.json +48 -0
- package/package.json +19 -3
- package/src/index.js +85 -30
- package/src/index.js.map +1 -1
- package/src/lib/cli-args.d.ts +9 -0
- package/src/lib/cli-args.js +48 -0
- package/src/lib/cli-args.js.map +1 -0
- package/src/lib/commitlint.d.ts +10 -0
- package/src/lib/commitlint.js +51 -0
- package/src/lib/commitlint.js.map +1 -0
- package/src/lib/config.d.ts +41 -5
- package/src/lib/config.js +117 -10
- package/src/lib/config.js.map +1 -1
- package/src/lib/filter.d.ts +2 -0
- package/src/lib/filter.js +4 -4
- package/src/lib/filter.js.map +1 -1
- package/src/lib/frontmatter.d.ts +2 -0
- package/src/lib/frontmatter.js +33 -5
- package/src/lib/frontmatter.js.map +1 -1
- package/src/lib/git-flow/build.d.ts +35 -0
- package/src/lib/git-flow/build.js +24 -0
- package/src/lib/git-flow/build.js.map +1 -0
- package/src/lib/git-flow/config.d.ts +59 -0
- package/src/lib/git-flow/config.js +50 -0
- package/src/lib/git-flow/config.js.map +1 -0
- package/src/lib/git-flow/index.d.ts +6 -0
- package/src/lib/git-flow/index.js +23 -0
- package/src/lib/git-flow/index.js.map +1 -0
- package/src/lib/git-flow/parse.d.ts +49 -0
- package/src/lib/git-flow/parse.js +300 -0
- package/src/lib/git-flow/parse.js.map +1 -0
- package/src/lib/git-flow/rename.d.ts +24 -0
- package/src/lib/git-flow/rename.js +70 -0
- package/src/lib/git-flow/rename.js.map +1 -0
- package/src/lib/git-flow/start.d.ts +49 -0
- package/src/lib/git-flow/start.js +57 -0
- package/src/lib/git-flow/start.js.map +1 -0
- package/src/lib/git-flow/validate.d.ts +34 -0
- package/src/lib/git-flow/validate.js +72 -0
- package/src/lib/git-flow/validate.js.map +1 -0
- package/src/lib/git-flow-command.d.ts +4 -0
- package/src/lib/git-flow-command.js +165 -0
- package/src/lib/git-flow-command.js.map +1 -0
- package/src/lib/git-flow-repair.d.ts +17 -0
- package/src/lib/git-flow-repair.js +165 -0
- package/src/lib/git-flow-repair.js.map +1 -0
- package/src/lib/git-flow-start.d.ts +20 -0
- package/src/lib/git-flow-start.js +140 -0
- package/src/lib/git-flow-start.js.map +1 -0
- package/src/lib/git.d.ts +64 -0
- package/src/lib/git.js +131 -0
- package/src/lib/git.js.map +1 -0
- package/src/lib/gitlab.d.ts +35 -0
- package/src/lib/gitlab.js +116 -0
- package/src/lib/gitlab.js.map +1 -0
- package/src/lib/index.d.ts +2 -0
- package/src/lib/index.js +26 -11
- package/src/lib/index.js.map +1 -1
- package/src/lib/migrate.js +6 -1
- package/src/lib/migrate.js.map +1 -1
- package/src/lib/output-style-command.d.ts +3 -0
- package/src/lib/output-style-command.js +69 -0
- package/src/lib/output-style-command.js.map +1 -0
- package/src/lib/output-style.d.ts +38 -0
- package/src/lib/output-style.js +134 -0
- package/src/lib/output-style.js.map +1 -0
- package/src/lib/owned-paths.js +27 -3
- package/src/lib/owned-paths.js.map +1 -1
- package/src/lib/package-runner.d.ts +4 -0
- package/src/lib/package-runner.js +86 -0
- package/src/lib/package-runner.js.map +1 -0
- package/src/lib/plain-text.d.ts +9 -0
- package/src/lib/plain-text.js +22 -0
- package/src/lib/plain-text.js.map +1 -0
- package/src/lib/plan.d.ts +6 -6
- package/src/lib/plan.js +164 -40
- package/src/lib/plan.js.map +1 -1
- package/src/lib/prompt.d.ts +8 -0
- package/src/lib/prompt.js +35 -0
- package/src/lib/prompt.js.map +1 -0
- package/src/lib/render.d.ts +25 -2
- package/src/lib/render.js +43 -10
- package/src/lib/render.js.map +1 -1
- package/src/lib/sessions-command.d.ts +4 -0
- package/src/lib/sessions-command.js +16 -0
- package/src/lib/sessions-command.js.map +1 -0
- package/src/lib/sync.d.ts +0 -1
- package/src/lib/sync.js +30 -17
- package/src/lib/sync.js.map +1 -1
- package/src/lib/targets/agents-skills.js +1 -1
- package/src/lib/targets/agents-skills.js.map +1 -1
- package/src/lib/targets/claude-hooks.d.ts +3 -23
- package/src/lib/targets/claude-hooks.js +22 -86
- package/src/lib/targets/claude-hooks.js.map +1 -1
- package/src/lib/targets/claude-mods.d.ts +20 -0
- package/src/lib/targets/claude-mods.js +47 -0
- package/src/lib/targets/claude-mods.js.map +1 -0
- package/src/lib/targets/claude.js +2 -3
- package/src/lib/targets/claude.js.map +1 -1
- package/src/lib/targets/codex-hooks.d.ts +12 -0
- package/src/lib/targets/codex-hooks.js +35 -0
- package/src/lib/targets/codex-hooks.js.map +1 -0
- package/src/lib/targets/codex.js +4 -4
- package/src/lib/targets/codex.js.map +1 -1
- package/src/lib/targets/copilot.js +4 -4
- package/src/lib/targets/copilot.js.map +1 -1
- package/src/lib/targets/cursor.js +3 -3
- package/src/lib/targets/cursor.js.map +1 -1
- package/src/lib/targets/git-hooks.d.ts +25 -0
- package/src/lib/targets/git-hooks.js +71 -0
- package/src/lib/targets/git-hooks.js.map +1 -0
- package/src/lib/targets/hooks-shared.d.ts +60 -0
- package/src/lib/targets/hooks-shared.js +130 -0
- package/src/lib/targets/hooks-shared.js.map +1 -0
- package/src/lib/targets/mcp.d.ts +16 -0
- package/src/lib/targets/mcp.js +68 -0
- package/src/lib/targets/mcp.js.map +1 -0
- package/src/lib/targets/shared.d.ts +1 -9
- package/src/lib/targets/shared.js +4 -8
- package/src/lib/targets/shared.js.map +1 -1
- package/src/lib/timetrack-command.d.ts +11 -0
- package/src/lib/timetrack-command.js +1063 -0
- package/src/lib/timetrack-command.js.map +1 -0
- package/src/lib/timetrack-snapshot.d.ts +55 -0
- package/src/lib/timetrack-snapshot.js +120 -0
- package/src/lib/timetrack-snapshot.js.map +1 -0
- package/src/lib/timetrack.d.ts +561 -0
- package/src/lib/timetrack.js +279 -0
- package/src/lib/timetrack.js.map +1 -0
- package/content/skills/styleguide/STYLEGUIDE.md +0 -520
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find out why a story does not render, in the cheapest order.
|
|
3
|
+
*
|
|
4
|
+
* A compile error renders as an overlay on top of the story, so a screenshot of it looks
|
|
5
|
+
* like a design. Run this first, and never screenshot before it says `ok`.
|
|
6
|
+
*
|
|
7
|
+
* Copy to the repository root - Node resolves `playwright` from the working directory.
|
|
8
|
+
*
|
|
9
|
+
* node check-story.mjs --lint <file>… --tsconfig <path> # ~4s, no browser
|
|
10
|
+
* node check-story.mjs --tsconfig <path> --story <story-id> # + the render
|
|
11
|
+
* SB_URL=http://localhost:4401 node check-story.mjs --story <story-id>
|
|
12
|
+
*
|
|
13
|
+
* Three stages, because they cost three different amounts:
|
|
14
|
+
*
|
|
15
|
+
* 1. `eslint` on the files you changed, about a second. Name the files - never a whole
|
|
16
|
+
* project, which is far slower and can fight the user's own editor.
|
|
17
|
+
* 2. `tsc --noEmit` reads the source. It needs no dev server, waits for no rebuild, and
|
|
18
|
+
* catches every syntax and type error in about three seconds. Almost everything you
|
|
19
|
+
* break is caught here.
|
|
20
|
+
* 3. The browser catches what the source cannot: an Angular template error, a runtime
|
|
21
|
+
* throw, a missing story id. It costs a browser launch, and it can only see a build the
|
|
22
|
+
* dev server has already finished, so a fresh edit needs a few seconds first.
|
|
23
|
+
*
|
|
24
|
+
* A type error does not stop this repo's Storybook - it transpiles without checking - so
|
|
25
|
+
* stage 2 alone will report `ok` on a file that does not compile. That is why stage 1 exists.
|
|
26
|
+
*/
|
|
27
|
+
import { execFileSync } from 'node:child_process';
|
|
28
|
+
|
|
29
|
+
const BASE = process.env.SB_URL ?? 'http://localhost:4400';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The type checker to run. `tsc` resolves the one the repository installed. Set
|
|
33
|
+
* `TSC=@typescript/native-preview` for the Go port, which is about a third faster - but it
|
|
34
|
+
* rejects a `baseUrl` in the config, so a repository that still uses one cannot run it.
|
|
35
|
+
*/
|
|
36
|
+
const TSC = process.env.TSC ?? null;
|
|
37
|
+
|
|
38
|
+
const flags = { '--lint': [], '--tsconfig': [], '--story': [] };
|
|
39
|
+
let current = null;
|
|
40
|
+
|
|
41
|
+
for (const arg of process.argv.slice(2)) {
|
|
42
|
+
if (arg in flags) current = arg;
|
|
43
|
+
else if (current) flags[current].push(arg);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const lintFiles = flags['--lint'];
|
|
47
|
+
const [tsconfig] = flags['--tsconfig'];
|
|
48
|
+
const [id] = flags['--story'];
|
|
49
|
+
|
|
50
|
+
if (lintFiles.length === 0 && !tsconfig && !id) {
|
|
51
|
+
console.error('usage: node check-story.mjs [--lint <file>…] [--tsconfig <path>] [--story <id>]');
|
|
52
|
+
process.exit(2);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A bundler stack runs to hundreds of frames and says nothing. Keep the message. */
|
|
56
|
+
const trim = (text) =>
|
|
57
|
+
[...new Set(text.split('\n').filter((line) => !/^\s+at /.test(line) && line.trim() !== ''))].slice(0, 12).join('\n');
|
|
58
|
+
|
|
59
|
+
if (lintFiles.length > 0) {
|
|
60
|
+
try {
|
|
61
|
+
execFileSync('npx', ['eslint', ...lintFiles], { encoding: 'utf8', stdio: 'pipe' });
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(`LINT\n${trim(`${error.stdout ?? ''}${error.stderr ?? ''}`)}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (tsconfig) {
|
|
69
|
+
try {
|
|
70
|
+
const cmd = TSC ? ['-y', TSC] : ['tsc'];
|
|
71
|
+
execFileSync('npx', [...cmd, '--noEmit', '-p', tsconfig], { encoding: 'utf8', stdio: 'pipe' });
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error(`TYPE ERROR\n${trim(`${error.stdout ?? ''}${error.stderr ?? ''}`)}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!id) {
|
|
79
|
+
console.log('ok');
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const { chromium } = await import('playwright');
|
|
84
|
+
|
|
85
|
+
const browser = await chromium.launch();
|
|
86
|
+
const page = await browser.newPage({ viewport: { width: 1400, height: 900 } });
|
|
87
|
+
|
|
88
|
+
/** Angular throws at render time; the overlay never shows those, so read the console too. */
|
|
89
|
+
const consoleErrors = [];
|
|
90
|
+
page.on('console', (m) => m.type() === 'error' && consoleErrors.push(m.text()));
|
|
91
|
+
page.on('pageerror', (e) => consoleErrors.push(e.message));
|
|
92
|
+
|
|
93
|
+
await page.goto(`${BASE}/iframe.html?id=${id}&viewMode=story`, { waitUntil: 'domcontentloaded' });
|
|
94
|
+
|
|
95
|
+
const rendered = await page
|
|
96
|
+
.locator('#storybook-root > *')
|
|
97
|
+
.first()
|
|
98
|
+
.waitFor({ timeout: 15000 })
|
|
99
|
+
.then(
|
|
100
|
+
() => true,
|
|
101
|
+
() => false,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
/** The webpack overlay is an iframe, so its text is only reachable through a frame locator. */
|
|
105
|
+
const webpackOverlayText =
|
|
106
|
+
(await page.locator('#webpack-dev-server-client-overlay').count()) > 0
|
|
107
|
+
? await page
|
|
108
|
+
.frameLocator('#webpack-dev-server-client-overlay')
|
|
109
|
+
.locator('body')
|
|
110
|
+
.innerText()
|
|
111
|
+
.catch(() => '(overlay present, text unreadable)')
|
|
112
|
+
: '';
|
|
113
|
+
/** The Vite overlay keeps its text in a shadow root. */
|
|
114
|
+
const viteOverlayText = await page.evaluate(() => {
|
|
115
|
+
const overlay = document.querySelector('vite-error-overlay');
|
|
116
|
+
if (!overlay) return '';
|
|
117
|
+
return overlay.shadowRoot?.textContent?.trim() || '(overlay present, text unreadable)';
|
|
118
|
+
});
|
|
119
|
+
const overlayText = webpackOverlayText || viteOverlayText;
|
|
120
|
+
|
|
121
|
+
await browser.close();
|
|
122
|
+
|
|
123
|
+
if (overlayText) {
|
|
124
|
+
console.error(`COMPILE ERROR on ${id}\n${trim(overlayText)}`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!rendered) {
|
|
129
|
+
console.error(`EMPTY on ${id} - #storybook-root never got a child.`);
|
|
130
|
+
if (consoleErrors.length > 0) console.error(trim(consoleErrors.join('\n')));
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (consoleErrors.length > 0) {
|
|
135
|
+
console.error(`RENDERED WITH ERRORS on ${id}\n${trim(consoleErrors.join('\n'))}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log(`ok ${id}`);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screenshot one Storybook story, with optional forced pseudo-states.
|
|
3
|
+
*
|
|
4
|
+
* Copy to the repository root - Node resolves `playwright` from the working directory.
|
|
5
|
+
*
|
|
6
|
+
* node shoot.mjs <story-id> [width] [height] [out.png]
|
|
7
|
+
*
|
|
8
|
+
* To hold :hover / :focus-visible / :active open at the same time, fill FORCED below with
|
|
9
|
+
* one entry per element. A CSS selector is matched inside the story's document.
|
|
10
|
+
*/
|
|
11
|
+
import { chromium } from 'playwright';
|
|
12
|
+
|
|
13
|
+
const BASE = process.env.SB_URL ?? 'http://localhost:4400';
|
|
14
|
+
|
|
15
|
+
/** @type {{ selector: string, states: ('hover'|'focus'|'focus-visible'|'active')[] }[]} */
|
|
16
|
+
const FORCED = [
|
|
17
|
+
// { selector: '[data-state="hover"] .the-band', states: ['hover'] },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const [id, width = '1100', height = '760', out = 'shot.png'] = process.argv.slice(2);
|
|
21
|
+
|
|
22
|
+
if (!id) {
|
|
23
|
+
console.error('usage: node shoot.mjs <story-id> [width] [height] [out.png]');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const browser = await chromium.launch();
|
|
28
|
+
const page = await browser.newPage({
|
|
29
|
+
viewport: { width: Number(width), height: Number(height) },
|
|
30
|
+
deviceScaleFactor: 2,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
await page.goto(`${BASE}/iframe.html?id=${id}&viewMode=story`, { waitUntil: 'domcontentloaded' });
|
|
34
|
+
await page.waitForSelector('#storybook-root > *', { timeout: 15000 });
|
|
35
|
+
|
|
36
|
+
const overlays = await page.locator('#webpack-dev-server-client-overlay, vite-error-overlay').count();
|
|
37
|
+
if (overlays > 0) {
|
|
38
|
+
console.error('compile error overlay is present - the image would lie');
|
|
39
|
+
await browser.close();
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (FORCED.length > 0) {
|
|
44
|
+
const cdp = await page.context().newCDPSession(page);
|
|
45
|
+
await cdp.send('DOM.enable');
|
|
46
|
+
await cdp.send('CSS.enable');
|
|
47
|
+
const { root } = await cdp.send('DOM.getDocument', { depth: -1 });
|
|
48
|
+
|
|
49
|
+
for (const { selector, states } of FORCED) {
|
|
50
|
+
const { nodeIds } = await cdp.send('DOM.querySelectorAll', { nodeId: root.nodeId, selector });
|
|
51
|
+
for (const nodeId of nodeIds) {
|
|
52
|
+
await cdp.send('CSS.forcePseudoState', { nodeId, forcedPseudoClasses: states });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await page.waitForTimeout(400);
|
|
58
|
+
await page.screenshot({ path: out });
|
|
59
|
+
await browser.close();
|
|
60
|
+
|
|
61
|
+
console.log(out);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# ADR Format
|
|
2
|
+
|
|
3
|
+
ADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc.
|
|
4
|
+
|
|
5
|
+
Create the `docs/adr/` directory lazily: only when the first ADR is needed.
|
|
6
|
+
|
|
7
|
+
## Template
|
|
8
|
+
|
|
9
|
+
```md
|
|
10
|
+
# {Short title of the decision}
|
|
11
|
+
|
|
12
|
+
{1-3 sentences: what's the context, what did we decide, and why.}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
That's it. An ADR can be a single paragraph. The value is in recording _that_ a decision was made and _why_, not in filling out sections.
|
|
16
|
+
|
|
17
|
+
## Optional sections
|
|
18
|
+
|
|
19
|
+
Only include these when they add genuine value. Most ADRs won't need them.
|
|
20
|
+
|
|
21
|
+
- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`): useful when decisions are revisited
|
|
22
|
+
- **Considered Options**: only when the rejected alternatives are worth remembering
|
|
23
|
+
- **Consequences**: only when non-obvious downstream effects need to be called out
|
|
24
|
+
|
|
25
|
+
## Numbering
|
|
26
|
+
|
|
27
|
+
Scan `docs/adr/` for the highest existing number and increment by one.
|
|
28
|
+
|
|
29
|
+
## When to offer an ADR
|
|
30
|
+
|
|
31
|
+
All three of these must be true:
|
|
32
|
+
|
|
33
|
+
1. **Hard to reverse**: the cost of changing your mind later is meaningful
|
|
34
|
+
2. **Surprising without context**: a future reader will look at the code and wonder "why on earth did they do it this way?"
|
|
35
|
+
3. **The result of a real trade-off**: there were genuine alternatives and you picked one for specific reasons
|
|
36
|
+
|
|
37
|
+
If a decision is easy to reverse, skip it: you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond "we did the obvious thing."
|
|
38
|
+
|
|
39
|
+
### What qualifies
|
|
40
|
+
|
|
41
|
+
- **Architectural shape.** "We're using a monorepo." "The write model is event-sourced, the read model is projected into Postgres."
|
|
42
|
+
- **Integration patterns between contexts.** "Ordering and Billing communicate via domain events, not synchronous HTTP."
|
|
43
|
+
- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library: just the ones that would take a quarter to swap out.
|
|
44
|
+
- **Boundary and scope decisions.** "Customer data is owned by the Customer context; other contexts reference it by ID only." The explicit no-s are as valuable as the yes-s.
|
|
45
|
+
- **Deliberate deviations from the obvious path.** "We're using manual SQL instead of an ORM because X." Anything where a reasonable reader would assume the opposite. These stop the next engineer from "fixing" something that was deliberate.
|
|
46
|
+
- **Constraints not visible in the code.** "We can't use AWS because of compliance requirements." "Response times must be under 200ms because of the partner API contract."
|
|
47
|
+
- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it; otherwise someone will suggest GraphQL again in six months.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# CONTEXT.md Format
|
|
2
|
+
|
|
3
|
+
## Structure
|
|
4
|
+
|
|
5
|
+
```md
|
|
6
|
+
# {Context Name}
|
|
7
|
+
|
|
8
|
+
{One or two sentence description of what this context is and why it exists.}
|
|
9
|
+
|
|
10
|
+
## Language
|
|
11
|
+
|
|
12
|
+
**Order**:
|
|
13
|
+
{A one or two sentence description of the term}
|
|
14
|
+
_Avoid_: Purchase, transaction
|
|
15
|
+
|
|
16
|
+
**Invoice**:
|
|
17
|
+
A request for payment sent to a customer after delivery.
|
|
18
|
+
_Avoid_: Bill, payment request
|
|
19
|
+
|
|
20
|
+
**Customer**:
|
|
21
|
+
A person or organization that places orders.
|
|
22
|
+
_Avoid_: Client, buyer, account
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Rules
|
|
26
|
+
|
|
27
|
+
- **Be opinionated.** When multiple words exist for the same concept, pick the best one and list the others under `_Avoid_`.
|
|
28
|
+
- **Keep definitions tight.** One or two sentences max. Define what it IS, not what it does.
|
|
29
|
+
- **Only include terms specific to this project's context.** General programming concepts (timeouts, error types, utility patterns) don't belong even if the project uses them extensively. Before adding a term, ask: is this a concept unique to this context, or a general programming concept? Only the former belongs.
|
|
30
|
+
- **Group terms under subheadings** when natural clusters emerge. If all terms belong to a single cohesive area, a flat list is fine.
|
|
31
|
+
|
|
32
|
+
## Single vs multi-context repos
|
|
33
|
+
|
|
34
|
+
**Single context (most repos):** One `CONTEXT.md` at the repo root.
|
|
35
|
+
|
|
36
|
+
**Multiple contexts:** A `CONTEXT-MAP.md` at the repo root lists the contexts, where they live, and how they relate to each other:
|
|
37
|
+
|
|
38
|
+
```md
|
|
39
|
+
# Context Map
|
|
40
|
+
|
|
41
|
+
## Contexts
|
|
42
|
+
|
|
43
|
+
- [Ordering](./src/ordering/CONTEXT.md): receives and tracks customer orders
|
|
44
|
+
- [Billing](./src/billing/CONTEXT.md): generates invoices and processes payments
|
|
45
|
+
- [Fulfillment](./src/fulfillment/CONTEXT.md): manages warehouse picking and shipping
|
|
46
|
+
|
|
47
|
+
## Relationships
|
|
48
|
+
|
|
49
|
+
- **Ordering → Fulfillment**: Ordering emits `OrderPlaced` events; Fulfillment consumes them to start picking
|
|
50
|
+
- **Fulfillment → Billing**: Fulfillment emits `ShipmentDispatched` events; Billing consumes them to generate invoices
|
|
51
|
+
- **Ordering ↔ Billing**: Shared types for `CustomerId` and `Money`
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The skill infers which structure applies:
|
|
55
|
+
|
|
56
|
+
- If `CONTEXT-MAP.md` exists, read it to find contexts
|
|
57
|
+
- If only a root `CONTEXT.md` exists, single context
|
|
58
|
+
- If neither exists, create a root `CONTEXT.md` lazily when the first term is resolved
|
|
59
|
+
|
|
60
|
+
When multiple contexts exist, infer which one the current topic relates to. If unclear, ask.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: domain-modeling
|
|
3
|
+
description: Build and sharpen a project's domain model. Use when discussing codebase terminology, writing or editing a CONTEXT.md, or recording or editing an ADR.
|
|
4
|
+
kind: skill
|
|
5
|
+
scope: both
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Domain Modeling
|
|
9
|
+
|
|
10
|
+
Actively build and sharpen the project's domain model as you design. This is the _active_ discipline: challenging terms, inventing edge-case scenarios, and writing the glossary and decisions down the moment they crystallise. (Merely _reading_ `CONTEXT.md` for vocabulary is not this skill: that's a one-line habit any skill can do. This skill is for when you're changing the model, not just consuming it.)
|
|
11
|
+
|
|
12
|
+
## File structure
|
|
13
|
+
|
|
14
|
+
Most repos have a single context:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
/
|
|
18
|
+
├── CONTEXT.md
|
|
19
|
+
├── docs/
|
|
20
|
+
│ └── adr/
|
|
21
|
+
│ ├── 0001-event-sourced-orders.md
|
|
22
|
+
│ └── 0002-postgres-for-write-model.md
|
|
23
|
+
└── src/
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
/
|
|
30
|
+
├── CONTEXT-MAP.md
|
|
31
|
+
├── docs/
|
|
32
|
+
│ └── adr/ ← system-wide decisions
|
|
33
|
+
├── src/
|
|
34
|
+
│ ├── ordering/
|
|
35
|
+
│ │ ├── CONTEXT.md
|
|
36
|
+
│ │ └── docs/adr/ ← context-specific decisions
|
|
37
|
+
│ └── billing/
|
|
38
|
+
│ ├── CONTEXT.md
|
|
39
|
+
│ └── docs/adr/
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Create files lazily: only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed.
|
|
43
|
+
|
|
44
|
+
## During the session
|
|
45
|
+
|
|
46
|
+
### Challenge against the glossary
|
|
47
|
+
|
|
48
|
+
When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y. Which is it?"
|
|
49
|
+
|
|
50
|
+
### Sharpen fuzzy language
|
|
51
|
+
|
|
52
|
+
When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account': do you mean the Customer or the User? Those are different things."
|
|
53
|
+
|
|
54
|
+
### Discuss concrete scenarios
|
|
55
|
+
|
|
56
|
+
When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts.
|
|
57
|
+
|
|
58
|
+
### Cross-reference with code
|
|
59
|
+
|
|
60
|
+
When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible. Which is right?"
|
|
61
|
+
|
|
62
|
+
### Update CONTEXT.md inline
|
|
63
|
+
|
|
64
|
+
When a term is resolved, update `CONTEXT.md` right there. Don't batch these up: capture them as they happen. Use the format in {%resource:CONTEXT-FORMAT.md%}.
|
|
65
|
+
|
|
66
|
+
`CONTEXT.md` should be totally devoid of implementation details. Do not treat `CONTEXT.md` as a spec, a scratch pad, or a repository for implementation decisions. It is a glossary and nothing else.
|
|
67
|
+
|
|
68
|
+
### Offer ADRs sparingly
|
|
69
|
+
|
|
70
|
+
Only offer to create an ADR when all three are true:
|
|
71
|
+
|
|
72
|
+
1. **Hard to reverse**: the cost of changing your mind later is meaningful
|
|
73
|
+
2. **Surprising without context**: a future reader will wonder "why did they do it this way?"
|
|
74
|
+
3. **The result of a real trade-off**: there were genuine alternatives and you picked one for specific reasons
|
|
75
|
+
|
|
76
|
+
If any of the three is missing, skip the ADR. Use the format in {%resource:ADR-FORMAT.md%}.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
Vendored from [mattpocock/skills](https://github.com/mattpocock/skills) at `3cca18b`, MIT (Copyright (c) 2026 Matt Pocock).
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: e2e-triage
|
|
3
|
+
description: Work through a failed Playwright e2e run - read the failures file the @ethlete/cli failure reporter writes, group the failures by error, fix the largest group first, and re-run only the specs that failed. Read when an e2e run fails with more than a few tests, when an `e2e-failures.jsonl` file is present, or before you start a full e2e run after a change.
|
|
4
|
+
kind: skill
|
|
5
|
+
scope: consumer
|
|
6
|
+
requires: ['@ethlete/cli']
|
|
7
|
+
vars: [docsBaseUrl]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Triage a failed e2e run
|
|
11
|
+
|
|
12
|
+
Many failed tests rarely mean many bugs. In one real run, a single change in a shared pipe failed 61
|
|
13
|
+
tests. After the fix, 158 of the first 159 retried tests passed. So find the shared cause first, and
|
|
14
|
+
never fix failures one test at a time from the top of the log.
|
|
15
|
+
|
|
16
|
+
## Before the run
|
|
17
|
+
|
|
18
|
+
1. Check that `playwright.config.ts` lists `@ethlete/cli/playwright/failure-reporter` in `reporter`.
|
|
19
|
+
If not, add it next to the reporter that is there: `[['list'], ['@ethlete/cli/playwright/failure-reporter']]`.
|
|
20
|
+
The reporter writes one JSON line per failed attempt while the run goes on. The guide is
|
|
21
|
+
{%docsBaseUrl%}/cli/e2e.
|
|
22
|
+
2. Run the specs of the files you changed first. Start the full suite only when they pass. A full
|
|
23
|
+
run takes long, and one broken shared file fails it everywhere.
|
|
24
|
+
|
|
25
|
+
## Read the failures
|
|
26
|
+
|
|
27
|
+
The file is `e2e-failures.jsonl` next to `playwright.config.ts`, unless `E2E_FAILURES_FILE` or the
|
|
28
|
+
`outputFile` option of the reporter names another path. You can read it while the run goes on.
|
|
29
|
+
|
|
30
|
+
Each line has `test`, `location`, `project`, `status`, `retry`, `seconds`, `error`, `detail` and `context`.
|
|
31
|
+
`error` holds the first lines of the message. `detail` holds the first later line with a URL, for errors whose first lines do not name it, or `null`. `context` is the path of Playwright's
|
|
32
|
+
`error-context.md`, which holds the page snapshot at the failure.
|
|
33
|
+
|
|
34
|
+
Do not parse the console log of the run. It mixes the output of parallel workers, and it has no end
|
|
35
|
+
line if the run stops.
|
|
36
|
+
|
|
37
|
+
## Group, then fix the largest group
|
|
38
|
+
|
|
39
|
+
1. Normalize the `error` text of each line. Remove line and column numbers, timeouts in ms, ids,
|
|
40
|
+
hashes and dates. Keep the matcher, the locator and the expected value.
|
|
41
|
+
2. Group the lines by the normalized text. Count the distinct spec files per group, not only the lines.
|
|
42
|
+
A group that spans many spec files points at shared code: a component, a pipe, a fixture or a
|
|
43
|
+
test helper.
|
|
44
|
+
3. Take the largest group. Read one `context` file of it, then the code the error points at. Find
|
|
45
|
+
one cause that explains the whole group.
|
|
46
|
+
4. Fix that cause. Then re-run only the spec files of that group.
|
|
47
|
+
|
|
48
|
+
A group whose errors are a connection refused (`ERR_CONNECTION_REFUSED`, `ECONNREFUSED`) on a local mock or
|
|
49
|
+
`webServer` port is an infrastructure group, not a test verdict. Check that the server is still up,
|
|
50
|
+
restart it, and re-run those tests before you read them as failures.
|
|
51
|
+
|
|
52
|
+
Small groups that differ only in a locator often have the same cause. Check them after the large group
|
|
53
|
+
is fixed, because a fix often clears them too.
|
|
54
|
+
|
|
55
|
+
## Re-run only what failed
|
|
56
|
+
|
|
57
|
+
Take the spec file from `location`, without the `:line`. Collect each file once, then run:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx playwright test <file> <file> --project=<project> --no-deps
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- Pass each file as its own argument. In zsh, an unquoted `$files` is one argument, and Playwright
|
|
64
|
+
then finds no tests.
|
|
65
|
+
- `--no-deps` skips the setup projects. Leave it out if the failure is in a setup project, or if the
|
|
66
|
+
setup state is gone.
|
|
67
|
+
- `--project` comes from the `project` field. Run one call per project.
|
|
68
|
+
|
|
69
|
+
## Retries and flaky tests
|
|
70
|
+
|
|
71
|
+
With `retries` on, a test that fails each attempt has one line per attempt (`retry` 0, 1, and so on).
|
|
72
|
+
A test with a line for `retry` 0 and no line for the last retry passed on a later attempt: it is flaky.
|
|
73
|
+
Report a flaky test, but fix the stable failures first.
|
|
74
|
+
|
|
75
|
+
## Rules
|
|
76
|
+
|
|
77
|
+
- Never skip a failed test, or mark it with `test.fail()` or `test.fixme()`, to make a run pass.
|
|
78
|
+
- Never change an assertion to match the wrong value the app shows now. A failed e2e test is a
|
|
79
|
+
signal first. If you think the test is wrong, say why, and ask before you change it.
|
|
80
|
+
- Report per group: the cause, the fix, the spec files you re-ran, and what still fails.
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: figma-export
|
|
3
|
+
description: Reconcile a component against a Figma export - which exports to ask the designer for (an .svg frame and its "copy as CSS" dump), how to dump the geometry out of each, which of their numbers are authoritative, and how to measure the real rendered result against them headlessly. Use whenever a design export is dropped next to a component and the component has to be matched to it.
|
|
4
|
+
kind: skill
|
|
5
|
+
scope: both
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Reconcile a component against a Figma export
|
|
9
|
+
|
|
10
|
+
Three things arrive from Figma, and each answers a different question:
|
|
11
|
+
|
|
12
|
+
| Export | Gives you | Cannot tell you |
|
|
13
|
+
| ------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------- |
|
|
14
|
+
| **`.svg`** (Export frame) | Exact geometry with nesting, exact fills, and a picture once you rasterise it | Layer names, auto-layout intent, font sizes |
|
|
15
|
+
| **`.css`** (Copy as CSS) | Named layers, auto-layout properties, typography metrics, design-token names | Any hierarchy at all — the dump is flat |
|
|
16
|
+
| **`.png`** (a screenshot) | Figma's own blue measurement overlays, and what the designer chose to frame | Nothing machine-readable |
|
|
17
|
+
|
|
18
|
+
**Ask for the `.svg` and the `.css` together.** That pair is the preferred complete input:
|
|
19
|
+
the SVG is the export you can look at and measure, and the CSS names layers and records
|
|
20
|
+
type. A PNG earns its place only when it is a _screenshot_ carrying dev-mode annotations.
|
|
21
|
+
Exports expose rendered color values, but they do not identify the authoritative semantic
|
|
22
|
+
theme token.
|
|
23
|
+
|
|
24
|
+
If one artifact is missing, inspect the existing code and the artifact you do have. List
|
|
25
|
+
the exact facts that remain unknown and do not invent their measurements. Ask before a
|
|
26
|
+
structural choice or an untraceable numeric value, not before useful read-only inspection.
|
|
27
|
+
|
|
28
|
+
## 1. Read the export before touching code
|
|
29
|
+
|
|
30
|
+
### From an `.svg` — look at it, then measure it
|
|
31
|
+
|
|
32
|
+
Rasterise it and actually open the image. Figma outlines text on export, so this needs no
|
|
33
|
+
webfonts and matches the frame exactly:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
magick -density 144 <export.svg> <scratch>/frame.png # then read frame.png
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
(If the render looks wrong, ImageMagick fell back to its own SVG renderer — check for
|
|
40
|
+
`RSVG` in `magick -list format | grep SVG`, or screenshot the file in Playwright instead.)
|
|
41
|
+
|
|
42
|
+
Then dump the geometry with {%resource:dump-figma-svg.py%} — every shape in document order,
|
|
43
|
+
indented by group nesting, with absolute coordinates:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python3 dump-figma-svg.py <export.svg>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
It closes with two summaries worth reading first: repeated rect sizes, which say _one
|
|
50
|
+
component rendered N times_ where the CSS dump would show N unrelated frames; and the measured
|
|
51
|
+
gaps between rects that share a top edge, which is the auto-layout gap without having to
|
|
52
|
+
believe a label.
|
|
53
|
+
|
|
54
|
+
What the SVG does **not** carry:
|
|
55
|
+
|
|
56
|
+
- **Layer names.** Nothing is called `Card` or `Badge`; you match shapes to the picture.
|
|
57
|
+
- **Auto-layout intent.** `Hug` vs `Fixed` vs `Fill` is gone, so a width you read off is the
|
|
58
|
+
width _at this one size_, not a rule. Derive padding and gaps from the coordinates, and
|
|
59
|
+
treat a child width as content-driven until the picture or the CSS dump says otherwise.
|
|
60
|
+
- **Typography.** Outlined text is a `<path>`; the dumper labels the wide ones `text?` and
|
|
61
|
+
boxes them, which places a text run but tells you nothing about `font-size` or
|
|
62
|
+
`line-height`. If the designer exported with _Outline Text_ off, `<text>` nodes survive and
|
|
63
|
+
the dumper prints their font metrics and strings — take them when you get them.
|
|
64
|
+
|
|
65
|
+
Two numbers to read carefully: a **stroke is centred**, so a 32px circle with a 1px border
|
|
66
|
+
exports as `31 × 31` at `.5` offsets — add the stroke width back before comparing. And an
|
|
67
|
+
**outlined glyph box is the ink**, not the line box: a 17px/20px title measures ~12px tall,
|
|
68
|
+
the same cap-trim the CSS dump reports, so never match a text node's height.
|
|
69
|
+
|
|
70
|
+
### From a `.css` dump — names, intent and type metrics
|
|
71
|
+
|
|
72
|
+
Never grep the raw file — its layout defeats naive parsing (see the traps below). Run
|
|
73
|
+
{%resource:dump-figma-layers.py%}:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
python3 dump-figma-layers.py <export.css> # every layer, artwork dropped
|
|
77
|
+
python3 dump-figma-layers.py <export.css> '^(Widget|Frame|Card)' # only what you name
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Then work out the frame's box model top-down — outer frame width and padding, then each
|
|
81
|
+
child's `width`/`gap`/`flex-grow` — and write the ladder down before you start editing, so you
|
|
82
|
+
can tell a deliberate design decision from a Figma artefact.
|
|
83
|
+
|
|
84
|
+
### Whatever you have, look at the picture before you decide what to build
|
|
85
|
+
|
|
86
|
+
The CSS dump is a **flat** sequence of layers with no nesting whatsoever, so the tree is not in
|
|
87
|
+
it; the SVG has the tree but no names. The image is what disambiguates:
|
|
88
|
+
|
|
89
|
+
- **Hierarchy and reading order** — which layer contains which. The only other clue in the CSS
|
|
90
|
+
is `order:` inside a sibling run, which does not cross frames.
|
|
91
|
+
- **Repetition vs distinct layers.** Six blocks with identical declarations are one component
|
|
92
|
+
rendered six times. The CSS looks like six unrelated frames.
|
|
93
|
+
- **Multiple states in one board.** Frames are routinely laid out side by side as
|
|
94
|
+
default / hover / selected / empty, with a pink cursor marking the interaction. The CSS
|
|
95
|
+
gives you every state flattened together with nothing saying which is which, so a number
|
|
96
|
+
read blind may belong to a hover state you are not building.
|
|
97
|
+
- **Figma's own measurement overlays.** Blue annotations like `396 × 401 Hug` or `347 × 23`
|
|
98
|
+
are authoritative and frequently _absent_ from the CSS — `Hug` in particular tells you a
|
|
99
|
+
dimension is content-driven, which no declaration in the export records. These live only in
|
|
100
|
+
a canvas _screenshot_; a frame export of any format drops them.
|
|
101
|
+
- **What is decoration.** Cursors, callout arrows and section captions painted on the board
|
|
102
|
+
are not part of the component, but they do emit layers.
|
|
103
|
+
|
|
104
|
+
### Traps in the `.css` export itself
|
|
105
|
+
|
|
106
|
+
- **Sub-comments carry the real properties.** Figma writes `/* Auto layout */`,
|
|
107
|
+
`/* Inside auto layout */`, `/* or 16px */` and token names like
|
|
108
|
+
`/* Brand/Chalk White/500 */` _between_ a layer's declarations. A parser that starts a new
|
|
109
|
+
layer at every comment reports frames with **zero properties** and swallows the geometry.
|
|
110
|
+
The dumper folds them in — it decides by the blank line that follows a real layer name, not
|
|
111
|
+
by the name, so component layers called `Profile/Badge` survive.
|
|
112
|
+
- **Frame labels lie.** A frame _named_ "Widget 636px" routinely has `width: 640px`, and a
|
|
113
|
+
label like "8 rows = 564px" often does not reconcile with the grid it sits in. Trust the
|
|
114
|
+
declarations, never the label.
|
|
115
|
+
- **Text heights are cap-trimmed.** With `leading-trim: both; text-edge: cap`, a 17px/20px
|
|
116
|
+
title reports `height: 12px`. Compare `font-size`, `line-height` and `letter-spacing`;
|
|
117
|
+
never compare a text node's height.
|
|
118
|
+
- **Absolute `left`/`top`** are canvas coordinates of the whole board. Only the _differences_
|
|
119
|
+
within one frame mean anything.
|
|
120
|
+
|
|
121
|
+
## 2. Decide what the export is allowed to dictate
|
|
122
|
+
|
|
123
|
+
- **Authoritative:** geometry and typography metrics — widths, padding, gaps, `flex-grow`,
|
|
124
|
+
border radius, font size / weight / line-height / letter-spacing, and the breakpoints at
|
|
125
|
+
which the layout changes.
|
|
126
|
+
- **Never authoritative: semantic colour choice.** Backgrounds, text, borders and
|
|
127
|
+
interaction states resolve from the repository's theme tokens. A hex in the
|
|
128
|
+
export is information about the _designer's_ palette, not a value to paste. Where the
|
|
129
|
+
export's colour and the token disagree, keep the token and note the delta for the design
|
|
130
|
+
review; the export can be wrong about contrast in a way the tokens are not. This holds
|
|
131
|
+
hardest for an SVG, whose fills are exact and therefore tempting: an exact wrong answer is
|
|
132
|
+
still wrong.
|
|
133
|
+
- **Radius and spacing snap to the scale.** Round to the nearest token rather than emitting an
|
|
134
|
+
arbitrary value, and say so if the export's number is more than a step away.
|
|
135
|
+
|
|
136
|
+
## 3. Ask before making a structural choice
|
|
137
|
+
|
|
138
|
+
Matching numbers is mechanical; the choices around them are not. Stop and ask when the export
|
|
139
|
+
implies:
|
|
140
|
+
|
|
141
|
+
- a **breakpoint strategy** — container queries with an exact ladder vs a retuned
|
|
142
|
+
`auto-fill`/`auto-fit` grid;
|
|
143
|
+
- **shared-component churn** — a new variant on a component other screens already use, vs a
|
|
144
|
+
local override;
|
|
145
|
+
- a **scroll region** — which part scrolls and what stays pinned;
|
|
146
|
+
- **fixed sizing** where the component is currently fluid, or the reverse;
|
|
147
|
+
- anything the export shows that the API does not yet return.
|
|
148
|
+
|
|
149
|
+
Colour deltas are informational — report them, do not block on them.
|
|
150
|
+
|
|
151
|
+
## 4. Measure the real thing, don't eyeball it
|
|
152
|
+
|
|
153
|
+
A build passing proves nothing about geometry. Render the component's real markup against the
|
|
154
|
+
**production stylesheet** and read the computed boxes back. {%resource:measure-template.mjs%}
|
|
155
|
+
is a working starting point; copy it into a scratch directory with the compiled stylesheet
|
|
156
|
+
beside it:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
# whatever your build emits — the point is a real, fully compiled stylesheet
|
|
160
|
+
cp dist/apps/<app>/browser/styles-*.css <scratch>/styles.css
|
|
161
|
+
node <scratch>/measure-template.mjs
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Confirm the arbitrary utilities you wrote actually compiled — a typo in
|
|
165
|
+
`@min-[392px]` fails silently:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
grep -oE '@container \(width >= [0-9]+px\)|\.(h-15|rounded-sm)\{[^}]*\}' dist/apps/<app>/browser/styles-*.css | sort -u
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Harness gotchas, each of which will cost you an hour:
|
|
172
|
+
|
|
173
|
+
- **`page.setContent()` renders on `about:blank`, which blocks `file://` subresources** — the
|
|
174
|
+
stylesheet silently never loads. Write a real HTML file and `page.goto('file://…')`.
|
|
175
|
+
- **Never lay the probes out in a flex _row_.** They shrink, and container queries then report
|
|
176
|
+
results for a width the component would never see. Stack them in a column at explicit widths.
|
|
177
|
+
- **The harness has no webfonts** unless you copy the `@font-face` sources too. Text runs
|
|
178
|
+
~1px wide of reality, so a label that wraps in the harness may well fit in the app. Check
|
|
179
|
+
before calling a wrap a defect.
|
|
180
|
+
- Playwright is CommonJS and unresolvable from a scratch directory — `createRequire` against
|
|
181
|
+
the repo root, as the template does. When driving a story, follow the repository's
|
|
182
|
+
installed Storybook verification guidance if present.
|
|
183
|
+
|
|
184
|
+
## 5. Close the loop
|
|
185
|
+
|
|
186
|
+
Report the measured numbers next to the export's, per width — not "matches the design". Say
|
|
187
|
+
explicitly which parts of the export you deliberately did **not** implement and why (colour
|
|
188
|
+
kept as tokens, a label the product decided never to render, a field the API lacks). Then
|
|
189
|
+
report that the export files are no longer needed. Delete only artifacts this workflow
|
|
190
|
+
created and only when the workflow or user explicitly authorizes cleanup; otherwise let the
|
|
191
|
+
user decide whether supplied exports remain useful records.
|