@cloverleaf/reference-impl 0.5.1 → 0.5.2

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cloverleaf",
3
3
  "description": "Cloverleaf reference implementation — Claude Code skills for task scaffolding and the Delivery pipeline (implementer, documenter, reviewer, UI reviewer with multi-viewport visual diff, QA, merge).",
4
- "version": "0.5.1",
4
+ "version": "0.5.2",
5
5
  "author": {
6
6
  "name": "Renato D'Arrigo",
7
7
  "email": "renato.darrigo@gmail.com"
package/README.md CHANGED
@@ -87,16 +87,32 @@ All overrides are read fresh on every skill invocation; no caching. Edit and the
87
87
  ### Known limitations
88
88
 
89
89
  - Concurrent `/cloverleaf-run` on the same repo may race on preview ports.
90
- - UI Reviewer visual diff + multi-viewport deferred to v0.4.
91
90
  - QA does not produce HTML reports (no `report_uri`).
92
91
 
93
92
  ### Prerequisites for UI Reviewer
94
93
 
95
- Run once per machine:
94
+ The installer (`install.sh`) automatically runs the Playwright browser install step. If you need to install manually, run:
96
95
 
97
- npx playwright install chromium
96
+ npx playwright install chromium webkit firefox
98
97
 
99
- This installs chromium into `~/.cache/ms-playwright/` (the default `PLAYWRIGHT_BROWSERS_PATH`). Subsequent `/cloverleaf-ui-review` invocations reuse this cache — no ~300 MB re-download per run.
98
+ On **Linux**, webkit additionally requires system-level dependencies:
99
+
100
+ npx playwright install-deps webkit
101
+
102
+ **Disk footprint:** approximately 600–650 MB total across all three browsers in the default `PLAYWRIGHT_BROWSERS_PATH` location (`~/.cache/ms-playwright/`).
103
+
104
+ | Browser | Approx. size |
105
+ |-----------|-------------|
106
+ | chromium | ~300 MB |
107
+ | webkit | ~150–170 MB |
108
+ | firefox | ~150–180 MB |
109
+
110
+ To store browsers in a non-default location, set `PLAYWRIGHT_BROWSERS_PATH` before installing and before running the UI Reviewer skill:
111
+
112
+ export PLAYWRIGHT_BROWSERS_PATH=/mnt/data/playwright
113
+ npx playwright install chromium webkit firefox
114
+
115
+ Subsequent `/cloverleaf-ui-review` invocations reuse the cache — no re-download per run as long as `PLAYWRIGHT_BROWSERS_PATH` is set consistently.
100
116
 
101
117
  ## Quick start — toy repo
102
118
 
@@ -129,7 +145,7 @@ The Reviewer never switches branches. It reads files via `git show` and runs tes
129
145
 
130
146
  ## Package layout
131
147
 
132
- - `lib/` — TypeScript library used by the CLI. State, events, feedback, IDs, paths.
148
+ - `lib/` — TypeScript library used by the CLI. State, events, feedback, IDs, paths. Includes `buildBaselinePath(repoRoot, browser, slug, viewport)` (`lib/visual-diff.ts`) for constructing canonical baseline paths under `.cloverleaf/baselines/{browser}/`.
133
149
  - `skills/` — Claude Code skill markdown files.
134
150
  - `prompts/` — Implementer/Reviewer subagent system prompts.
135
151
  - `examples/toy-repo/` — standalone demo repo.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.1
1
+ 0.5.2
@@ -1,4 +1,5 @@
1
1
  {
2
+ "browsers": ["chromium"],
2
3
  "viewports": {
3
4
  "mobile": { "width": 375, "height": 667 },
4
5
  "tablet": { "width": 768, "height": 1024 },
@@ -12,7 +13,9 @@
12
13
  },
13
14
  "axe": {
14
15
  "viewports": ["desktop"],
16
+ "browser": "chromium",
15
17
  "dedupeBy": ["ruleId", "target"],
16
18
  "ignored": []
17
- }
19
+ },
20
+ "maxCombinations": 90
18
21
  }
package/dist/cli.mjs CHANGED
@@ -26,6 +26,7 @@
26
26
  * materialise-tasks <repoRoot> <planId>
27
27
  * next-work-item-id <repoRoot> <project>
28
28
  * discovery-config --repo-root <repoRoot>
29
+ * prep-worktree <mainRoot> <worktreePath>
29
30
  */
30
31
  import { readFileSync } from 'node:fs';
31
32
  import { execSync } from 'node:child_process';
@@ -44,6 +45,7 @@ import { loadRfc, saveRfc, advanceRfcStatus } from './rfc.mjs';
44
45
  import { loadSpike, saveSpike, advanceSpikeStatus } from './spike.mjs';
45
46
  import { loadPlan, savePlan, advancePlanStatus, materialiseTasksFromPlan } from './plan.mjs';
46
47
  import { loadDiscoveryConfig } from './discovery-config.mjs';
48
+ import { prepWorktree } from './prep-worktree.mjs';
47
49
  function die(msg, code = 1) {
48
50
  process.stderr.write(msg + '\n');
49
51
  process.exit(code);
@@ -73,7 +75,8 @@ function usage(msg) {
73
75
  ' advance-plan <repoRoot> <id> <toStatus> <agent|human> [gate]\n' +
74
76
  ' materialise-tasks <repoRoot> <planId>\n' +
75
77
  ' next-work-item-id <repoRoot> <project>\n' +
76
- ' discovery-config --repo-root <repoRoot>\n');
78
+ ' discovery-config --repo-root <repoRoot>\n' +
79
+ ' prep-worktree <mainRoot> <worktreePath>\n');
77
80
  process.exit(2);
78
81
  }
79
82
  const [, , command, ...rest] = process.argv;
@@ -368,6 +371,13 @@ try {
368
371
  process.stdout.write(JSON.stringify(c, null, 2));
369
372
  break;
370
373
  }
374
+ case 'prep-worktree': {
375
+ const [mainRoot, worktreePath] = rest;
376
+ if (!mainRoot || !worktreePath)
377
+ usage('prep-worktree requires <mainRoot> <worktreePath>');
378
+ prepWorktree(mainRoot, worktreePath);
379
+ break;
380
+ }
371
381
  default:
372
382
  usage(`Unknown command: ${command}`);
373
383
  }
@@ -0,0 +1,50 @@
1
+ import { cpSync, existsSync } from 'node:fs';
2
+ import { execSync } from 'node:child_process';
3
+ import { join } from 'node:path';
4
+ /**
5
+ * Prepare a freshly-created git worktree of the cloverleaf monorepo for running reference-impl
6
+ * tests. Addresses the v0.5 dogfood finding (CLV-16, CLV-17 Delivery runs) where Reviewer/QA
7
+ * subagents hit `Cannot find module '@cloverleaf/standard/validators/index.js'` because:
8
+ *
9
+ * 1. The worktree has no `node_modules` at all (git worktrees don't inherit it).
10
+ * 2. Running `npm install` in the worktree's `reference-impl/` follows the `file:../standard`
11
+ * dep, but the worktree's `standard/` has no `dist/` (nothing built) and no `node_modules/`
12
+ * (ajv-formats etc., needed by the conformance runner).
13
+ *
14
+ * Strategy: reuse main's already-installed deps and build standard/ fresh from the worktree
15
+ * sources so any branch changes to `standard/src` are picked up.
16
+ *
17
+ * - Copy `<main>/standard/node_modules` → `<wt>/standard/node_modules`
18
+ * - Run `npm run build` in `<wt>/standard` (produces worktree dist/)
19
+ * - Copy `<main>/reference-impl/node_modules` → `<wt>/reference-impl/node_modules`
20
+ * The `@cloverleaf/standard → ../../../standard` relative symlink is preserved verbatim so
21
+ * it resolves to the worktree's OWN standard/, not main's.
22
+ */
23
+ export function prepWorktree(mainRoot, worktreePath) {
24
+ const mainStandardNm = join(mainRoot, 'standard', 'node_modules');
25
+ const mainRefImplNm = join(mainRoot, 'reference-impl', 'node_modules');
26
+ const wtStandardPkg = join(worktreePath, 'standard', 'package.json');
27
+ const wtRefImplPkg = join(worktreePath, 'reference-impl', 'package.json');
28
+ if (!existsSync(wtStandardPkg)) {
29
+ throw new Error(`worktree missing standard/package.json at ${wtStandardPkg}`);
30
+ }
31
+ if (!existsSync(wtRefImplPkg)) {
32
+ throw new Error(`worktree missing reference-impl/package.json at ${wtRefImplPkg}`);
33
+ }
34
+ if (!existsSync(mainStandardNm)) {
35
+ throw new Error(`main missing standard/node_modules at ${mainStandardNm} — run \`npm ci\` in main's standard/ first`);
36
+ }
37
+ if (!existsSync(mainRefImplNm)) {
38
+ throw new Error(`main missing reference-impl/node_modules at ${mainRefImplNm} — run \`npm ci\` in main's reference-impl/ first`);
39
+ }
40
+ const wtStandardNm = join(worktreePath, 'standard', 'node_modules');
41
+ const wtRefImplNm = join(worktreePath, 'reference-impl', 'node_modules');
42
+ // verbatimSymlinks keeps relative symlink targets byte-identical, so the @cloverleaf/standard
43
+ // link in reference-impl/node_modules/ resolves against the worktree after copy.
44
+ cpSync(mainStandardNm, wtStandardNm, { recursive: true, verbatimSymlinks: true });
45
+ cpSync(mainRefImplNm, wtRefImplNm, { recursive: true, verbatimSymlinks: true });
46
+ execSync('npm run build', {
47
+ cwd: join(worktreePath, 'standard'),
48
+ stdio: 'pipe',
49
+ });
50
+ }
@@ -4,22 +4,58 @@ import { dirname, join } from 'node:path';
4
4
  const here = dirname(fileURLToPath(import.meta.url));
5
5
  const PACKAGE_DEFAULT = join(here, '..', 'config', 'ui-review.json');
6
6
  const HARDCODED_FALLBACK = {
7
+ browsers: ['chromium'],
7
8
  viewports: {
8
9
  mobile: { width: 375, height: 667 },
9
10
  tablet: { width: 768, height: 1024 },
10
11
  desktop: { width: 1280, height: 800 },
11
12
  },
12
13
  visualDiff: { enabled: true, threshold: 0.1, maxDiffRatio: 0.01, mask: [] },
13
- axe: { viewports: ['desktop'], dedupeBy: ['ruleId', 'target'], ignored: [] },
14
+ axe: {
15
+ viewports: ['desktop'],
16
+ browser: 'chromium',
17
+ dedupeBy: ['ruleId', 'target'],
18
+ ignored: [],
19
+ },
20
+ maxCombinations: 90,
14
21
  };
15
- function readAsConfig(path) {
16
- try {
17
- const doc = JSON.parse(readFileSync(path, 'utf-8'));
18
- // Back-compat: if ignored is missing from an older override, default it.
19
- if (doc.axe && !('ignored' in doc.axe)) {
22
+ function applyDefaults(doc) {
23
+ // browsers — default ["chromium"]
24
+ if (!Array.isArray(doc.browsers)) {
25
+ doc.browsers = ['chromium'];
26
+ }
27
+ // viewports — fall through to hardcoded if omitted entirely
28
+ if (!doc.viewports) {
29
+ doc.viewports = HARDCODED_FALLBACK.viewports;
30
+ }
31
+ // visualDiff
32
+ if (!doc.visualDiff) {
33
+ doc.visualDiff = { ...HARDCODED_FALLBACK.visualDiff };
34
+ }
35
+ // axe sub-fields
36
+ if (!doc.axe) {
37
+ doc.axe = { ...HARDCODED_FALLBACK.axe };
38
+ }
39
+ else {
40
+ // Back-compat: ignored omitted in older overrides
41
+ if (!('ignored' in doc.axe)) {
20
42
  doc.axe.ignored = [];
21
43
  }
22
- return doc;
44
+ // axe.browser — default "chromium"
45
+ if (!('browser' in doc.axe)) {
46
+ doc.axe.browser = 'chromium';
47
+ }
48
+ }
49
+ // maxCombinations — default 90
50
+ if (typeof doc.maxCombinations !== 'number') {
51
+ doc.maxCombinations = 90;
52
+ }
53
+ return doc;
54
+ }
55
+ function readAsConfig(path) {
56
+ try {
57
+ const raw = JSON.parse(readFileSync(path, 'utf-8'));
58
+ return applyDefaults(raw);
23
59
  }
24
60
  catch {
25
61
  return null;
@@ -1,7 +1,20 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { dirname } from 'node:path';
2
+ import { dirname, join } from 'node:path';
3
3
  import pixelmatch from 'pixelmatch';
4
4
  import { PNG } from 'pngjs';
5
+ /**
6
+ * Construct the canonical baseline path for a visual-diff capture.
7
+ *
8
+ * Layout (as of CLV-17): `.cloverleaf/baselines/{browser}/{slug}-{viewport}.png`
9
+ *
10
+ * The flat layout `.cloverleaf/baselines/{slug}-{viewport}.png` is DEPRECATED.
11
+ * All new baselines MUST be placed under `baselines/{browser}/`.
12
+ * Existing flat chromium baselines were migrated to `baselines/chromium/` via
13
+ * explicit `git mv` in CLV-17.
14
+ */
15
+ export function buildBaselinePath(repoRoot, browser, slug, viewport) {
16
+ return join(repoRoot, '.cloverleaf', 'baselines', browser, `${slug}-${viewport}.png`);
17
+ }
5
18
  function ensureDir(path) {
6
19
  mkdirSync(dirname(path), { recursive: true });
7
20
  }
package/install.sh CHANGED
@@ -42,9 +42,22 @@ echo " /cloverleaf-merge — merge gate"
42
42
  echo ""
43
43
  echo "Restart any open Claude Code sessions to pick up the new skills."
44
44
 
45
- # Post-install: warn about Playwright chromium if not cached
46
- if [ ! -d "${HOME}/.cache/ms-playwright" ] || [ -z "$(ls -A "${HOME}/.cache/ms-playwright" 2>/dev/null)" ]; then
45
+ # Post-install: install Playwright browsers for UI Reviewer.
46
+ echo ""
47
+ echo "Installing Playwright browsers (chromium, webkit, firefox)..."
48
+ echo " (approx. 600–650 MB total in \${PLAYWRIGHT_BROWSERS_PATH:-~/.cache/ms-playwright})"
49
+ echo ""
50
+
51
+ npx playwright install chromium webkit firefox
52
+
53
+ # On Linux, system deps for webkit are also required.
54
+ if [ "$(uname -s)" = "Linux" ]; then
47
55
  echo ""
48
- echo "Note: UI Reviewer uses Playwright chromium. If you plan to run /cloverleaf-ui-review, install once with:"
49
- echo " npx playwright install chromium"
56
+ echo "Linux detected installing webkit system dependencies..."
57
+ npx playwright install-deps webkit
50
58
  fi
59
+
60
+ echo ""
61
+ echo "Playwright browsers installed."
62
+ echo "To use a custom cache directory, set PLAYWRIGHT_BROWSERS_PATH before running install."
63
+ echo " e.g. PLAYWRIGHT_BROWSERS_PATH=~/.cache/ms-playwright ./install.sh"
package/lib/cli.ts CHANGED
@@ -26,6 +26,7 @@
26
26
  * materialise-tasks <repoRoot> <planId>
27
27
  * next-work-item-id <repoRoot> <project>
28
28
  * discovery-config --repo-root <repoRoot>
29
+ * prep-worktree <mainRoot> <worktreePath>
29
30
  */
30
31
 
31
32
  import { readFileSync } from 'node:fs';
@@ -46,6 +47,7 @@ import { loadRfc, saveRfc, advanceRfcStatus, type RfcDoc } from './rfc.js';
46
47
  import { loadSpike, saveSpike, advanceSpikeStatus, type SpikeDoc } from './spike.js';
47
48
  import { loadPlan, savePlan, advancePlanStatus, materialiseTasksFromPlan, type PlanDoc } from './plan.js';
48
49
  import { loadDiscoveryConfig } from './discovery-config.js';
50
+ import { prepWorktree } from './prep-worktree.js';
49
51
 
50
52
  function die(msg: string, code = 1): never {
51
53
  process.stderr.write(msg + '\n');
@@ -77,7 +79,8 @@ function usage(msg?: string): never {
77
79
  ' advance-plan <repoRoot> <id> <toStatus> <agent|human> [gate]\n' +
78
80
  ' materialise-tasks <repoRoot> <planId>\n' +
79
81
  ' next-work-item-id <repoRoot> <project>\n' +
80
- ' discovery-config --repo-root <repoRoot>\n'
82
+ ' discovery-config --repo-root <repoRoot>\n' +
83
+ ' prep-worktree <mainRoot> <worktreePath>\n'
81
84
  );
82
85
  process.exit(2);
83
86
  }
@@ -375,6 +378,13 @@ try {
375
378
  break;
376
379
  }
377
380
 
381
+ case 'prep-worktree': {
382
+ const [mainRoot, worktreePath] = rest;
383
+ if (!mainRoot || !worktreePath) usage('prep-worktree requires <mainRoot> <worktreePath>');
384
+ prepWorktree(mainRoot, worktreePath);
385
+ break;
386
+ }
387
+
378
388
  default:
379
389
  usage(`Unknown command: ${command}`);
380
390
  }
@@ -0,0 +1,55 @@
1
+ import { cpSync, existsSync } from 'node:fs';
2
+ import { execSync } from 'node:child_process';
3
+ import { join } from 'node:path';
4
+
5
+ /**
6
+ * Prepare a freshly-created git worktree of the cloverleaf monorepo for running reference-impl
7
+ * tests. Addresses the v0.5 dogfood finding (CLV-16, CLV-17 Delivery runs) where Reviewer/QA
8
+ * subagents hit `Cannot find module '@cloverleaf/standard/validators/index.js'` because:
9
+ *
10
+ * 1. The worktree has no `node_modules` at all (git worktrees don't inherit it).
11
+ * 2. Running `npm install` in the worktree's `reference-impl/` follows the `file:../standard`
12
+ * dep, but the worktree's `standard/` has no `dist/` (nothing built) and no `node_modules/`
13
+ * (ajv-formats etc., needed by the conformance runner).
14
+ *
15
+ * Strategy: reuse main's already-installed deps and build standard/ fresh from the worktree
16
+ * sources so any branch changes to `standard/src` are picked up.
17
+ *
18
+ * - Copy `<main>/standard/node_modules` → `<wt>/standard/node_modules`
19
+ * - Run `npm run build` in `<wt>/standard` (produces worktree dist/)
20
+ * - Copy `<main>/reference-impl/node_modules` → `<wt>/reference-impl/node_modules`
21
+ * The `@cloverleaf/standard → ../../../standard` relative symlink is preserved verbatim so
22
+ * it resolves to the worktree's OWN standard/, not main's.
23
+ */
24
+ export function prepWorktree(mainRoot: string, worktreePath: string): void {
25
+ const mainStandardNm = join(mainRoot, 'standard', 'node_modules');
26
+ const mainRefImplNm = join(mainRoot, 'reference-impl', 'node_modules');
27
+ const wtStandardPkg = join(worktreePath, 'standard', 'package.json');
28
+ const wtRefImplPkg = join(worktreePath, 'reference-impl', 'package.json');
29
+
30
+ if (!existsSync(wtStandardPkg)) {
31
+ throw new Error(`worktree missing standard/package.json at ${wtStandardPkg}`);
32
+ }
33
+ if (!existsSync(wtRefImplPkg)) {
34
+ throw new Error(`worktree missing reference-impl/package.json at ${wtRefImplPkg}`);
35
+ }
36
+ if (!existsSync(mainStandardNm)) {
37
+ throw new Error(`main missing standard/node_modules at ${mainStandardNm} — run \`npm ci\` in main's standard/ first`);
38
+ }
39
+ if (!existsSync(mainRefImplNm)) {
40
+ throw new Error(`main missing reference-impl/node_modules at ${mainRefImplNm} — run \`npm ci\` in main's reference-impl/ first`);
41
+ }
42
+
43
+ const wtStandardNm = join(worktreePath, 'standard', 'node_modules');
44
+ const wtRefImplNm = join(worktreePath, 'reference-impl', 'node_modules');
45
+
46
+ // verbatimSymlinks keeps relative symlink targets byte-identical, so the @cloverleaf/standard
47
+ // link in reference-impl/node_modules/ resolves against the worktree after copy.
48
+ cpSync(mainStandardNm, wtStandardNm, { recursive: true, verbatimSymlinks: true });
49
+ cpSync(mainRefImplNm, wtRefImplNm, { recursive: true, verbatimSymlinks: true });
50
+
51
+ execSync('npm run build', {
52
+ cwd: join(worktreePath, 'standard'),
53
+ stdio: 'pipe',
54
+ });
55
+ }
@@ -10,7 +10,12 @@ export interface Viewport {
10
10
  height: number;
11
11
  }
12
12
 
13
+ /** Valid Playwright browser engine strings. */
14
+ export type BrowserEngine = 'chromium' | 'webkit' | 'firefox';
15
+
13
16
  export interface UiReviewConfig {
17
+ /** Browser engines to run visual-diff + axe across. Default: ["chromium"]. */
18
+ browsers: BrowserEngine[];
14
19
  viewports: Record<string, Viewport>;
15
20
  visualDiff: {
16
21
  enabled: boolean;
@@ -20,29 +25,78 @@ export interface UiReviewConfig {
20
25
  };
21
26
  axe: {
22
27
  viewports: string[];
28
+ /** Browser engine to use for axe accessibility runs. Default: "chromium". */
29
+ browser: BrowserEngine;
23
30
  dedupeBy: ('ruleId' | 'target')[];
24
31
  ignored: Array<{ ruleId: string; target: string }>;
25
32
  };
33
+ /**
34
+ * Maximum number of (browser × viewport) combinations to run.
35
+ * The skill will skip combinations beyond this limit to avoid runaway runtimes.
36
+ * Default: 90.
37
+ */
38
+ maxCombinations: number;
26
39
  }
27
40
 
28
41
  const HARDCODED_FALLBACK: UiReviewConfig = {
42
+ browsers: ['chromium'],
29
43
  viewports: {
30
44
  mobile: { width: 375, height: 667 },
31
45
  tablet: { width: 768, height: 1024 },
32
46
  desktop: { width: 1280, height: 800 },
33
47
  },
34
48
  visualDiff: { enabled: true, threshold: 0.1, maxDiffRatio: 0.01, mask: [] },
35
- axe: { viewports: ['desktop'], dedupeBy: ['ruleId', 'target'], ignored: [] },
49
+ axe: {
50
+ viewports: ['desktop'],
51
+ browser: 'chromium',
52
+ dedupeBy: ['ruleId', 'target'],
53
+ ignored: [],
54
+ },
55
+ maxCombinations: 90,
36
56
  };
37
57
 
38
- function readAsConfig(path: string): UiReviewConfig | null {
39
- try {
40
- const doc = JSON.parse(readFileSync(path, 'utf-8')) as UiReviewConfig;
41
- // Back-compat: if ignored is missing from an older override, default it.
42
- if (doc.axe && !('ignored' in doc.axe)) {
58
+ function applyDefaults(doc: Partial<UiReviewConfig>): UiReviewConfig {
59
+ // browsers — default ["chromium"]
60
+ if (!Array.isArray(doc.browsers)) {
61
+ (doc as UiReviewConfig).browsers = ['chromium'];
62
+ }
63
+
64
+ // viewports — fall through to hardcoded if omitted entirely
65
+ if (!doc.viewports) {
66
+ (doc as UiReviewConfig).viewports = HARDCODED_FALLBACK.viewports;
67
+ }
68
+
69
+ // visualDiff
70
+ if (!doc.visualDiff) {
71
+ (doc as UiReviewConfig).visualDiff = { ...HARDCODED_FALLBACK.visualDiff };
72
+ }
73
+
74
+ // axe sub-fields
75
+ if (!doc.axe) {
76
+ (doc as UiReviewConfig).axe = { ...HARDCODED_FALLBACK.axe };
77
+ } else {
78
+ // Back-compat: ignored omitted in older overrides
79
+ if (!('ignored' in doc.axe)) {
43
80
  (doc.axe as UiReviewConfig['axe']).ignored = [];
44
81
  }
45
- return doc;
82
+ // axe.browser — default "chromium"
83
+ if (!('browser' in doc.axe)) {
84
+ (doc.axe as UiReviewConfig['axe']).browser = 'chromium';
85
+ }
86
+ }
87
+
88
+ // maxCombinations — default 90
89
+ if (typeof doc.maxCombinations !== 'number') {
90
+ (doc as UiReviewConfig).maxCombinations = 90;
91
+ }
92
+
93
+ return doc as UiReviewConfig;
94
+ }
95
+
96
+ function readAsConfig(path: string): UiReviewConfig | null {
97
+ try {
98
+ const raw = JSON.parse(readFileSync(path, 'utf-8')) as Partial<UiReviewConfig>;
99
+ return applyDefaults(raw);
46
100
  } catch {
47
101
  return null;
48
102
  }
@@ -1,10 +1,29 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { dirname } from 'node:path';
2
+ import { dirname, join } from 'node:path';
3
3
  import pixelmatch from 'pixelmatch';
4
4
  import { PNG } from 'pngjs';
5
5
 
6
6
  export type VisualDiffStatus = 'new-baseline' | 'match' | 'diff' | 'dimension-mismatch';
7
7
 
8
+ /**
9
+ * Construct the canonical baseline path for a visual-diff capture.
10
+ *
11
+ * Layout (as of CLV-17): `.cloverleaf/baselines/{browser}/{slug}-{viewport}.png`
12
+ *
13
+ * The flat layout `.cloverleaf/baselines/{slug}-{viewport}.png` is DEPRECATED.
14
+ * All new baselines MUST be placed under `baselines/{browser}/`.
15
+ * Existing flat chromium baselines were migrated to `baselines/chromium/` via
16
+ * explicit `git mv` in CLV-17.
17
+ */
18
+ export function buildBaselinePath(
19
+ repoRoot: string,
20
+ browser: string,
21
+ slug: string,
22
+ viewport: string,
23
+ ): string {
24
+ return join(repoRoot, '.cloverleaf', 'baselines', browser, `${slug}-${viewport}.png`);
25
+ }
26
+
8
27
  export interface VisualDiffResult {
9
28
  status: VisualDiffStatus;
10
29
  diffPixels: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloverleaf/reference-impl",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "description": "Reference implementation of the Cloverleaf methodology as Claude Code skills. Implements the Tight Loop (Implementer + Reviewer).",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/prompts/qa.md CHANGED
@@ -17,10 +17,15 @@ The Standard's QA contract requires a `preview_uri`. You were passed the sentine
17
17
 
18
18
  ## Runtime procedure
19
19
 
20
- 1. Set up isolated worktree:
20
+ 1. Set up isolated worktree and prepare its node_modules + standard/dist. The `prep-worktree`
21
+ helper copies main's `standard/node_modules` and `reference-impl/node_modules` into the
22
+ worktree and runs the standard build script so the @cloverleaf/standard symlink resolves
23
+ correctly inside the worktree. (Without this, `tsc` fails with `Cannot find module
24
+ '@cloverleaf/standard/validators/index.js'` because git worktrees don't inherit node_modules.)
21
25
  ```bash
22
26
  TMPDIR=$(mktemp -d)
23
27
  git worktree add "$TMPDIR" {{branch}}
28
+ cloverleaf-cli prep-worktree {{repo_root}} "$TMPDIR"
24
29
  ```
25
30
 
26
31
  2. Inspect the changed files (from the diff). For each QA rule whose `match` patterns match ≥1 changed file, queue its command.
@@ -41,12 +41,14 @@ A `pass` verdict MAY have an empty `findings` array or omit it. A `bounce` verdi
41
41
  - You are a fresh pair of eyes. Do not rubber-stamp. If you have substantive doubts, bounce.
42
42
  - Check that tests actually cover the AC; a passing test suite with no AC coverage is a bounce.
43
43
  - Do NOT modify any files. You are read-only.
44
- - Do NOT use `git checkout` or `git switch`. Read files via `git show <branch>:<path>`. If you need a live checkout to run tests, use a worktree:
44
+ - Do NOT use `git checkout` or `git switch`. Read files via `git show <branch>:<path>`. If you need a live checkout to run tests, use a worktree and prime it with `cloverleaf-cli prep-worktree` (copies main's node_modules + builds standard/dist inside the worktree — without this, `tsc` fails with `Cannot find module '@cloverleaf/standard/validators/index.js'`):
45
45
 
46
46
  ```bash
47
+ MAIN=$(pwd)
47
48
  git worktree add /tmp/cl-review-<task-id> cloverleaf/<task-id>
48
- cd /tmp/cl-review-<task-id>
49
- npm install && npm test
49
+ cloverleaf-cli prep-worktree "$MAIN" /tmp/cl-review-<task-id>
50
+ cd /tmp/cl-review-<task-id>/reference-impl
51
+ npm test
50
52
  cd -
51
53
  git worktree remove /tmp/cl-review-<task-id>
52
54
  ```
@@ -30,7 +30,7 @@ The rationale: baselines on `{{repo_root}}/.cloverleaf/baselines/` get picked up
30
30
  Apply the allowlist in `{{ui_review_config}}.axe.ignored` to drop pre-existing violations that the consumer has accepted (e.g., a11y debt being tracked separately).
31
31
  Dedupe findings across viewports by the `{{ui_review_config}}.axe.dedupeBy` composite key (default `["ruleId", "target"]`).
32
32
  Emit one finding per (ruleId, target) pair, with a `metadata.viewports` array aggregating the viewports where the violation was detected.
33
- - **Visual diff (pixelmatch):** when `{{ui_review_config}}.visualDiff.enabled` is true, screenshot each route at each viewport in `{{ui_review_config}}.viewports`, compare to `.cloverleaf/baselines/{route-slug}-{viewport}.png`, emit `severity: "info"` findings with baseline/candidate/diff attachments when the diff ratio exceeds `maxDiffRatio`.
33
+ - **Visual diff (pixelmatch):** when `{{ui_review_config}}.visualDiff.enabled` is true, screenshot each route at each viewport in `{{ui_review_config}}.viewports`, compare to `.cloverleaf/baselines/{browser}/{route-slug}-{viewport}.png`, emit `severity: "info"` findings with baseline/candidate/diff attachments when the diff ratio exceeds `maxDiffRatio`.
34
34
  - Visual diffs are **informational**, never gating. A diff does not fail the review — it surfaces to the human final-gate reviewer.
35
35
  - Route empty-set / "all" handling preserves v0.3 behavior:
36
36
  - `{{affected_routes}}` is `[]` → `verdict: "pass"`, summary `"No renderable routes affected, skipping axe."`, do NOT start the preview server.
@@ -75,7 +75,7 @@ The `PLAYWRIGHT_BROWSERS_PATH` environment variable is set to `~/.cache/ms-playw
75
75
  - Compute slug for the route (lowercase, strip leading/trailing slashes, replace slashes with hyphens; `/` → `index`).
76
76
  - Note: use `{{repo_root}}` (the absolute main-repo path), NOT `$TMPDIR` or the worktree. See the "Paths" section.
77
77
  - Call `compareVisual` (from `lib/visual-diff.ts`) with:
78
- - `baselinePath = {{repo_root}}/.cloverleaf/baselines/{slug}-{viewport}.png`
78
+ - `baselinePath = {{repo_root}}/.cloverleaf/baselines/{browser}/{slug}-{viewport}.png`
79
79
  - `candidateBuf = <candidate PNG>`
80
80
  - `diffPath = {{repo_root}}/.cloverleaf/runs/{taskId}/ui-review/diff-{slug}-{viewport}.png`
81
81
  - `candidateOutPath = {{repo_root}}/.cloverleaf/runs/{taskId}/ui-review/candidate-{slug}-{viewport}.png`
@@ -145,7 +145,7 @@ For a11y findings there is usually no meaningful file/line, so OMIT `location` e
145
145
  "message": "<description; include the page URL for a11y, route+viewport+diff for visual-diff>",
146
146
  "metadata": { /* per §7/§8 above */ },
147
147
  "attachments": [ /* for visual-diff with status="diff" */
148
- { "label": "baseline", "path": ".cloverleaf/baselines/{slug}-{viewport}.png" },
148
+ { "label": "baseline", "path": ".cloverleaf/baselines/{browser}/{slug}-{viewport}.png" },
149
149
  { "label": "candidate", "path": ".cloverleaf/runs/{taskId}/ui-review/candidate-{slug}-{viewport}.png" },
150
150
  { "label": "diff", "path": ".cloverleaf/runs/{taskId}/ui-review/diff-{slug}-{viewport}.png" }
151
151
  ]
@@ -71,9 +71,9 @@ description: Human gate for merging a Cloverleaf task. Branches on state — fro
71
71
  cloverleaf-cli advance-status <repo_root> ${TASK_ID} escalated agent
72
72
  ```
73
73
  Exit with a human-readable error explaining the conflict.
74
- - Advance task status on main (commits `.cloverleaf/tasks/${TASK_ID}.json` + event):
74
+ - Advance task status on main (commits `.cloverleaf/tasks/${TASK_ID}.json` + event). The `final-gate → merged` transition is `allowed_actors: [human]` per the task state machine; the skill passes the gate + path as positional args:
75
75
  ```bash
76
- cloverleaf-cli advance-status <repo_root> ${TASK_ID} merged agent
76
+ cloverleaf-cli advance-status <repo_root> ${TASK_ID} merged human final_approval_gate full_pipeline
77
77
  ```
78
78
 
79
79
  ### 4. Common: report