@bongos/core 1.19.987 → 1.19.989
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 +33 -18
- package/.claude/skills/design/SKILL.md +2 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/packs/artist.md +1 -0
- package/docs/packs/engineer.md +2 -1
- package/modules/hall-ui/public/style.css +172 -0
- package/modules/hall-ui/public/work.css +1 -170
- 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/hall_board_world.mjs +9 -2
- 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.989",
|
|
6
|
+
"core_contract": "1.19.989",
|
|
7
|
+
"source_commit": "e2ff7b827391116e3e99f4b850441ca7482a3466",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-23T19:
|
|
9
|
+
"built_at": "2026-09-23T19:44:16.401Z",
|
|
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": "65dea20f4453b26ed36b673a7a34da0008c338d0f4307aa1b327f3171ce42cb0",
|
|
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": "b06d9ffcfde6e55a0997ffa926a671d0f53478d5e7c351b92374485e359cc0cd"
|
|
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",
|
|
@@ -5407,7 +5407,7 @@
|
|
|
5407
5407
|
{
|
|
5408
5408
|
"path": "modules/hall-ui/public/style.css",
|
|
5409
5409
|
"mode": "0000644",
|
|
5410
|
-
"sha256": "
|
|
5410
|
+
"sha256": "b8a9b64e1febe57eddbb1005335712a19e581e69d16bb42346eba24bd73acfb7"
|
|
5411
5411
|
},
|
|
5412
5412
|
{
|
|
5413
5413
|
"path": "modules/hall-ui/public/task-detail.js",
|
|
@@ -5492,7 +5492,7 @@
|
|
|
5492
5492
|
{
|
|
5493
5493
|
"path": "modules/hall-ui/public/work.css",
|
|
5494
5494
|
"mode": "0000644",
|
|
5495
|
-
"sha256": "
|
|
5495
|
+
"sha256": "68399594a00fdffaf9833d3105d8c9e2f5872642b0f49868edae6cc526fa9021"
|
|
5496
5496
|
},
|
|
5497
5497
|
{
|
|
5498
5498
|
"path": "modules/hall-ui/public/work.html",
|
|
@@ -8162,12 +8162,12 @@
|
|
|
8162
8162
|
{
|
|
8163
8163
|
"path": "package-lock.json",
|
|
8164
8164
|
"mode": "0000644",
|
|
8165
|
-
"sha256": "
|
|
8165
|
+
"sha256": "09936259916688c880c3c787419714a83a798ccf7fd09b50d7fe26d3d293e600"
|
|
8166
8166
|
},
|
|
8167
8167
|
{
|
|
8168
8168
|
"path": "package.json",
|
|
8169
8169
|
"mode": "0000644",
|
|
8170
|
-
"sha256": "
|
|
8170
|
+
"sha256": "126708cbc785a14f0cf42ef0a151889033a852f1a331e6ab73b0c75743f41856"
|
|
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": "5ed09f42ce4e04ca5f27bc3d4004ceab18bf604bd8a8fca72648afbf389b0935"
|
|
10196
10206
|
},
|
|
10197
10207
|
{
|
|
10198
10208
|
"path": "src/module-loader/catalog.js",
|
|
@@ -12067,7 +12077,7 @@
|
|
|
12067
12077
|
{
|
|
12068
12078
|
"path": "tests/hall_board_world.mjs",
|
|
12069
12079
|
"mode": "0000644",
|
|
12070
|
-
"sha256": "
|
|
12080
|
+
"sha256": "48c6176c97d4cc167c57aeaf77baa78667731125c5ff2369564f08cbe9a146c7"
|
|
12071
12081
|
},
|
|
12072
12082
|
{
|
|
12073
12083
|
"path": "tests/hall_builders_page_pass.mjs",
|
|
@@ -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,9 @@ 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)
|
|
2453
|
+
1.19.989 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2454
|
+
landed since 1.19.988 with no explicit bump. run 35911149318. (task 1002620)
|
|
2451
2455
|
---------------------------------------------------------------------------
|
|
2452
2456
|
```
|
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
|
|
|
@@ -2661,6 +2661,178 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
|
|
|
2661
2661
|
text-transform: var(--label-case);
|
|
2662
2662
|
color: var(--ink-soft);
|
|
2663
2663
|
}
|
|
2664
|
+
/* How old the task is (task 1002955, OTBKit.ageChipHtml). Quiet by default —
|
|
2665
|
+
it's orientation, not a call to act — but a task past the stale threshold
|
|
2666
|
+
steps up from faint to soft so a queue that has been sitting reads at a
|
|
2667
|
+
glance. The exact date is on the tooltip, hence `cursor: help`. Deliberately
|
|
2668
|
+
NOT --warn: that token is the verification vocabulary and as bare text on the
|
|
2669
|
+
dark card it loses contrast. --ink-soft steps up legibly in both modes. */
|
|
2670
|
+
.task__age {
|
|
2671
|
+
font-family: var(--font-mono);
|
|
2672
|
+
font-size: 11.5px;
|
|
2673
|
+
font-variant-numeric: tabular-nums;
|
|
2674
|
+
color: var(--ink-faint);
|
|
2675
|
+
white-space: nowrap;
|
|
2676
|
+
cursor: help;
|
|
2677
|
+
}
|
|
2678
|
+
.task__age--stale { color: var(--ink-soft); }
|
|
2679
|
+
|
|
2680
|
+
/* The four tag variants that carry meaning beyond "a fact". Each keeps the
|
|
2681
|
+
outlined shape style.css gives .task__tag and only changes what it says. */
|
|
2682
|
+
.task__tag--newcomer {
|
|
2683
|
+
background: var(--ok-weak);
|
|
2684
|
+
color: var(--ok);
|
|
2685
|
+
border-color: color-mix(in srgb, var(--ok) 30%, transparent);
|
|
2686
|
+
}
|
|
2687
|
+
.task__tag--mine {
|
|
2688
|
+
background: var(--accent-tint);
|
|
2689
|
+
color: var(--accent-ink);
|
|
2690
|
+
border-color: color-mix(in srgb, var(--accent) 35%, transparent);
|
|
2691
|
+
}
|
|
2692
|
+
/* task 1498 / ADR 0084: per-task rank floor. Outlined + emphasized (not a
|
|
2693
|
+
filled tag) so a gated task reads as "authority required", distinct from the
|
|
2694
|
+
filled newcomer tag. --warn is the verification vocabulary, kept on purpose. */
|
|
2695
|
+
.task__tag--rank {
|
|
2696
|
+
background: transparent;
|
|
2697
|
+
color: var(--warn);
|
|
2698
|
+
border-color: color-mix(in srgb, var(--warn) 45%, transparent);
|
|
2699
|
+
}
|
|
2700
|
+
/* task 1752: the open (no floor) state — muted so "Any rank" reads as "open to
|
|
2701
|
+
all", not as a warning like the gated --rank tag above. */
|
|
2702
|
+
.task__tag--rank-open {
|
|
2703
|
+
background: transparent;
|
|
2704
|
+
color: var(--ink-faint);
|
|
2705
|
+
border-color: var(--rule-soft);
|
|
2706
|
+
}
|
|
2707
|
+
/* ---- #616 / task 2170 / task 1003304: claim + copy on the claimable card ----
|
|
2708
|
+
The card's geometry, tags, credits figure, checkbox and copy button all live
|
|
2709
|
+
in this work-board section (they are the re-skin), and so do the primary door,
|
|
2710
|
+
the touches chip and the whole-card selection target (promoted from work.css,
|
|
2711
|
+
task 1003659, so their cascade position is the same on every page). */
|
|
2712
|
+
|
|
2713
|
+
/* The page's one primary action, and the only accent FILL on the board.
|
|
2714
|
+
--accent-on is the dark ink that goes on the salmon: white on it fails AA and
|
|
2715
|
+
DESIGN.md bans it outright, which is what the old `color: #fff` was. */
|
|
2716
|
+
.task__claim {
|
|
2717
|
+
display: inline-flex;
|
|
2718
|
+
align-items: center;
|
|
2719
|
+
justify-content: center;
|
|
2720
|
+
min-height: 34px;
|
|
2721
|
+
min-width: 74px;
|
|
2722
|
+
padding: 0 16px;
|
|
2723
|
+
background: var(--accent);
|
|
2724
|
+
border: 0;
|
|
2725
|
+
color: var(--accent-on);
|
|
2726
|
+
border-radius: var(--radius-pill);
|
|
2727
|
+
font-size: 13.5px;
|
|
2728
|
+
font-weight: 500;
|
|
2729
|
+
text-transform: lowercase;
|
|
2730
|
+
cursor: pointer;
|
|
2731
|
+
white-space: nowrap;
|
|
2732
|
+
transition: background 150ms, opacity 150ms;
|
|
2733
|
+
}
|
|
2734
|
+
.task__claim:hover:not(:disabled) { background: var(--accent-lift); color: var(--accent-on); }
|
|
2735
|
+
.task__claim:disabled { background: var(--glass); color: var(--ink-faint); cursor: default; }
|
|
2736
|
+
|
|
2737
|
+
/* Files a claimable task is responsible for — a compact count chip in the meta
|
|
2738
|
+
row; the full path list rides the tooltip (task 2170 density pass). The mono
|
|
2739
|
+
fact voice: this is a number the ledger produced. */
|
|
2740
|
+
.task__touches {
|
|
2741
|
+
font-family: var(--font-mono);
|
|
2742
|
+
font-size: 11.5px;
|
|
2743
|
+
font-variant-numeric: tabular-nums;
|
|
2744
|
+
color: var(--ink-faint);
|
|
2745
|
+
white-space: nowrap;
|
|
2746
|
+
cursor: help;
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
/* task 1753: the whole card is clickable → toggles selection (task 1869). */
|
|
2750
|
+
.task { cursor: pointer; }
|
|
2751
|
+
|
|
2752
|
+
@media (max-width: 640px) {
|
|
2753
|
+
.task__actions { grid-column: 1 / -1; justify-self: start; padding-top: 6px; }
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
/* ---- task 1759: the bottom bulk-action toolbar (promoted from work.css, task 1003659) ---- */
|
|
2757
|
+
.bulk-toolbar {
|
|
2758
|
+
position: fixed;
|
|
2759
|
+
left: var(--sidebar-w);
|
|
2760
|
+
right: 0;
|
|
2761
|
+
bottom: 0;
|
|
2762
|
+
/* The chrome tier, not above it: the toast (z 100) has to stay readable over
|
|
2763
|
+
this bar, and the drawer + its scrim have to cover it. */
|
|
2764
|
+
z-index: var(--z-topbar);
|
|
2765
|
+
display: flex;
|
|
2766
|
+
align-items: center;
|
|
2767
|
+
gap: 16px;
|
|
2768
|
+
flex-wrap: wrap;
|
|
2769
|
+
padding: 12px 20px;
|
|
2770
|
+
/* TWO layers, and the bottom one is why. The topbar and the rail can be bare
|
|
2771
|
+
--glass because the page GROUND is behind them; this bar sits over the task
|
|
2772
|
+
cards, and in light --glass is a 5% ink tint with the blur at 0px, so the
|
|
2773
|
+
card underneath read straight through it and the buttons landed on top of a
|
|
2774
|
+
task title (looked at, 1280w light). Painting --bg under the tint makes the
|
|
2775
|
+
bar opaque without giving up the glass character or spending a literal. */
|
|
2776
|
+
background: linear-gradient(var(--glass), var(--glass)), var(--bg);
|
|
2777
|
+
border-top: var(--line) solid var(--glass-edge);
|
|
2778
|
+
backdrop-filter: blur(var(--glass-blur));
|
|
2779
|
+
-webkit-backdrop-filter: blur(var(--glass-blur));
|
|
2780
|
+
color: var(--ink);
|
|
2781
|
+
}
|
|
2782
|
+
.bulk-toolbar[hidden] { display: none; }
|
|
2783
|
+
.bulk-toolbar__count {
|
|
2784
|
+
font-family: var(--label-face);
|
|
2785
|
+
font-size: var(--label-size);
|
|
2786
|
+
letter-spacing: var(--label-space);
|
|
2787
|
+
text-transform: var(--label-case);
|
|
2788
|
+
color: var(--ink);
|
|
2789
|
+
white-space: nowrap;
|
|
2790
|
+
}
|
|
2791
|
+
.bulk-toolbar__actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-left: auto; }
|
|
2792
|
+
.bulk-toolbar__btn {
|
|
2793
|
+
display: inline-flex;
|
|
2794
|
+
align-items: center;
|
|
2795
|
+
justify-content: center;
|
|
2796
|
+
min-height: 30px;
|
|
2797
|
+
padding: 0 14px;
|
|
2798
|
+
background: var(--glass);
|
|
2799
|
+
border: var(--line) solid var(--glass-edge);
|
|
2800
|
+
color: var(--ink);
|
|
2801
|
+
border-radius: var(--radius-pill);
|
|
2802
|
+
font-size: 13px;
|
|
2803
|
+
text-transform: lowercase;
|
|
2804
|
+
cursor: pointer;
|
|
2805
|
+
white-space: nowrap;
|
|
2806
|
+
transition: background 150ms, opacity 150ms;
|
|
2807
|
+
}
|
|
2808
|
+
.bulk-toolbar__btn:hover:not(:disabled) { background: var(--glass-strong); }
|
|
2809
|
+
.bulk-toolbar__btn:disabled { color: var(--ink-faint); border-color: var(--rule-soft); cursor: not-allowed; }
|
|
2810
|
+
.bulk-toolbar__clear {
|
|
2811
|
+
min-height: 30px;
|
|
2812
|
+
padding: 0 10px;
|
|
2813
|
+
background: transparent;
|
|
2814
|
+
border: none;
|
|
2815
|
+
color: var(--ink-soft);
|
|
2816
|
+
font-size: 13px;
|
|
2817
|
+
text-transform: lowercase;
|
|
2818
|
+
text-decoration: underline;
|
|
2819
|
+
text-underline-offset: 4px;
|
|
2820
|
+
cursor: pointer;
|
|
2821
|
+
}
|
|
2822
|
+
.bulk-toolbar__clear:hover { color: var(--ink); }
|
|
2823
|
+
|
|
2824
|
+
/* The shell's .app-content already ends in 108px of bottom air at desktop
|
|
2825
|
+
width, which is what clears the bar — but at 768 it drops to 48px for the
|
|
2826
|
+
phone, and there the bar is TALLER (its actions wrap to their own row). So
|
|
2827
|
+
the phone width buys the room back, scoped to this page: goals.html loads
|
|
2828
|
+
this sheet too and has no bulk toolbar to clear. */
|
|
2829
|
+
@media (max-width: 768px) {
|
|
2830
|
+
/* At the drawer breakpoint the sidebar leaves the flow, so the bar spans. */
|
|
2831
|
+
.bulk-toolbar { left: 0; padding: 10px 14px; gap: 10px; }
|
|
2832
|
+
.bulk-toolbar__actions { margin-left: 0; width: 100%; }
|
|
2833
|
+
body[data-page="work"] .app-content { padding-bottom: 200px; }
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2664
2836
|
.task__credits { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; padding-top: 2px; }
|
|
2665
2837
|
/* A credit figure is DATA, not live — the display face in ink, never accent
|
|
2666
2838
|
(DESIGN.md, The One Warm Thing Rule + The Tabular Rule). */
|
|
@@ -46,93 +46,6 @@
|
|
|
46
46
|
color: var(--ink-faint);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/* ---- #616 / task 2170 / task 1003304: claim + copy on the claimable card ----
|
|
50
|
-
The card's geometry, tags, credits figure, checkbox and copy button all live
|
|
51
|
-
in style.css's work-board section now (they are the re-skin). What stays here
|
|
52
|
-
is the page-local behaviour: the primary door, the two meta chips, and the
|
|
53
|
-
fact that the whole card is a selection target. */
|
|
54
|
-
|
|
55
|
-
/* The page's one primary action, and the only accent FILL on the board.
|
|
56
|
-
--accent-on is the dark ink that goes on the salmon: white on it fails AA and
|
|
57
|
-
DESIGN.md bans it outright, which is what the old `color: #fff` was. */
|
|
58
|
-
.task__claim {
|
|
59
|
-
display: inline-flex;
|
|
60
|
-
align-items: center;
|
|
61
|
-
justify-content: center;
|
|
62
|
-
min-height: 34px;
|
|
63
|
-
min-width: 74px;
|
|
64
|
-
padding: 0 16px;
|
|
65
|
-
background: var(--accent);
|
|
66
|
-
border: 0;
|
|
67
|
-
color: var(--accent-on);
|
|
68
|
-
border-radius: var(--radius-pill);
|
|
69
|
-
font-size: 13.5px;
|
|
70
|
-
font-weight: 500;
|
|
71
|
-
text-transform: lowercase;
|
|
72
|
-
cursor: pointer;
|
|
73
|
-
white-space: nowrap;
|
|
74
|
-
transition: background 150ms, opacity 150ms;
|
|
75
|
-
}
|
|
76
|
-
.task__claim:hover:not(:disabled) { background: var(--accent-lift); color: var(--accent-on); }
|
|
77
|
-
.task__claim:disabled { background: var(--glass); color: var(--ink-faint); cursor: default; }
|
|
78
|
-
|
|
79
|
-
/* Files a claimable task is responsible for — a compact count chip in the meta
|
|
80
|
-
row; the full path list rides the tooltip (task 2170 density pass). The mono
|
|
81
|
-
fact voice: this is a number the ledger produced. */
|
|
82
|
-
.task__touches {
|
|
83
|
-
font-family: var(--font-mono);
|
|
84
|
-
font-size: 11.5px;
|
|
85
|
-
font-variant-numeric: tabular-nums;
|
|
86
|
-
color: var(--ink-faint);
|
|
87
|
-
white-space: nowrap;
|
|
88
|
-
cursor: help;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/* How old the task is (task 1002955, OTBKit.ageChipHtml). Quiet by default —
|
|
92
|
-
it's orientation, not a call to act — but a task past the stale threshold
|
|
93
|
-
steps up from faint to soft so a queue that has been sitting reads at a
|
|
94
|
-
glance. The exact date is on the tooltip, hence `cursor: help`. Deliberately
|
|
95
|
-
NOT --warn: that token is the verification vocabulary and as bare text on the
|
|
96
|
-
dark card it loses contrast. --ink-soft steps up legibly in both modes. */
|
|
97
|
-
.task__age {
|
|
98
|
-
font-family: var(--font-mono);
|
|
99
|
-
font-size: 11.5px;
|
|
100
|
-
font-variant-numeric: tabular-nums;
|
|
101
|
-
color: var(--ink-faint);
|
|
102
|
-
white-space: nowrap;
|
|
103
|
-
cursor: help;
|
|
104
|
-
}
|
|
105
|
-
.task__age--stale { color: var(--ink-soft); }
|
|
106
|
-
|
|
107
|
-
/* The three tag variants that carry meaning beyond "a fact". Each keeps the
|
|
108
|
-
outlined shape style.css gives .task__tag and only changes what it says. */
|
|
109
|
-
.task__tag--newcomer {
|
|
110
|
-
background: var(--ok-weak);
|
|
111
|
-
color: var(--ok);
|
|
112
|
-
border-color: color-mix(in srgb, var(--ok) 30%, transparent);
|
|
113
|
-
}
|
|
114
|
-
.task__tag--mine {
|
|
115
|
-
background: var(--accent-tint);
|
|
116
|
-
color: var(--accent-ink);
|
|
117
|
-
border-color: color-mix(in srgb, var(--accent) 35%, transparent);
|
|
118
|
-
}
|
|
119
|
-
/* task 1498 / ADR 0084: per-task rank floor. Outlined + emphasized (not a
|
|
120
|
-
filled tag) so a gated task reads as "authority required", distinct from the
|
|
121
|
-
filled newcomer tag. --warn is the verification vocabulary, kept on purpose. */
|
|
122
|
-
.task__tag--rank {
|
|
123
|
-
background: transparent;
|
|
124
|
-
color: var(--warn);
|
|
125
|
-
border-color: color-mix(in srgb, var(--warn) 45%, transparent);
|
|
126
|
-
}
|
|
127
|
-
/* task 1752: the open (no floor) state — muted so "Any rank" reads as "open to
|
|
128
|
-
all", not as a warning like the gated --rank tag above. */
|
|
129
|
-
.task__tag--rank-open {
|
|
130
|
-
background: transparent;
|
|
131
|
-
color: var(--ink-faint);
|
|
132
|
-
border-color: var(--rule-soft);
|
|
133
|
-
}
|
|
134
|
-
/* task 1753: the whole card is clickable → toggles selection (task 1869). */
|
|
135
|
-
.task { cursor: pointer; }
|
|
136
49
|
.backlog-count {
|
|
137
50
|
font-family: var(--font-mono);
|
|
138
51
|
font-size: 11px;
|
|
@@ -205,11 +118,8 @@
|
|
|
205
118
|
.history__goal { display: inline-block; margin-top: 3px; color: var(--ink-soft); font-size: 12.5px; }
|
|
206
119
|
.history__when { color: var(--ink-soft); font-size: 12.5px; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
207
120
|
|
|
208
|
-
@media (max-width: 640px) {
|
|
209
|
-
.task__actions { grid-column: 1 / -1; justify-self: start; padding-top: 6px; }
|
|
210
|
-
}
|
|
211
121
|
|
|
212
|
-
/* ---- task 1759: multi-select
|
|
122
|
+
/* ---- task 1759: multi-select checkbox (the toolbar itself is in style.css) ----
|
|
213
123
|
Re-cut for the 24px floor and the world (task 1003304). The floating dark
|
|
214
124
|
pill is gone: it painted an INVERTED surface (background: var(--ink)) with
|
|
215
125
|
three rgba(255,255,255,…) literals on top, which was correct only for as long
|
|
@@ -250,85 +160,6 @@
|
|
|
250
160
|
transform: rotate(42deg);
|
|
251
161
|
}
|
|
252
162
|
|
|
253
|
-
.bulk-toolbar {
|
|
254
|
-
position: fixed;
|
|
255
|
-
left: var(--sidebar-w);
|
|
256
|
-
right: 0;
|
|
257
|
-
bottom: 0;
|
|
258
|
-
/* The chrome tier, not above it: the toast (z 100) has to stay readable over
|
|
259
|
-
this bar, and the drawer + its scrim have to cover it. */
|
|
260
|
-
z-index: var(--z-topbar);
|
|
261
|
-
display: flex;
|
|
262
|
-
align-items: center;
|
|
263
|
-
gap: 16px;
|
|
264
|
-
flex-wrap: wrap;
|
|
265
|
-
padding: 12px 20px;
|
|
266
|
-
/* TWO layers, and the bottom one is why. The topbar and the rail can be bare
|
|
267
|
-
--glass because the page GROUND is behind them; this bar sits over the task
|
|
268
|
-
cards, and in light --glass is a 5% ink tint with the blur at 0px, so the
|
|
269
|
-
card underneath read straight through it and the buttons landed on top of a
|
|
270
|
-
task title (looked at, 1280w light). Painting --bg under the tint makes the
|
|
271
|
-
bar opaque without giving up the glass character or spending a literal. */
|
|
272
|
-
background: linear-gradient(var(--glass), var(--glass)), var(--bg);
|
|
273
|
-
border-top: var(--line) solid var(--glass-edge);
|
|
274
|
-
backdrop-filter: blur(var(--glass-blur));
|
|
275
|
-
-webkit-backdrop-filter: blur(var(--glass-blur));
|
|
276
|
-
color: var(--ink);
|
|
277
|
-
}
|
|
278
|
-
.bulk-toolbar[hidden] { display: none; }
|
|
279
|
-
.bulk-toolbar__count {
|
|
280
|
-
font-family: var(--label-face);
|
|
281
|
-
font-size: var(--label-size);
|
|
282
|
-
letter-spacing: var(--label-space);
|
|
283
|
-
text-transform: var(--label-case);
|
|
284
|
-
color: var(--ink);
|
|
285
|
-
white-space: nowrap;
|
|
286
|
-
}
|
|
287
|
-
.bulk-toolbar__actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-left: auto; }
|
|
288
|
-
.bulk-toolbar__btn {
|
|
289
|
-
display: inline-flex;
|
|
290
|
-
align-items: center;
|
|
291
|
-
justify-content: center;
|
|
292
|
-
min-height: 30px;
|
|
293
|
-
padding: 0 14px;
|
|
294
|
-
background: var(--glass);
|
|
295
|
-
border: var(--line) solid var(--glass-edge);
|
|
296
|
-
color: var(--ink);
|
|
297
|
-
border-radius: var(--radius-pill);
|
|
298
|
-
font-size: 13px;
|
|
299
|
-
text-transform: lowercase;
|
|
300
|
-
cursor: pointer;
|
|
301
|
-
white-space: nowrap;
|
|
302
|
-
transition: background 150ms, opacity 150ms;
|
|
303
|
-
}
|
|
304
|
-
.bulk-toolbar__btn:hover:not(:disabled) { background: var(--glass-strong); }
|
|
305
|
-
.bulk-toolbar__btn:disabled { color: var(--ink-faint); border-color: var(--rule-soft); cursor: not-allowed; }
|
|
306
|
-
.bulk-toolbar__clear {
|
|
307
|
-
min-height: 30px;
|
|
308
|
-
padding: 0 10px;
|
|
309
|
-
background: transparent;
|
|
310
|
-
border: none;
|
|
311
|
-
color: var(--ink-soft);
|
|
312
|
-
font-size: 13px;
|
|
313
|
-
text-transform: lowercase;
|
|
314
|
-
text-decoration: underline;
|
|
315
|
-
text-underline-offset: 4px;
|
|
316
|
-
cursor: pointer;
|
|
317
|
-
}
|
|
318
|
-
.bulk-toolbar__clear:hover { color: var(--ink); }
|
|
319
|
-
|
|
320
|
-
/* The shell's .app-content already ends in 108px of bottom air at desktop
|
|
321
|
-
width, which is what clears the bar — but at 768 it drops to 48px for the
|
|
322
|
-
phone, and there the bar is TALLER (its actions wrap to their own row). So
|
|
323
|
-
the phone width buys the room back, scoped to this page: goals.html loads
|
|
324
|
-
this sheet too and has no bulk toolbar to clear. */
|
|
325
|
-
@media (max-width: 768px) {
|
|
326
|
-
/* At the drawer breakpoint the sidebar leaves the flow, so the bar spans. */
|
|
327
|
-
.bulk-toolbar { left: 0; padding: 10px 14px; gap: 10px; }
|
|
328
|
-
.bulk-toolbar__actions { margin-left: 0; width: 100%; }
|
|
329
|
-
body[data-page="work"] .app-content { padding-bottom: 200px; }
|
|
330
|
-
}
|
|
331
|
-
|
|
332
163
|
/* ---- "Only mine" lens (the board's tab-wide filter; goal board's To-do bar
|
|
333
164
|
borrows the same class) --------------------------------------------------
|
|
334
165
|
Deliberately QUIETER than the filter bar below it: a checkbox in the label
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.989",
|
|
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.989",
|
|
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.989",
|
|
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.989'; // 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');
|
|
@@ -104,7 +104,7 @@ test('a prose link buys BOTH axes — padding for width, negative margin to give
|
|
|
104
104
|
// ---- 3. the world's own bans ------------------------------------------------
|
|
105
105
|
|
|
106
106
|
test('nothing on the board paints text white on the accent', () => {
|
|
107
|
-
const claim = rule(
|
|
107
|
+
const claim = rule(styleCss, '.task__claim');
|
|
108
108
|
assert.ok(claim, '.task__claim has a rule');
|
|
109
109
|
assert.match(claim, /background:\s*var\(--accent\)/, 'the claim button is the page\'s one accent fill');
|
|
110
110
|
assert.match(claim, /color:\s*var\(--accent-on\)/,
|
|
@@ -115,6 +115,13 @@ test('nothing on the board paints text white on the accent', () => {
|
|
|
115
115
|
'the literal this task removed must not come back');
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
+
test('the shared task + bulk-toolbar rules live in style.css (the shared base), not the page-local work.css', () => {
|
|
119
|
+
for (const sel of ['.task__age', '.task__age--stale', '.task__tag--newcomer', '.task__tag--mine', '.task__tag--rank', '.task__tag--rank-open', '.task__claim', '.task__touches', '.task', '.bulk-toolbar', '.bulk-toolbar__count', '.bulk-toolbar__actions', '.bulk-toolbar__btn', '.bulk-toolbar__clear']) {
|
|
120
|
+
assert.ok(rule(styleCss, sel), `style.css carries ${sel}, beside .task__tag`);
|
|
121
|
+
assert.equal(rule(workCss, sel), null, `work.css must not restate ${sel}`);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
118
125
|
test('work.css spends no compat alias — that was the sheet\'s removal list', () => {
|
|
119
126
|
const aliases = ['--surface', '--surface-2', '--border', '--border-strong', '--accent-weak', '--accent-text'];
|
|
120
127
|
const bare = workCss.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
@@ -124,7 +131,7 @@ test('work.css spends no compat alias — that was the sheet\'s removal list', (
|
|
|
124
131
|
});
|
|
125
132
|
|
|
126
133
|
test('the bulk toolbar carries no rgba literal and is opaque over the cards', () => {
|
|
127
|
-
const bar = rule(
|
|
134
|
+
const bar = rule(styleCss, '.bulk-toolbar');
|
|
128
135
|
assert.ok(bar, '.bulk-toolbar has a rule');
|
|
129
136
|
assert.doesNotMatch(bar, /rgba?\(/i,
|
|
130
137
|
'the three rgba(255,255,255,…) literals went with the inverted --ink surface that needed them');
|
|
@@ -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();
|