@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +21 -5
- package/VERSION +1 -1
- package/config/ui-review.json +4 -1
- package/dist/cli.mjs +11 -1
- package/dist/prep-worktree.mjs +50 -0
- package/dist/ui-review-config.mjs +43 -7
- package/dist/visual-diff.mjs +14 -1
- package/install.sh +17 -4
- package/lib/cli.ts +11 -1
- package/lib/prep-worktree.ts +55 -0
- package/lib/ui-review-config.ts +61 -7
- package/lib/visual-diff.ts +20 -1
- package/package.json +1 -1
- package/prompts/qa.md +6 -1
- package/prompts/reviewer.md +5 -3
- package/prompts/ui-reviewer.md +3 -3
- package/skills/cloverleaf-merge/SKILL.md +2 -2
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
0.5.2
|
package/config/ui-review.json
CHANGED
|
@@ -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: {
|
|
14
|
+
axe: {
|
|
15
|
+
viewports: ['desktop'],
|
|
16
|
+
browser: 'chromium',
|
|
17
|
+
dedupeBy: ['ruleId', 'target'],
|
|
18
|
+
ignored: [],
|
|
19
|
+
},
|
|
20
|
+
maxCombinations: 90,
|
|
14
21
|
};
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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;
|
package/dist/visual-diff.mjs
CHANGED
|
@@ -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:
|
|
46
|
-
|
|
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 "
|
|
49
|
-
|
|
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
|
+
}
|
package/lib/ui-review-config.ts
CHANGED
|
@@ -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: {
|
|
49
|
+
axe: {
|
|
50
|
+
viewports: ['desktop'],
|
|
51
|
+
browser: 'chromium',
|
|
52
|
+
dedupeBy: ['ruleId', 'target'],
|
|
53
|
+
ignored: [],
|
|
54
|
+
},
|
|
55
|
+
maxCombinations: 90,
|
|
36
56
|
};
|
|
37
57
|
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
}
|
package/lib/visual-diff.ts
CHANGED
|
@@ -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.
|
|
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.
|
package/prompts/reviewer.md
CHANGED
|
@@ -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
|
-
|
|
49
|
-
|
|
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
|
```
|
package/prompts/ui-reviewer.md
CHANGED
|
@@ -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
|
|
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
|