@bongos/core 1.19.987 → 1.19.988
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/.bongos-core.json +30 -15
- package/.claude/skills/design/SKILL.md +2 -1
- package/docs/module-api-changelog.md +2 -0
- package/docs/packs/artist.md +1 -0
- package/docs/packs/engineer.md +2 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/claim.js +7 -0
- package/scripts/gds/review-sheet.js +108 -0
- package/scripts/gds/visual-review-nudge.js +37 -0
- package/src/module-api.js +1 -1
- package/tests/visual_review.mjs +110 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.988",
|
|
6
|
+
"core_contract": "1.19.988",
|
|
7
|
+
"source_commit": "fe4b08f918073241c24208ac22bb4e2a505a00f4",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-23T19:
|
|
9
|
+
"built_at": "2026-09-23T19:26:45.671Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 518,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2394,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2938,
|
|
20
|
+
"tree_sha256": "bc9a86c3157f1080179c3d82293d7cfad4a40cc7b5895538465816e8f1238416",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
{
|
|
128
128
|
"path": ".claude/skills/design/SKILL.md",
|
|
129
129
|
"mode": "0000644",
|
|
130
|
-
"sha256": "
|
|
130
|
+
"sha256": "2ffaa5f9c6b6ff5b9a3a4730b4d172cdb905f1ed5efa879d3b14dca5ae60aeeb"
|
|
131
131
|
},
|
|
132
132
|
{
|
|
133
133
|
"path": ".claude/skills/feedback/SKILL.md",
|
|
@@ -2712,7 +2712,7 @@
|
|
|
2712
2712
|
{
|
|
2713
2713
|
"path": "docs/module-api-changelog.md",
|
|
2714
2714
|
"mode": "0000644",
|
|
2715
|
-
"sha256": "
|
|
2715
|
+
"sha256": "22b37ede3331f4be0569380c58ab10e90d79aaaf3dd52191d6fc49bab095b2b2"
|
|
2716
2716
|
},
|
|
2717
2717
|
{
|
|
2718
2718
|
"path": "docs/modules-contract.md",
|
|
@@ -2787,12 +2787,12 @@
|
|
|
2787
2787
|
{
|
|
2788
2788
|
"path": "docs/packs/artist.md",
|
|
2789
2789
|
"mode": "0000644",
|
|
2790
|
-
"sha256": "
|
|
2790
|
+
"sha256": "ef94247b390fe32676c910545c35a7cfd39bb2ed87680174d7bd590eb1fbe2af"
|
|
2791
2791
|
},
|
|
2792
2792
|
{
|
|
2793
2793
|
"path": "docs/packs/engineer.md",
|
|
2794
2794
|
"mode": "0000644",
|
|
2795
|
-
"sha256": "
|
|
2795
|
+
"sha256": "76c058f9fa63846caf132e2b4703ae7913572d59dbcb47f3e8b565f21262fea8"
|
|
2796
2796
|
},
|
|
2797
2797
|
{
|
|
2798
2798
|
"path": "docs/packs/ideator.md",
|
|
@@ -8162,12 +8162,12 @@
|
|
|
8162
8162
|
{
|
|
8163
8163
|
"path": "package-lock.json",
|
|
8164
8164
|
"mode": "0000644",
|
|
8165
|
-
"sha256": "
|
|
8165
|
+
"sha256": "b852a0142a8be848b6e231b88b0c7b5e2f86baa0b88dcb44e42c1f1e65afb9ea"
|
|
8166
8166
|
},
|
|
8167
8167
|
{
|
|
8168
8168
|
"path": "package.json",
|
|
8169
8169
|
"mode": "0000644",
|
|
8170
|
-
"sha256": "
|
|
8170
|
+
"sha256": "46df90fa4ee1a722331e5bd7a6531f82e4245a545412e70ef73825bce85840e7"
|
|
8171
8171
|
},
|
|
8172
8172
|
{
|
|
8173
8173
|
"path": "public-docs/index.html",
|
|
@@ -8502,7 +8502,7 @@
|
|
|
8502
8502
|
{
|
|
8503
8503
|
"path": "scripts/gds/claim.js",
|
|
8504
8504
|
"mode": "0000644",
|
|
8505
|
-
"sha256": "
|
|
8505
|
+
"sha256": "c9a7f39cf1f03e985466005a1857f570b4b3d449b0cbd8b3dfd0f7a7524a7c31"
|
|
8506
8506
|
},
|
|
8507
8507
|
{
|
|
8508
8508
|
"path": "scripts/gds/classify-discipline.js",
|
|
@@ -9259,6 +9259,11 @@
|
|
|
9259
9259
|
"mode": "0000644",
|
|
9260
9260
|
"sha256": "084fa5c4da1e755fa0edde5ec770e5151a9f66a247bca222bc7a2366ccf61748"
|
|
9261
9261
|
},
|
|
9262
|
+
{
|
|
9263
|
+
"path": "scripts/gds/review-sheet.js",
|
|
9264
|
+
"mode": "0000644",
|
|
9265
|
+
"sha256": "a0234b79890f9eae0fdc9d1910b723d61d21ca6b2888092b45084fb13c0c63c6"
|
|
9266
|
+
},
|
|
9262
9267
|
{
|
|
9263
9268
|
"path": "scripts/gds/role-pack-guard.js",
|
|
9264
9269
|
"mode": "0000644",
|
|
@@ -9759,6 +9764,11 @@
|
|
|
9759
9764
|
"mode": "0000644",
|
|
9760
9765
|
"sha256": "be4fec619d5a0c76ba05bcc75042c1497f70da21b54a3d646884e2228f90b2ba"
|
|
9761
9766
|
},
|
|
9767
|
+
{
|
|
9768
|
+
"path": "scripts/gds/visual-review-nudge.js",
|
|
9769
|
+
"mode": "0000644",
|
|
9770
|
+
"sha256": "a86b2cd427c9b52090d906b0b5febb9d8098fae1589e01828e4d7698d38db599"
|
|
9771
|
+
},
|
|
9762
9772
|
{
|
|
9763
9773
|
"path": "scripts/gds/worktree-claim-guard.js",
|
|
9764
9774
|
"mode": "0000644",
|
|
@@ -10192,7 +10202,7 @@
|
|
|
10192
10202
|
{
|
|
10193
10203
|
"path": "src/module-api.js",
|
|
10194
10204
|
"mode": "0000644",
|
|
10195
|
-
"sha256": "
|
|
10205
|
+
"sha256": "2a9b6a4c34d0321b9684a3054d93a5224359fcdc89d81df9ec01bee4334c25c7"
|
|
10196
10206
|
},
|
|
10197
10207
|
{
|
|
10198
10208
|
"path": "src/module-loader/catalog.js",
|
|
@@ -14614,6 +14624,11 @@
|
|
|
14614
14624
|
"mode": "0000644",
|
|
14615
14625
|
"sha256": "8a8df7e2a025029ffe7e18b7c7b18c6bd92a256822609931f0cd88628e446041"
|
|
14616
14626
|
},
|
|
14627
|
+
{
|
|
14628
|
+
"path": "tests/visual_review.mjs",
|
|
14629
|
+
"mode": "0000644",
|
|
14630
|
+
"sha256": "6c583bd0cb3c2611044e5adad4c17a57d5aed89ba5daf921c355903df239a2ab"
|
|
14631
|
+
},
|
|
14617
14632
|
{
|
|
14618
14633
|
"path": "tests/wandering_prefs.mjs",
|
|
14619
14634
|
"mode": "0000644",
|
|
@@ -49,7 +49,8 @@ Generated code uses the `--dt-*` variables; adapters write only `src/ui/<surface
|
|
|
49
49
|
- `node scripts/gds/validate-design.js --strict` after touching adapters, tokens or `design-sources.json` (fitness runs it on every PR).
|
|
50
50
|
- **New interactive code gets a pin in the page's own test file in the same diff** — the grader's Quality worker fails a chrome change that arrives with no test. Contrast pairs are measured in CI, not eyeballed.
|
|
51
51
|
- The task's DONE WHEN is read literally — "both pages" means both pages. Deliberate deviations from a mock stay minor if the ship notes say why.
|
|
52
|
-
-
|
|
52
|
+
- **The person you're working with OKs it before it ships** — the project default for every builder (owner, 2026-09-23). Open the before/after shots in their browser with `node scripts/gds/review-sheet.js --title … --shot "<caption>=<png>"` (a terminal shows no images, so a screenshot you only read yourself never reached them), and wait. Autonomous: no one to ask, so list the shots in the notes and ship; the change files a visual review that holds the deploy (ADR 0241), never the ship.
|
|
53
|
+
- `/builder-ship` with real `--notes` (what changed, which screens you looked at, in which modes, and who OKed them) and a plain-language `--summary`.
|
|
53
54
|
|
|
54
55
|
## Round-tripping with a design tool
|
|
55
56
|
|
|
@@ -2448,5 +2448,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2448
2448
|
landed since 1.19.985 with no explicit bump. run 35903379073. (task 1002620)
|
|
2449
2449
|
1.19.987 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2450
2450
|
landed since 1.19.986 with no explicit bump. run 35906802480. (task 1002620)
|
|
2451
|
+
1.19.988 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2452
|
+
landed since 1.19.987 with no explicit bump. run 35909292753. (task 1002620)
|
|
2451
2453
|
---------------------------------------------------------------------------
|
|
2452
2454
|
```
|
package/docs/packs/artist.md
CHANGED
|
@@ -15,6 +15,7 @@ In every other craft Claude narrates — diffs, logs, paragraphs about what it i
|
|
|
15
15
|
- **End every step with something visual.** After a generation, display the image. After a copy change, show the rendered text in place, not a diff. After a check, show the scorecard, not a prose summary. They should see the result before they read a word.
|
|
16
16
|
- **Collapse the plumbing to one-liners.** Budget checks, config edits, retry tiers, atlas rebuilds — do them, report them as a single line or a number. "Generated, scored 0.91, on-palette, staged ✓" beats three paragraphs.
|
|
17
17
|
- **Verification is looking, not reading.** Surface the rendered surface and the sandbox preview, not console logs. Logs are for when something breaks, not for routine success.
|
|
18
|
+
- **Nothing that changes a page ships until they have seen it** (the project default, owner 2026-09-23). Show the before and after in both modes at desktop and phone width, opened in their browser with `node scripts/gds/review-sheet.js` (a terminal shows no images), and ship on their OK.
|
|
18
19
|
|
|
19
20
|
## The second rule: as little interpretation as possible
|
|
20
21
|
|
package/docs/packs/engineer.md
CHANGED
|
@@ -20,7 +20,8 @@ Engineering is the project's **default loop**, so this pack introduces no new dy
|
|
|
20
20
|
|
|
21
21
|
4. **Verify it — never report done on faith.** The step engineers most often skip.
|
|
22
22
|
- Run the smoke tests yourself while iterating (`/builder-ship` runs them too, but later is not the same as sooner).
|
|
23
|
-
- If the change is observable in the running app, use the preview/verification workflow (preview → reload → console + page check → screenshot proof)
|
|
23
|
+
- If the change is observable in the running app, use the preview/verification workflow (preview → reload → console + page check → screenshot proof) yourself first. Verify, then share the proof.
|
|
24
|
+
- **A change to a page people see is reviewed by your human before it ships — the project default for every builder (owner, 2026-09-23).** Render it before and after, in both modes, at desktop and phone width; open the shots for them with `node scripts/gds/review-sheet.js` (a terminal shows no images — this opens one page in their browser); wait for their OK, then ship. With nobody to ask, list the shots in the ship notes and ship: the change also files a visual review on the board that holds the deploy (ADR 0241), never the ship.
|
|
24
25
|
- If it is not browser-observable (a different runtime, types, tooling), say so and verify the way that *does* exercise it.
|
|
25
26
|
- `/ship-check` runs every freshness and fitness gate `ship.js` will run, and prints the healing command for each stale generated file.
|
|
26
27
|
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.988",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.988",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.988",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/scripts/gds/claim.js
CHANGED
|
@@ -36,6 +36,7 @@ const { plainCardLines } = require('./plain-cards');
|
|
|
36
36
|
// and the detector. A lifecycle CLI may require the lifecycle module directly
|
|
37
37
|
// (the documented exception in modules/lifecycle/CLAUDE.md).
|
|
38
38
|
const { splitDeadDeps, deadDepHint } = require('../../modules/lifecycle/dead-deps.js');
|
|
39
|
+
const { visualReviewNudge } = require('./visual-review-nudge');
|
|
39
40
|
|
|
40
41
|
// Fetch up to 3 learnings relevant to the task being claimed. touches[] cleanup
|
|
41
42
|
// [3/8] (task 878): key off the task's discipline + title (always present) rather
|
|
@@ -728,6 +729,12 @@ async function main() {
|
|
|
728
729
|
// playbook) into its skill. Printed right after the description so it's the
|
|
729
730
|
// last thing the builder reads before deciding how to work — task 1267.
|
|
730
731
|
printDisciplineMode(task);
|
|
732
|
+
// task 1004202: a task that changes a page is told to use /design and get its
|
|
733
|
+
// human's OK on the before/after. A nudge, never a gate (ADR 0162).
|
|
734
|
+
try {
|
|
735
|
+
const nudge = visualReviewNudge(task);
|
|
736
|
+
if (nudge) console.log(`\n${nudge.join('\n')}`);
|
|
737
|
+
} catch (_) { /* never let a hint break a successful claim */ }
|
|
731
738
|
if (worktree) console.log(`\nWorktree: ${worktree}`);
|
|
732
739
|
// Claim-time knowledge (#956 / V4.R18, criterion sessions-start-knowing):
|
|
733
740
|
// learnings first (curated, short), then the top recall sources for this task
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// scripts/gds/review-sheet.js — open before/after screenshots for a person to
|
|
5
|
+
// review, as ONE page in their own browser (task 1004202).
|
|
6
|
+
//
|
|
7
|
+
// WHY. Every change to a rendered surface is reviewed by the session's human
|
|
8
|
+
// before it ships — the project default since the owner's 2026-09-23 decision.
|
|
9
|
+
// A terminal shows no images: a screenshot an agent "sends" or reads into its
|
|
10
|
+
// own context never reaches a CLI user's eyes. So the review is a local HTML page
|
|
11
|
+
// of captioned shots, opened with the OS's own opener. Nothing is uploaded.
|
|
12
|
+
//
|
|
13
|
+
// Usage:
|
|
14
|
+
// node scripts/gds/review-sheet.js --title "Task 1004200 — graph titles" \
|
|
15
|
+
// --shot "Before — 1280, light=C:/shots/before.png" \
|
|
16
|
+
// --shot "After — 1280, light=C:/shots/after.png" \
|
|
17
|
+
// [--note "One line on what changed."] [--out <dir>] [--no-open]
|
|
18
|
+
//
|
|
19
|
+
// Prints the page's path. Each --shot is "<caption>=<image path>", in order.
|
|
20
|
+
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
const os = require('node:os');
|
|
23
|
+
const path = require('node:path');
|
|
24
|
+
const { pathToFileURL } = require('node:url');
|
|
25
|
+
const { spawn } = require('node:child_process');
|
|
26
|
+
|
|
27
|
+
function escapeHtml(s) {
|
|
28
|
+
return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// parseArgs — the flags above; `shots` keep their order. A --shot with no "="
|
|
32
|
+
// is an error, not a guess: the caption and the path cannot be told apart.
|
|
33
|
+
function parseArgs(argv) {
|
|
34
|
+
const out = { title: 'Visual review', note: '', shots: [], outDir: null, open: true, errors: [] };
|
|
35
|
+
for (let i = 0; i < argv.length; i++) {
|
|
36
|
+
const a = argv[i];
|
|
37
|
+
if (a === '--no-open') { out.open = false; continue; }
|
|
38
|
+
const v = argv[i + 1];
|
|
39
|
+
if (a === '--title') { out.title = v; i++; }
|
|
40
|
+
else if (a === '--note') { out.note = v; i++; }
|
|
41
|
+
else if (a === '--out') { out.outDir = v; i++; }
|
|
42
|
+
else if (a === '--shot') {
|
|
43
|
+
i++;
|
|
44
|
+
const at = String(v || '').lastIndexOf('=');
|
|
45
|
+
if (at < 1) out.errors.push(`--shot needs "<caption>=<image path>", got: ${v}`);
|
|
46
|
+
else out.shots.push({ caption: v.slice(0, at).trim(), file: v.slice(at + 1).trim() });
|
|
47
|
+
} else out.errors.push(`unknown argument: ${a}`);
|
|
48
|
+
}
|
|
49
|
+
if (!out.shots.length) out.errors.push('at least one --shot is required');
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// buildReviewPage — the page itself. Images are absolute file:// URLs, so the
|
|
54
|
+
// page works wherever it is written and nothing is copied.
|
|
55
|
+
function buildReviewPage({ title, note, shots }) {
|
|
56
|
+
const figures = shots.map((s) => `<figure><figcaption>${escapeHtml(s.caption)}</figcaption>`
|
|
57
|
+
+ `<img src="${escapeHtml(pathToFileURL(path.resolve(s.file)).href)}" alt="${escapeHtml(s.caption)}"></figure>`).join('\n');
|
|
58
|
+
return `<!doctype html>
|
|
59
|
+
<html lang="en">
|
|
60
|
+
<head>
|
|
61
|
+
<meta charset="utf-8">
|
|
62
|
+
<title>${escapeHtml(title)}</title>
|
|
63
|
+
<style>
|
|
64
|
+
body { margin: 0; padding: 24px; font: 15px/1.5 system-ui, sans-serif; background: #f4f4f5; color: #17181a; }
|
|
65
|
+
h1 { font-size: 20px; margin: 0 0 6px; }
|
|
66
|
+
p { margin: 0 0 20px; max-width: 70ch; }
|
|
67
|
+
figure { margin: 0 0 28px; }
|
|
68
|
+
figcaption { font-weight: 600; margin-bottom: 6px; }
|
|
69
|
+
img { display: block; max-width: 100%; border: 1px solid #d4d4d8; border-radius: 8px; }
|
|
70
|
+
</style>
|
|
71
|
+
</head>
|
|
72
|
+
<body>
|
|
73
|
+
<h1>${escapeHtml(title)}</h1>
|
|
74
|
+
${note ? `<p>${escapeHtml(note)}</p>\n` : ''}${figures}
|
|
75
|
+
</body>
|
|
76
|
+
</html>
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function openInBrowser(file) {
|
|
81
|
+
// `start`'s first quoted argument is a window title, hence the empty "".
|
|
82
|
+
const [cmd, args] = process.platform === 'win32' ? ['cmd', ['/c', 'start', '""', `"${file}"`]]
|
|
83
|
+
: process.platform === 'darwin' ? ['open', [file]] : ['xdg-open', [file]];
|
|
84
|
+
const child = spawn(cmd, args, { detached: true, stdio: 'ignore', windowsVerbatimArguments: process.platform === 'win32' });
|
|
85
|
+
child.on('error', () => console.error(`Could not open a browser — open this file yourself: ${file}`));
|
|
86
|
+
child.unref();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function main(argv) {
|
|
90
|
+
const opts = parseArgs(argv);
|
|
91
|
+
const missing = opts.shots.filter((s) => !fs.existsSync(s.file)).map((s) => `no such image: ${s.file}`);
|
|
92
|
+
const errors = [...opts.errors, ...missing];
|
|
93
|
+
if (errors.length) {
|
|
94
|
+
console.error(errors.join('\n'));
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
const dir = opts.outDir || fs.mkdtempSync(path.join(os.tmpdir(), 'bongos-review-'));
|
|
98
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
99
|
+
const file = path.join(dir, 'index.html');
|
|
100
|
+
fs.writeFileSync(file, buildReviewPage(opts));
|
|
101
|
+
console.log(file);
|
|
102
|
+
if (opts.open) openInBrowser(file);
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (require.main === module) process.exitCode = main(process.argv.slice(2));
|
|
107
|
+
|
|
108
|
+
module.exports = { parseArgs, buildReviewPage };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// scripts/gds/visual-review-nudge.js — the claim-time lines that tell a session
|
|
4
|
+
// its task changes a page people see (task 1004202): use /design, check it in a
|
|
5
|
+
// real browser, and get the human's OK on the before/after before shipping — the
|
|
6
|
+
// project default for every builder since the owner's 2026-09-23 decision.
|
|
7
|
+
//
|
|
8
|
+
// A NUDGE, never a gate (ADR 0162). "Is this UI work" is the ONE predicate the
|
|
9
|
+
// artist review and the grader's copy lens already use (lifecycle/cascade.js), so
|
|
10
|
+
// the three cannot disagree. Nothing has changed yet at claim time, so it is fed
|
|
11
|
+
// the paths the task NAMES: its touches[] and any modules/*/public/ path written
|
|
12
|
+
// in its description.
|
|
13
|
+
|
|
14
|
+
const { hasCopyOrVisualWork } = require('../../modules/lifecycle/cascade.js');
|
|
15
|
+
|
|
16
|
+
const PUBLIC_PATH_IN_TEXT = /modules\/[\w.-]+\/public\/[^\s`'"()<>[\]{}|,;]+/g;
|
|
17
|
+
|
|
18
|
+
// pathsNamedIn — touches[] plus every public/ path the description mentions,
|
|
19
|
+
// with sentence punctuation stripped off the end ("…/goal-forest-lib.js." → .js).
|
|
20
|
+
function pathsNamedIn(task) {
|
|
21
|
+
const touches = Array.isArray(task && task.touches) ? task.touches.filter((t) => typeof t === 'string') : [];
|
|
22
|
+
const named = (String((task && task.description) || '').match(PUBLIC_PATH_IN_TEXT) || [])
|
|
23
|
+
.map((p) => p.replace(/[.:]+$/, ''));
|
|
24
|
+
return [...touches, ...named];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// visualReviewNudge — the lines to print, or null when the task is not UI work.
|
|
28
|
+
function visualReviewNudge(task) {
|
|
29
|
+
if (!hasCopyOrVisualWork(task, pathsNamedIn(task))) return null;
|
|
30
|
+
return [
|
|
31
|
+
'🎨 This task changes a page people see.',
|
|
32
|
+
' Build it with /design, and check it in a real browser at desktop and phone width, light and dark.',
|
|
33
|
+
' Before you ship: open the before/after for your human (node scripts/gds/review-sheet.js) and wait for their OK.',
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { pathsNamedIn, visualReviewNudge };
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.988'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// tests/visual_review.mjs — the visual review as the project default for every
|
|
2
|
+
// builder (task 1004202; owner decision 2026-09-23).
|
|
3
|
+
//
|
|
4
|
+
// Three parts, pinned together because each is useless without the others: the
|
|
5
|
+
// review page a CLI user can actually SEE (review-sheet.js), the claim-time nudge
|
|
6
|
+
// that tells a UI task to use it (visual-review-nudge.js + claim.js), and the
|
|
7
|
+
// three playbooks every builder's session reads, which carry the rule itself.
|
|
8
|
+
//
|
|
9
|
+
// Run: node tests/visual_review.mjs
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { spawnSync } from 'node:child_process';
|
|
15
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
16
|
+
import { makeRunner } from './helpers.mjs';
|
|
17
|
+
import { parseArgs, buildReviewPage } from '../scripts/gds/review-sheet.js';
|
|
18
|
+
import { pathsNamedIn, visualReviewNudge } from '../scripts/gds/visual-review-nudge.js';
|
|
19
|
+
|
|
20
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
|
22
|
+
const { test, summary } = makeRunner();
|
|
23
|
+
|
|
24
|
+
// ---- the review page ---------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
await test('parseArgs keeps the shots in order and splits caption from path at the LAST "="', () => {
|
|
27
|
+
const o = parseArgs(['--title', 'T', '--shot', 'Before — a=b=C:/x/one.png', '--shot', 'After=C:/x/two.png', '--no-open']);
|
|
28
|
+
assert.deepEqual(o.shots, [{ caption: 'Before — a=b', file: 'C:/x/one.png' }, { caption: 'After', file: 'C:/x/two.png' }]);
|
|
29
|
+
assert.equal(o.open, false);
|
|
30
|
+
assert.deepEqual(o.errors, []);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
await test('parseArgs refuses a shot it cannot split, and a page with no shots', () => {
|
|
34
|
+
assert.match(parseArgs(['--shot', 'nocaption']).errors.join('\n'), /needs "<caption>=<image path>"/);
|
|
35
|
+
assert.match(parseArgs([]).errors.join('\n'), /at least one --shot/);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await test('the page shows every shot as a captioned image, and no caption becomes markup', () => {
|
|
39
|
+
const html = buildReviewPage({
|
|
40
|
+
title: 'Task <b>1</b>',
|
|
41
|
+
note: 'A & B',
|
|
42
|
+
shots: [{ caption: 'Before <img onerror=x>', file: 'C:/shots/before.png' }, { caption: 'After', file: 'C:/shots/after.png' }],
|
|
43
|
+
});
|
|
44
|
+
assert.ok(!html.includes('<b>1</b>') && !html.includes('<img onerror'), 'captions and title are escaped');
|
|
45
|
+
assert.ok(html.includes('A & B'));
|
|
46
|
+
const srcs = [...html.matchAll(/<img src="([^"]+)"/g)].map((m) => m[1]);
|
|
47
|
+
assert.deepEqual(srcs, [pathToFileURL(path.resolve('C:/shots/before.png')).href, pathToFileURL(path.resolve('C:/shots/after.png')).href]);
|
|
48
|
+
assert.ok(html.indexOf('Before') < html.indexOf('After'), 'in the order given');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await test('the CLI writes the page and prints its path; a missing image is an error, not a blank page', () => {
|
|
52
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'visual-review-test-'));
|
|
53
|
+
const png = path.join(dir, 'shot.png');
|
|
54
|
+
fs.writeFileSync(png, 'not really a png');
|
|
55
|
+
const cli = path.join(ROOT, 'scripts', 'gds', 'review-sheet.js');
|
|
56
|
+
const ok = spawnSync(process.execPath, [cli, '--shot', `Before=${png}`, '--out', dir, '--no-open'], { encoding: 'utf8' });
|
|
57
|
+
assert.equal(ok.status, 0, ok.stderr);
|
|
58
|
+
assert.equal(ok.stdout.trim(), path.join(dir, 'index.html'));
|
|
59
|
+
assert.match(fs.readFileSync(path.join(dir, 'index.html'), 'utf8'), /<figcaption>Before<\/figcaption>/);
|
|
60
|
+
const bad = spawnSync(process.execPath, [cli, '--shot', `After=${path.join(dir, 'nope.png')}`, '--no-open'], { encoding: 'utf8' });
|
|
61
|
+
assert.equal(bad.status, 1);
|
|
62
|
+
assert.match(bad.stderr, /no such image/);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ---- the claim-time nudge ----------------------------------------------------
|
|
66
|
+
|
|
67
|
+
const task = (over = {}) => ({ id: '1', discipline: 'engineer', module_key: null, source: 'manual', touches: [], description: '', ...over });
|
|
68
|
+
|
|
69
|
+
await test('a task that names a page file in its description is told to use /design and get an OK', () => {
|
|
70
|
+
const t = task({ description: 'Where: modules/hall-ui/public/goal-forest-lib.js — NODE_W = 168. Drawing is modules/hall-ui/public/goal-forest-render.js.' });
|
|
71
|
+
assert.deepEqual(pathsNamedIn(t), ['modules/hall-ui/public/goal-forest-lib.js', 'modules/hall-ui/public/goal-forest-render.js'],
|
|
72
|
+
'sentence punctuation is not part of a path');
|
|
73
|
+
const lines = visualReviewNudge(t);
|
|
74
|
+
assert.ok(lines, 'nudged');
|
|
75
|
+
assert.match(lines.join('\n'), /\/design/);
|
|
76
|
+
assert.match(lines.join('\n'), /review-sheet\.js/);
|
|
77
|
+
assert.match(lines.join('\n'), /OK/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await test('touches[], a UI module label, and the artist craft nudge too — the same predicate the review uses', () => {
|
|
81
|
+
assert.ok(visualReviewNudge(task({ touches: ['modules/public-landing/public/projects.html'] })));
|
|
82
|
+
assert.ok(visualReviewNudge(task({ module_key: 'hall-ui' })));
|
|
83
|
+
assert.ok(visualReviewNudge(task({ discipline: 'artist' })));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await test('a backend task is not nudged — no noise on work that renders nothing', () => {
|
|
87
|
+
assert.equal(visualReviewNudge(task({ description: 'Fix modules/lifecycle/cascade.js and modules/hall-ui/module.json.' })), null);
|
|
88
|
+
assert.equal(visualReviewNudge(task({ touches: ['scripts/gds/claim.js'] })), null);
|
|
89
|
+
assert.equal(visualReviewNudge(task()), null);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await test('claim.js prints the nudge after the craft directive, and cannot fail a claim with it', () => {
|
|
93
|
+
const src = read('scripts/gds/claim.js');
|
|
94
|
+
const at = src.indexOf('visualReviewNudge(task)');
|
|
95
|
+
assert.ok(at > src.indexOf('printDisciplineMode(task);'), 'right after the craft directive');
|
|
96
|
+
assert.match(src.slice(at - 200, at + 300), /catch \(_\)/, 'wrapped, so a hint can never break a successful claim');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ---- the rule, where every builder's session reads it ------------------------
|
|
100
|
+
|
|
101
|
+
await test('all three playbooks carry the rule and the way to show a CLI user the shots', () => {
|
|
102
|
+
for (const rel of ['docs/packs/engineer.md', 'docs/packs/artist.md', '.claude/skills/design/SKILL.md']) {
|
|
103
|
+
const doc = read(rel);
|
|
104
|
+
assert.match(doc, /review-sheet\.js/, `${rel} names the helper`);
|
|
105
|
+
assert.match(doc, /2026-09-23/, `${rel} dates the owner decision`);
|
|
106
|
+
}
|
|
107
|
+
assert.doesNotMatch(read('docs/packs/engineer.md'), /rather than asking a human to look/, 'the old contrary instruction is gone');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
summary();
|