@bongos/core 1.21.3 → 1.21.5

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.
Files changed (73) hide show
  1. package/.bongos-core.json +128 -68
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +2 -0
  4. package/clients/bongos-client/index.cjs +2 -0
  5. package/clients/bongos-client/index.d.ts +2 -0
  6. package/clients/bongos-client/index.mjs +2 -0
  7. package/docs/adr/0362-the-governor-docket-is-collected-from-contributed-sources.md +34 -0
  8. package/docs/adr/README.md +1 -0
  9. package/docs/api/openapi.json +35 -1
  10. package/docs/api-reference.md +3 -2
  11. package/docs/copy-inventory.md +389 -518
  12. package/docs/copy-registry.json +880 -2143
  13. package/docs/module-api-changelog.md +4 -0
  14. package/docs/page-inventory.json +17 -25
  15. package/docs/page-readings.json +1008 -1246
  16. package/migrations/core_272_rename_cn_criterion_slugs.sql +40 -0
  17. package/modules/autonomy/governor-docket.js +101 -0
  18. package/modules/autonomy/routes/autonomy.js +5 -0
  19. package/modules/discord/board-broadcast.js +3 -1
  20. package/modules/discord/craft-broadcast.js +107 -0
  21. package/modules/discord/routes/discord.js +10 -0
  22. package/modules/discord/ship-broadcast.js +30 -2
  23. package/modules/economy/reward.js +3 -0
  24. package/modules/government/board.js +20 -5
  25. package/modules/government/docket-vocab.js +82 -0
  26. package/modules/government/docket.js +458 -0
  27. package/modules/government/routes/government.js +34 -0
  28. package/modules/hall-ui/public/genesis-home.js +9 -10
  29. package/modules/ideas/governor-docket.js +93 -0
  30. package/modules/ideas/routes/blockers.js +5 -0
  31. package/modules/lifecycle/criterion-ref.js +11 -0
  32. package/modules/lifecycle/db-tasks.js +7 -0
  33. package/modules/lifecycle/governor-docket.js +192 -0
  34. package/modules/lifecycle/routes/done-when.js +6 -0
  35. package/modules/lifecycle/routes/gate-approvals.js +78 -33
  36. package/modules/lifecycle/routes/lifecycle.js +3 -0
  37. package/modules/onboarding/routes/access-requests.js +68 -27
  38. package/modules/public-landing/public/assets/cosmos.css +6 -50
  39. package/modules/public-landing/public/projects.html +1170 -1659
  40. package/modules/public-landing/public/projects.probes.json +18 -18
  41. package/modules/public-landing/public/projects.states.json +23 -31
  42. package/modules/security/routes/reports.js +39 -0
  43. package/package-lock.json +2 -2
  44. package/package.json +1 -1
  45. package/release-notes.json +20 -0
  46. package/src/bongos/module-rechecks.js +77 -0
  47. package/src/bongos/route-rank-check.js +3 -0
  48. package/src/bongos/routes/core-update.js +41 -0
  49. package/src/bongos/routes/modules.js +17 -0
  50. package/src/module-api.js +1 -1
  51. package/tests/criterion_goal_attach.mjs +12 -4
  52. package/tests/criterion_ref_parse.mjs +41 -0
  53. package/tests/discord_craft_broadcast.mjs +162 -0
  54. package/tests/genesis_home.mjs +1 -1
  55. package/tests/government_routes.mjs +3 -1
  56. package/tests/governor_docket.mjs +552 -0
  57. package/tests/onboard.mjs +10 -4
  58. package/tests/platform_boot.mjs +1 -1
  59. package/tests/projects_hub.mjs +196 -89
  60. package/tests/projects_hub_app_status.mjs +3 -1
  61. package/tests/projects_hub_app_step.mjs +72 -48
  62. package/tests/projects_hub_dns_ready.mjs +10 -2
  63. package/tests/projects_hub_look.mjs +32 -22
  64. package/tests/projects_hub_module_picker.mjs +112 -62
  65. package/tests/projects_hub_pre_uat.mjs +48 -41
  66. package/tests/public_landing_projects.mjs +12 -6
  67. package/tests/wizard_demo.mjs +37 -11
  68. package/tests/wizard_draft_resume.mjs +34 -22
  69. package/tests/wizard_front_door.mjs +173 -103
  70. package/tests/wizard_intent_resume.mjs +28 -25
  71. package/tests/wizard_physics.mjs +94 -76
  72. package/tests/wizard_physics_more.mjs +409 -277
  73. package/tests/wizard_six_screens.mjs +126 -0
@@ -283,9 +283,14 @@ test('/create is absorbed — the wizard lives here and the route only redirects
283
283
  const server = fs.readFileSync(path.join(ROOT, 'src', 'platform-server.js'), 'utf8');
284
284
  assert.match(server, /res\.redirect\(302, '\/projects\?view=new'/,
285
285
  '/create must 302 into the hub (302, not 301: the URL is an OAuth return target in the wild)');
286
- for (const id of ['panel-gate', 'panel-name', 'panel-type', 'panel-home', 'panel-address', 'panel-review', 'panel-done']) {
286
+ // the six blended screens (task 1004504): every one is real markup, and the steps they
287
+ // folded in are gone rather than left behind as dead panels
288
+ for (const id of ['panel-gate', 'panel-fork', 'panel-what', 'panel-checks', 'panel-review', 'panel-look', 'panel-done']) {
287
289
  assert.match(HUB, new RegExp(`id="${id}"`), `the wizard's ${id} ships as real markup (C7)`);
288
290
  }
291
+ for (const id of ['panel-terms', 'panel-physics', 'panel-name', 'panel-type', 'panel-detail', 'panel-modules', 'panel-home', 'panel-address', 'panel-app']) {
292
+ assert.doesNotMatch(HUB, new RegExp(`id="${id}"`), `the retired ${id} is gone (task 1004504)`);
293
+ }
289
294
  });
290
295
 
291
296
  test('the wizard step indices stay coupled — data-next lands one past its own panel (task 1002772)', () => {
@@ -308,10 +313,10 @@ test('the wizard step indices stay coupled — data-next lands one past its own
308
313
  for (const m of HUB.matchAll(/data-back="(\d+)"/g)) {
309
314
  assert.ok(map[Number(m[1])], `data-back="${m[1]}" must point at a mapped panel`);
310
315
  }
311
- // the rail numbers every step except the gate and the done panel
316
+ // the numbered rail is gone (task 1004504): no beat is left to drift from the map
312
317
  const beats = [...HUB.matchAll(/data-step="(\d+)"/g)].map((x) => Number(x[1]));
313
- assert.equal(Math.max.apply(null, beats), Object.keys(map).length - 2,
314
- 'the rail shows every numbered step except gate and done');
318
+ assert.deepEqual(beats, [], 'no rail beat is left on the page');
319
+ assert.doesNotMatch(HUB, /class="steps"/, 'no <ol class="steps"> rail in the wizard');
315
320
  });
316
321
 
317
322
  test('the absorbed wizard sheds its two URL landmines — /gate/ and the whole-query strip', () => {
@@ -657,40 +662,59 @@ test('every per-step hook in show() still points at the panel it belongs to (ren
657
662
  const STEP = {};
658
663
  for (const m of map.matchAll(/(\d+):\s*'([a-z-]+)'/g)) STEP[m[2]] = Number(m[1]);
659
664
  const show = SCRIPT.slice(SCRIPT.indexOf('function show(step, moveFocus)'));
660
- const body = show.slice(0, show.search(/\n\s*\/\* One delegated pair on the rail/));
661
-
662
- assert.ok(body.includes(`if (step === ${STEP['panel-home']}) { syncHome();`),
663
- 'the repo probe fires on the Home panel');
664
- assert.ok(body.includes(`if (step === ${STEP['panel-address']}) { checkSlugAvailable(); renderAddrPanel(); }`),
665
- 'the handle re-check fires on the Address panel');
666
- assert.ok(body.includes(`if (step === ${STEP['panel-review']}) renderReview();`),
667
- 'the review is rendered on the Review panel');
668
- assert.ok(body.includes(`'wizDone', step === ${STEP['panel-done']}`),
669
- 'the done panel is the one that takes the whole card');
670
- // and the About panel claims no hook of its own — it has nothing to fetch
671
- assert.ok(!body.includes(`if (step === ${STEP['panel-detail']})`),
672
- 'the optional step needs no side effect; adding one would give it a way to fail');
673
- });
674
-
675
- test('the About step is OFFERED by every form and can be walked past — the skip is a real control (task 1002334)', () => {
676
- const about = (HUB.match(/<section class="panel" id="panel-detail"[\s\S]*?<\/section>/) || [''])[0];
677
- assert.ok(about, '#panel-detail ships as real markup');
678
- assert.match(about, /id="f-desc"/, 'it asks what the owner is building — the publish gate half');
679
- assert.match(about, /id="teamCards"/, 'and who is building it — what the recommendations read');
680
- // the skip must be its OWN button: "leave it blank and press Next" is an
681
- // escape hatch the visitor has to infer, which is not an optional step
682
- assert.match(about, /id="skipDetail"/, 'the skip is a control, not an inference');
683
- assert.match(about, /Optional/, 'and the panel says so before it asks');
684
- // the consequence of skipping is stated where the choice is made
685
- assert.match(SCRIPT, /stays off the public map until it has one/,
686
- 'skipping names its cost (ADR 0182 D4), never leaves it to be discovered later');
687
- // nothing in validateStep may gate the About step
665
+ const endAt = show.search(/\n\s*\$\('wizBack'\)\.addEventListener\('click'/);
666
+ assert.ok(endAt > 0, 'show() still ends where the one Back button is wired — re-point this slice if it moved');
667
+ const body = show.slice(0, endAt);
668
+
669
+ // task 1004504: the old Home / Address / App steps live as rows on the review, so their
670
+ // entry hooks moved there — one block, run on every entry to the review
671
+ const review = (body.match(new RegExp(`if \\(step === ${STEP['panel-review']}\\) \\{([\\s\\S]*?)\\n \\}`)) || [, ''])[1];
672
+ assert.ok(review, 'the review screen has its own entry block');
673
+ assert.match(review, /syncHome\(\);/, 'the code row repaints its repo controls on the review');
674
+ assert.match(review, /if \(authKnown\) ensureRepoAccess\(\);/, 'the repo probe fires on the review, and never before /me settles');
675
+ assert.match(review, /checkSlugAvailable\(\); renderAddrPanel\(\);/, 'the handle re-check fires on the review (N10)');
676
+ assert.match(review, /syncAppStep\(\); syncRenderSetup\(\);/, 'the app row repaints its choice');
677
+ assert.match(review, /renderModules\(\);/, 'the extras row paints the picker');
678
+ assert.match(review, /paintSetup\(\);/, 'and the rows are painted on the review');
679
+ assert.ok(body.includes(`if (step === ${STEP['panel-fork']}) paintDoors();`),
680
+ 'the fork repaints its two doors against the tick');
681
+ assert.ok(body.includes(`if (step === ${STEP['panel-checks']}) { physLoadAccountOnce(); paintChecks(); }`),
682
+ 'the quick checks paint on the checks screen');
683
+ assert.ok(body.includes(`if (step === ${STEP['panel-look']}) { renderLook(); renderRecap(); }`),
684
+ 'the look and the recap paint on the look screen, the one Create sits on');
685
+ // the room behind the wizard follows the screen, through one map keyed like panels
686
+ assert.match(body, /\$\('wizRoom'\)\.setAttribute\('data-screen', SCREEN\[step\]\);/, 'show() writes the room\'s data-screen');
687
+ const screens = (SCRIPT.match(/var SCREEN = \{([^}]*)\}/) || [, ''])[1];
688
+ assert.deepEqual([...screens.matchAll(/(\d+):/g)].map((m) => Number(m[1])), Object.values(STEP).sort((a, b) => a - b),
689
+ 'SCREEN names every panel the map holds, and nothing else');
690
+ // and the done panel claims no hook of its own — renderDone runs from the create
691
+ assert.ok(!body.includes(`if (step === ${STEP['panel-done']})`),
692
+ 'the done panel is painted by the create, not by a show() hook');
693
+ });
694
+
695
+ test('the About answers are OFFERED by every form and can be walked past (task 1002334; folded into the screens by task 1004504)', () => {
696
+ // the About step is gone (task 1004504): its sentence is the optional field on "What are
697
+ // you making?", and its team shape is a choice on the review's Team size row
698
+ assert.doesNotMatch(HUB, /id="panel-detail"|id="skipDetail"|id="detailSkipNote"/, 'no About panel and no skip button are left');
699
+ const what = (HUB.match(/<section class="panel" id="panel-what"[\s\S]*?<\/section>/) || [''])[0];
700
+ assert.ok(what, '#panel-what ships as real markup');
701
+ assert.match(what, /<input type="text" id="f-desc"/, 'it asks what the owner is building — the publish gate half');
702
+ // optional, and the consequence of filling it in is stated on the field itself
703
+ assert.match(what, /<span>In a sentence <small>\(optional — puts it on the public map\)<\/small><\/span>/,
704
+ 'the field says it is optional and names what it is for (ADR 0182 D4), before it asks');
705
+ const team = (HUB.match(/<div class="setRow" data-row="team">[\s\S]*?<span class="acts">/) || [''])[0];
706
+ assert.match(team, /id="teamCards"/, 'and who is building it — what the recommendations read — on the Team size row');
707
+ // nothing in validateStep may gate the sentence: the screen asks only for the name and the kind
688
708
  const validate = SCRIPT.slice(SCRIPT.indexOf('function validateStep('));
689
709
  const map = SCRIPT.match(/var panels = \{([^}]*)\}/)[1];
690
- const aboutStep = Number((map.match(/(\d+):\s*'panel-detail'/) || [, 0])[1]);
691
- assert.ok(aboutStep > 0, 'the About panel is in the map');
692
- assert.ok(!validate.slice(0, validate.indexOf('function ', 1)).includes(`if (step === ${aboutStep}) {`),
693
- 'the optional step has no validation clause — Next past an empty answer is the point');
710
+ const whatStep = Number((map.match(/(\d+):\s*'panel-what'/) || [, 0])[1]);
711
+ assert.ok(whatStep > 0, 'the What panel is in the map');
712
+ const fn = validate.slice(0, validate.indexOf('\n function ', 1));
713
+ const clause = (fn.match(new RegExp(`if \\(step === ${whatStep}\\) \\{([\\s\\S]*?)\\n \\}`)) || [, ''])[1];
714
+ assert.ok(clause, 'the What screen has a validation clause (name + kind)');
715
+ assert.match(clause, /state\.name\.trim\(\)/, 'the name is required');
716
+ assert.match(clause, /state\.projType/, 'the kind is required');
717
+ assert.doesNotMatch(clause, /description|teamShape/, 'the optional answers have no validation — Continue past an empty one is the point');
694
718
  });
695
719
 
696
720
  test('the About answers travel on the create request, and an empty one is OMITTED not blanked', () => {
@@ -701,11 +725,12 @@ test('the About answers travel on the create request, and an empty one is OMITTE
701
725
  assert.match(SCRIPT, /detail: createDetail\(\),/);
702
726
  assert.match(SCRIPT, /if \(state\.teamShape\) d\.team_shape = state\.teamShape;\r?\n\s*return Object\.keys\(d\)\.length \? d : undefined;/,
703
727
  'and an unanswered team shape is never guessed into the request');
704
- // the review is the last screen before Create, so the skip's cost is restated there
705
- const review = (SCRIPT.match(/function renderReview\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
706
- assert.match(review, /off the public map until you add it/,
707
- 'the review row states the consequence rather than a bare dash');
708
- assert.match(review, /\['About',/, 'and the About answer has a row of its own');
728
+ // task 1004504: the ledger review is gone; the review now confirms only the answers the
729
+ // wizard PREFILLED, and the sentence is never prefilled, so it has no row there — its
730
+ // cost is stated on its own field (pinned above)
731
+ assert.doesNotMatch(SCRIPT, /function renderReview\(|\$\('reviewList'\)/, 'no ledger review is left');
732
+ assert.match(SCRIPT, /var ROWS = \['code', 'address', 'app', 'extras', 'team', 'rewards', 'run'\];/,
733
+ 'the review rows are the seven prefilled answers');
709
734
  });
710
735
 
711
736
  test('a renumbering of the wizard invalidates old drafts — the key carries the version', () => {
@@ -720,8 +745,9 @@ test('a renumbering of the wizard invalidates old drafts — the key carries the
720
745
  // task 1004417 put the front door's two screens (terms, fork) in front: the sixth renumbering
721
746
  // task 1004421 put the Look step between App and Review: the seventh
722
747
  // task 1004418 put planet physics right after the fork: the eighth
723
- assert.equal(count, 14, 'gate + the front door\'s two + ten numbered steps + done — bump DRAFT_KEY when this changes');
724
- assert.equal(key, 'cb-create-draft-v8', 'and the key moved with the physics step');
748
+ // task 1004504 blended the thirteen into six screens: the ninth
749
+ assert.equal(count, 7, 'gate + the six blended screens (fork, what, checks, review, look, done) — bump DRAFT_KEY when this changes');
750
+ assert.equal(key, 'cb-create-draft-v9', 'and the key moved with the six screens');
725
751
  });
726
752
 
727
753
  test('every row lays the same five parts — the ledger has no ragged row', () => {
@@ -2135,23 +2161,28 @@ test("its card is the planet section's twin: its own heading, the same channel,
2135
2161
  });
2136
2162
 
2137
2163
  /* ── the create wizard in the world (task 1003346; direction record §15, the pick: v1 the
2138
- rail above, the bongo pair kept). Two sheets carry it: cosmos.css draws the FRAME by scope
2139
- (#view-new), projects.html's own wizard block draws the wizard's structures. ── */
2140
- test('the wizard takes the world by scope: no card ground, the six-step strip as the rail, the doors as pills (task 1003346)', () => {
2164
+ rail above, the bongo pair kept; task 1004504 retired the rail and the pair for the night
2165
+ room). Two sheets carry it: cosmos.css draws the FRAME by scope (#view-new), projects.html's
2166
+ own wizard block draws the wizard's structures and the room behind them. ── */
2167
+ test('the wizard takes the world by scope: no card ground, no progress rail, the doors as pills (task 1003346; rail retired by task 1004504)', () => {
2141
2168
  const at = COSMOS.indexOf('THE CREATE WIZARD, IN THE WORLD');
2142
2169
  assert.ok(at > -1, 'the #view-new block exists in cosmos.css');
2143
2170
  const block = COSMOS.slice(at);
2144
2171
  assert.match(block, /#view-new \.card\{background:none;border-radius:0;box-shadow:none;/, 'no card ground');
2145
- // eight tracks since task 1004351 added the App step (seven since 1002339); the count is the
2146
- // rail's own, so it moves with the panels map rather than being a magic number
2147
- assert.match(block, /#view-new \.steps\{display:grid;grid-template-columns:repeat\(10,1fr\);/, 'the strip lays a track per numbered step');
2148
- assert.match(block, /#view-new \.steps li::before\{content:"";position:absolute;top:-4px;left:0;right:auto;width:7px;height:7px;/, 'the connector becomes the 7px dot');
2149
- assert.match(block, /#view-new \.steps li\.now::before\{background:var\(--accent\);border-color:var\(--accent\);outline:4px solid/, 'the lit step carries the accent and an outline halo');
2150
- assert.match(block, /#view-new \.panel h1\{font-weight:300;[^}]*text-transform:lowercase;/, 'the step title in the display voice, lowercase');
2151
- assert.match(block, /#view-new \.btn\{[^}]*text-transform:lowercase;[^}]*box-shadow:none;/, 'the doors are pills without a shadow');
2172
+ // the strip that used to be the rail is gone with the rail (task 1004504), and so is the
2173
+ // base .steps vocabulary it scoped over
2174
+ assert.doesNotMatch(block, /#view-new \.steps/, 'no #view-new .steps rule is left in the wizard block');
2175
+ assert.doesNotMatch(COSMOS, /(^|[\s,}])\.steps[\s{.:\[]/m, 'no .steps rule is left anywhere in cosmos.css');
2176
+ // task 1004504: the design of record's light display weight, in sentence case
2177
+ assert.match(block, /#view-new \.panel h1\{font-weight:200;[^}]*text-transform:none;/, 'the step title in the display voice, light and in sentence case');
2178
+ assert.match(block, /#view-new \.btn\{[^}]*text-transform:none;[^}]*box-shadow:none;/, 'the doors are pills without a shadow, in sentence case');
2152
2179
  assert.match(block, /#view-new \.btn\.ghost\{[^}]*border:1px solid var\(--rule\);/, 'back and skip are hairline pills');
2153
2180
  assert.match(block, /#view-new #wizSignin\{background:var\(--ink\);color:var\(--bg\);/, 'the GitHub sign-in is the inverted pill');
2154
- assert.match(block, /#view-new \.bongos \.glow\{display:none;\}/, 'the bongo pair stays (the owner\'s one mark on every surface), its halo goes');
2181
+ // the bongo pair left the wizard with the night room (task 1004504): no rule and no markup
2182
+ assert.doesNotMatch(block, /\.bongos\b/, 'no bongo-pair rule is left in the wizard block');
2183
+ assert.doesNotMatch(STYLE, /\.bongos\b/, 'nor in the page\'s own sheet');
2184
+ const wizMarkup = HUB.slice(HUB.indexOf('<section id="view-new"'), HUB.indexOf('<footer class="foot"'));
2185
+ assert.doesNotMatch(wizMarkup, /class="bongos/, 'and the wizard carries no bongo pair');
2155
2186
  assert.match(block, /#view-new \.panel \.lede a\{display:inline-block;padding:3px 0;min-height:24px;/, 'the done clause\'s link clears the 24px floor (the sheet\'s first problem)');
2156
2187
  assert.doesNotMatch(block, /box-shadow:(?!none)/, 'halos are outlines in the world, never box-shadow');
2157
2188
  assert.doesNotMatch(block, /#[0-9a-f]{3,8}\b/i, 'every colour is a token');
@@ -2161,13 +2192,16 @@ test('the wizard takes the world by scope: no card ground, the six-step strip as
2161
2192
  at the sheet's 900 while the wizard's fields, choice bands and review ledger folded at the
2162
2193
  page's 840 (cosmos's createSplit collapse), so between the two the rail was already three-up
2163
2194
  beside full-width bands. Both sheets now name the same width. ── */
2164
- test('the wizard folds at one width: the rail\'s media rule sits at 840px with the bands, and no 900px rule is left in the #view-new block (task 1003538)', () => {
2195
+ test('the wizard folds at one width: no rail fold is left, and no 900px rule is left in the #view-new block (task 1003538; rail retired by task 1004504)', () => {
2165
2196
  const at = COSMOS.indexOf('THE CREATE WIZARD, IN THE WORLD');
2166
2197
  assert.ok(at > -1, 'the #view-new block exists in cosmos.css');
2167
2198
  const block = COSMOS.slice(at);
2168
- assert.match(block, /@media \(max-width:840px\)\{\s*#view-new \.steps\{grid-template-columns:repeat\(3,1fr\);/, 'the rail folds to three columns at 840, the width the bands fold at');
2199
+ assert.doesNotMatch(block, /#view-new \.steps/, 'the rail and its 840 fold are gone (task 1004504)');
2169
2200
  assert.doesNotMatch(block, /@media \(max-width:900px\)/, 'no 900px rule is left in the wizard block');
2170
- assert.match(STYLE, /@media \(max-width:840px\)\{[\s\S]*?\.review\{grid-template-columns:1fr;/, 'the page\'s own wizard block still folds its bands and ledger at that same 840');
2201
+ assert.match(STYLE, /@media \(max-width:840px\)\{\s*\.row2,\.choices\{grid-template-columns:1fr;\}[\s\S]*?\.typeCards\{grid-template-columns:1fr;\}/, 'the page\'s own wizard block still folds its bands at that same 840');
2202
+ // the ledger review is gone (task 1004504); its rows stack at their own, narrower width
2203
+ assert.doesNotMatch(STYLE, /\.review\{/, 'no .review ledger rule is left');
2204
+ assert.match(STYLE, /@media \(max-width:560px\)\{\s*\.setRow\{grid-template-columns:minmax\(0,1fr\);/, 'the review\'s rows stack under 560');
2171
2205
  });
2172
2206
 
2173
2207
  test('the wizard\'s own structures wear the world: hairline fields, cells of one band, the review as a ledger, the human steps as a numbered ledger, the standup strip as the rail (task 1003346)', () => {
@@ -2180,8 +2214,10 @@ test('the wizard\'s own structures wear the world: hairline fields, cells of one
2180
2214
  assert.match(block, /\.tpill\[aria-pressed=true\]::before\{background:var\(--accent\);/, 'the chosen cell carries the dot');
2181
2215
  assert.match(block, /\.choices\{display:grid;grid-template-columns:1fr 1fr;gap:0;border-top:1px solid var\(--rule\);/, 'start fresh / adopt are two cells of one band');
2182
2216
  assert.match(block, /\.choice svg\{display:none;\}/, 'the dot is the mark, the icon leaves');
2183
- assert.match(block, /\.review\{list-style:none;display:grid;grid-template-columns:172px 1fr;/, 'the review is a ledger');
2184
- assert.match(block, /\.review li\{display:contents;\}/, 'renderReview\'s li markup stands; the grid lays its k and v');
2217
+ // task 1004504: the review is one hairline column of rows to confirm, not a ledger of li
2218
+ assert.match(block, /\.setRows\{margin:24px 0 0;border-top:1px solid var\(--rule\);\}/, 'the review\'s rows hang from one hairline');
2219
+ assert.match(block, /\.setRow\{display:grid;grid-template-columns:120px minmax\(0,1fr\) auto;[^}]*border-bottom:1px solid var\(--rule\);\}/, 'each row is label · value · actions on its own hairline');
2220
+ assert.doesNotMatch(block, /\.review( li)?\{/, 'the ledger\'s rules are gone with renderReview');
2185
2221
  assert.match(block, /\.humanSteps>li\{\s*display:grid;grid-template-columns:44px 1fr;/, 'the human steps are a numbered ledger');
2186
2222
  assert.match(block, /\.humanSteps>li>\*\{grid-column:2;/, 'every child of a step sits in the text column, never under the counter (the sheet\'s bug)');
2187
2223
  assert.match(block, /\.humanSteps>li\.sdone::before\{content:"done";/, 'a finished step reads DONE in the ok ink');
@@ -2190,8 +2226,15 @@ test('the wizard\'s own structures wear the world: hairline fields, cells of one
2190
2226
  assert.match(block, /\.doneHead \.tick\{display:none;\}/, 'the tick leaves; the rail says filed');
2191
2227
  assert.match(block, /\.addrBig code\{display:block;font-family:var\(--font-display\);/, 'the address stands in the display voice');
2192
2228
  assert.doesNotMatch(block, /linear-gradient/, 'no tinted plates');
2193
- assert.doesNotMatch(block, /box-shadow:(?!none)/, 'no box shadow in the wizard block');
2194
- assert.match(HUB, /<div class="nav">\s*<button type="button" class="btn ghost" data-back="11">← Back<\/button>\s*<button type="button" class="btn big" id="doCreate">Create my project<\/button>/, 'the review\'s doors sit on one row, back then the primary');
2229
+ // the one exception is drawn, not chrome: the night room's moon glows in the window
2230
+ // (task 1004504), and a control's halo is still an outline
2231
+ const shadows = [...block.matchAll(/([^{}]*)\{[^}]*box-shadow:(?!none)/g)].map((m) => m[1].trim());
2232
+ assert.deepEqual(shadows, ['#view-new .rmMoon'], 'no box shadow in the wizard block but the room\'s moon');
2233
+ // no per-panel Back any more (task 1004504): one Back in the wizard's top row, and Create
2234
+ // stands alone on the look screen
2235
+ assert.doesNotMatch(HUB, /data-back="/, 'no per-panel back door is left');
2236
+ assert.match(HUB, /<div class="wizTop">\s*<span class="wizEyebrow" id="wizEyebrow">New project<\/span>\s*<button type="button" class="wizBack" id="wizBack" hidden>← Back<\/button>\s*<\/div>/, 'one Back, beside the eyebrow');
2237
+ assert.match(HUB, /<div class="nav">\s*<span><\/span>\s*<button type="button" class="btn big" id="doCreate">Create my project<\/button>/, 'the look screen\'s one door is Create');
2195
2238
  assert.match(HUB, /<h2 class="stepsHead" id="doneStepsHead">Your steps — the parts only you can play<\/h2>/, 'the steps heading is the label face by class, not an inline style (the id is where a skipped invite step hands focus on, task 1002346)');
2196
2239
  // what is enqueued is unchanged (ADR 0111): the filing body the control plane reads
2197
2240
  assert.match(SCRIPT, /body: JSON\.stringify\(\{\s*slug: state\.slug,/, 'the filing still carries the slug first');
@@ -2200,32 +2243,94 @@ test('the wizard\'s own structures wear the world: hairline fields, cells of one
2200
2243
  assert.match(SCRIPT, /onboard_mode: state\.mode === 'adopt' \? 'adopt' : 'greenfield'/, 'the onboard mode is unchanged');
2201
2244
  // the kit walks every step and files against the stub
2202
2245
  const states = JSON.parse(fs.readFileSync(path.join(LANDING, 'projects.states.json'), 'utf8')).states;
2203
- for (const s of ['type', 'about', 'modules', 'home', 'address', 'review', 'done']) assert.ok(states['in-new-' + s], `the ${s} step has a kit state`);
2246
+ for (const s of ['fork-ticked', 'what', 'checks', 'review', 'review-extras', 'look', 'done']) assert.ok(states['in-new-' + s], `the ${s} screen has a kit state`);
2204
2247
  const probes = JSON.parse(fs.readFileSync(path.join(LANDING, 'projects.probes.json'), 'utf8')).probes;
2205
- assert.ok(probes.some((p) => /the wizard driver \(task 1003346\)/.test(p.name) && p.steps.some((st) => st[1] === '#doCreate')), 'the wizard driver files a project end to end');
2206
- });
2207
-
2208
- test('the Modules step ships as real markup and is skippable like About (task 1002339)', () => {
2209
- const panel = (HUB.match(/<section class="panel" id="panel-modules"[\s\S]*?<\/section>/) || [''])[0];
2210
- assert.ok(panel, '#panel-modules ships as real markup, not built by script');
2211
- assert.match(panel, /id="modCards"/, 'the band the picker paints its rows into');
2212
- assert.match(panel, /class="typeCards"/, 'and it is the SAME band the type and team questions use — no new grammar');
2213
- assert.match(panel, /id="modCoreNote"/, 'the always-on core is stated on the panel');
2214
- // the skip must be its own control, not "leave it and press Next" — the rule
2215
- // task 1002334 wrote for the one other optional step
2216
- assert.match(panel, /id="skipModules"/, 'the skip is a control, not an inference');
2217
- assert.match(panel, /Optional/, 'and the panel says so before it asks');
2218
- // nothing in validateStep may gate it
2219
- const validate = SCRIPT.slice(SCRIPT.indexOf('function validateStep('));
2220
- const map = SCRIPT.match(/var panels = \{([^}]*)\}/)[1];
2221
- const modStep = Number((map.match(/(\d+):\s*'panel-modules'/) || [, 0])[1]);
2222
- assert.ok(modStep > 0, 'the Modules panel is in the map');
2223
- assert.ok(!validate.slice(0, validate.indexOf('function ', 1)).includes(`if (step === ${modStep}) {`),
2224
- 'the optional step has no validation clause — Next past an untouched picker is the point');
2248
+ assert.ok(probes.some((p) => /the wizard driver \(tasks? 1003346\b/.test(p.name) && p.steps.some((st) => st[1] === '#doCreate')), 'the wizard driver files a project end to end');
2249
+ // a walk that clicks a panel the page no longer has stops dead at that click: every panel a
2250
+ // state or probe names must be one of the screens (task 1004504 retired seven of them)
2251
+ const live = new Set([...HUB.matchAll(/<section class="panel(?: on)?" id="(panel-[a-z]+)"/g)].map((m) => m[1]));
2252
+ const walks = [...Object.entries(states).map(([n, s]) => [n, s.actions || s.steps || []]), ...probes.map((p) => [p.name, p.steps || []])];
2253
+ for (const [name, steps] of walks) {
2254
+ for (const st of steps) {
2255
+ for (const m of String(st[1] || '').matchAll(/#(panel-[a-z]+)/g)) {
2256
+ assert.ok(live.has(m[1]), `"${name}" walks #${m[1]}, which the page no longer has`);
2257
+ }
2258
+ }
2259
+ }
2260
+ });
2261
+
2262
+ /* ── the night room (task 1004504; the design of record docs/design/mocks/combined-onboarding/).
2263
+ The studio's lofi room behind the whole wizard, in place of the plate, the planet and the
2264
+ bongo pair. Decoration only, re-lit by the pack, and still under the kill switch. ── */
2265
+ test('the night room is decoration, lit only by the fifteen, and moves only when motion is welcome (task 1004504)', () => {
2266
+ // real markup, hidden from assistive tech, carrying the screen show() writes
2267
+ const tag = (HUB.match(/<div class="wizRoom" id="wizRoom"[^>]*>/) || [''])[0];
2268
+ assert.ok(tag, '#wizRoom ships as real markup inside the new view');
2269
+ assert.ok(HUB.indexOf('id="wizRoom"') > HUB.indexOf('<section id="view-new"'), 'and it lives inside #view-new');
2270
+ assert.match(tag, /aria-hidden="true"/, 'the room is decoration: aria-hidden');
2271
+ assert.match(tag, /data-screen="[a-z]+"/, 'it carries a data-screen for show() to move');
2272
+ // every colour of its own is a mix of the pack's tokens, never a literal: a re-branded
2273
+ // pack re-lights the room (C1/C2), and the tokens are scoped to the room, never :root
2274
+ const rule = (STYLE.match(/#view-new \.wizRoom\{([^}]*)\}/) || [, ''])[1];
2275
+ assert.ok(rule, 'the room\'s own rule exists in the page sheet');
2276
+ const tokens = [...rule.matchAll(/(--rm-[a-z0-9-]+):([^;]+);/g)];
2277
+ assert.ok(tokens.length >= 10, `the room declares its colours as --rm-* tokens, found ${tokens.length}`);
2278
+ for (const [, name, value] of tokens) {
2279
+ assert.ok(/^color-mix\(/.test(value.trim()) || /^rgba\(255,255,255,/.test(value.trim()),
2280
+ `${name} must be a color-mix( of the fifteen or a white wash, not ${value.trim()}`);
2281
+ assert.doesNotMatch(value.trim(), /^#/, `${name} has no hex literal of its own`);
2282
+ }
2283
+ // the room's other rules read those tokens (or a pack token), never a literal colour
2284
+ const roomRules = [...STYLE.matchAll(/(#view-new \.(?:rm[A-Za-z0-9]+|wizRoom\[[^\]]*\])[^{]*)\{([^}]*)\}/g)];
2285
+ assert.ok(roomRules.length > 10, 'the room\'s parts are styled in the page sheet');
2286
+ for (const [, sel, body] of roomRules) {
2287
+ assert.doesNotMatch(body.replace(/url\("data:[^"]*"\)/g, ''), /(?:background|color|border[a-z-]*|box-shadow|filter):[^;]*#[0-9a-f]{3,8}\b/i, `${sel.trim()} paints no literal colour`);
2288
+ }
2289
+ // the kill switch: the twinkle, the breathe, the float and the seed's growth all sit
2290
+ // inside the no-preference block, and nothing in the room moves outside it
2291
+ const mqAt = STYLE.indexOf('@media (prefers-reduced-motion:no-preference){');
2292
+ assert.ok(mqAt > -1, 'the room\'s motion has a no-preference block');
2293
+ let depth = 0, mqEnd = -1;
2294
+ for (let i = STYLE.indexOf('{', mqAt); i < STYLE.length; i++) {
2295
+ if (STYLE[i] === '{') depth++;
2296
+ else if (STYLE[i] === '}' && --depth === 0) { mqEnd = i; break; }
2297
+ }
2298
+ const motion = STYLE.slice(mqAt, mqEnd);
2299
+ for (const want of [/\.rmStar\{animation:rmTwinkle /, /\.rmGlow\{animation:rmBreathe /, /\.rmSeed img\{animation:rmFloat /, /@keyframes rmTwinkle\{/, /@keyframes rmBreathe\{/, /@keyframes rmFloat\{/]) {
2300
+ assert.match(motion, want, `${want} lives inside the no-preference block`);
2301
+ }
2302
+ const outside = STYLE.slice(0, mqAt) + STYLE.slice(mqEnd);
2303
+ assert.doesNotMatch(outside, /@keyframes rm|animation:rm/, 'no room animation outside the kill switch');
2304
+ for (const [, sel, body] of [...outside.matchAll(/(#view-new \.(?:rm[A-Za-z0-9]+|wizRoom)[^{]*)\{([^}]*)\}/g)]) {
2305
+ assert.doesNotMatch(body, /\b(?:animation|transition)\s*:/, `${sel.trim()} moves only inside the no-preference block`);
2306
+ }
2307
+ });
2308
+
2309
+ // the Modules step became the review's Extras row (task 1004504): the picker is the row's
2310
+ // own controls, moved in whole, so these pin the row as they pinned the panel
2311
+ const EXTRAS_ROW = (HUB.match(/<div class="setRow" data-row="extras">[\s\S]*?<span class="acts">/) || [''])[0];
2312
+
2313
+ test('the extras picker ships as real markup on the review\'s Extras row and is a default you confirm, never a gate (tasks 1002339, 1004504)', () => {
2314
+ assert.doesNotMatch(HUB, /id="panel-modules"|id="skipModules"/, 'no Modules panel and no skip button are left');
2315
+ assert.ok(EXTRAS_ROW, 'the Extras row ships as real markup, not built by script');
2316
+ assert.match(EXTRAS_ROW, /id="modCards"/, 'the band the picker paints its rows into');
2317
+ assert.match(EXTRAS_ROW, /class="typeCards"/, 'and it is the SAME band the type and team questions use — no new grammar');
2318
+ assert.match(EXTRAS_ROW, /id="modCoreNote"/, 'the always-on core is stated on the row');
2319
+ // the suggested set is accepted by its own control — "Looks right" — never inferred
2320
+ assert.match(HUB, /<div class="setRow" data-row="extras">[\s\S]*?<button type="button" class="rowOk" data-row-ok>Looks right<\/button><button type="button" class="rowChg" data-row-chg aria-expanded="false">Change<\/button>/,
2321
+ 'accepting the suggested set is a control, not an inference');
2322
+ // confirming turns the shown set into an answer given; the row's validity never refuses it
2323
+ const confirm = (SCRIPT.match(/function confirmRow\(key\) \{([\s\S]*?)\n \}/) || [, ''])[1];
2324
+ assert.match(confirm, /if \(key === 'extras' && starterBundles\) state\.modules = pickedModules\(\);/,'confirming the row is the answer — once the catalog has landed, never an empty list in its place');
2325
+ const valid = (SCRIPT.match(/function rowValid\(key\) \{([\s\S]*?)\n \}/) || [, ''])[1];
2326
+ assert.ok(valid, 'rowValid() must exist');
2327
+ assert.doesNotMatch(valid, /'extras'/, 'the extras row has no check that can refuse it — any set, even none, is an answer');
2328
+ // and the kind is what the suggestion follows, so changing it asks again
2329
+ assert.match(SCRIPT, /\/\* the suggested extras follow the kind, so the review asks about them again \*\/\s*unconfirm\(\['extras'\]\);/);
2225
2330
  });
2226
2331
 
2227
2332
  test('the picker\'s search ships as real markup, in the wizard\'s own field grammar (task 1002340)', () => {
2228
- const panel = (HUB.match(/<section class="panel" id="panel-modules"[\s\S]*?<\/section>/) || [''])[0];
2333
+ const panel = EXTRAS_ROW;
2229
2334
  assert.match(panel, /<input type="text" id="modQ"[^>]*>/, 'the search is a real field, not one built by script');
2230
2335
  assert.match(panel, /id="modQ"[^>]*aria-label="Search the extras by name"/, 'and it is named for a screen reader');
2231
2336
  assert.match(panel, /id="modFilterNote"[^>]*aria-live="polite"/, 'the count under it is announced when it changes');
@@ -2272,9 +2377,11 @@ test('the picker\'s answer rides the create request, and an untouched picker sen
2272
2377
  test('the review names what the project will actually come with (task 1002339)', () => {
2273
2378
  // The picker is optional, so the last screen before Create must show the set
2274
2379
  // the owner is about to get — their picks, or the preselect they walked past.
2275
- const render = SCRIPT.slice(SCRIPT.indexOf('function renderReview()'));
2276
- const rows = render.slice(0, render.indexOf('$(\'reviewList\').innerHTML'));
2277
- assert.match(rows, /\['Extras', extrasRow\(\)\]/, 'the ledger carries an Extras row');
2380
+ // task 1004504: the review is the row-by-row confirm, and its Extras row reads the picker
2381
+ const text = SCRIPT.slice(SCRIPT.indexOf('function rowText(key)'));
2382
+ const extras = text.slice(text.indexOf("if (key === 'extras')"), text.indexOf("if (key === 'team')"));
2383
+ assert.ok(text.length && extras, 'rowText() and its extras arm must exist');
2384
+ assert.match(extras, /return \{ v: lead, s: starterBundles \? extrasRow\(\) : '' \};/, 'the review carries an Extras row, its line the picker\'s own');
2278
2385
  // What that row SAYS has three cases (picked / preselect / catalog not loaded)
2279
2386
  // and they live with the picker's own rules, executed by
2280
2387
  // tests/projects_hub_module_picker.mjs. This pins that the review defers to them.
@@ -2289,7 +2396,7 @@ test('the kit stub answers the picker\'s read, so the panel renders in the desig
2289
2396
  const stub = fs.readFileSync(path.join(ROOT, 'modules', 'ui-design', 'kit', 'serve.js'), 'utf8');
2290
2397
  assert.match(stub, /rest === '\/provisioning\/starter-bundles'/, 'the stub serves the picker\'s read');
2291
2398
  const probes = JSON.parse(fs.readFileSync(path.join(LANDING, 'projects.probes.json'), 'utf8')).probes;
2292
- assert.ok(probes.some((p) => /the wizard driver \(task 1003346\)/.test(p.name)
2293
- && p.steps.some((st) => st[1] === '#panel-modules button[type=submit]')),
2294
- 'and the end-to-end driver walks the new step rather than stopping at it');
2399
+ assert.ok(probes.some((p) => /the wizard driver \(tasks? 1003346\b/.test(p.name)
2400
+ && p.steps.some((st) => st[1] === '#reviewRows .setRow[data-row="extras"] [data-row-ok]')),
2401
+ 'and the end-to-end driver confirms the Extras row rather than stopping at it (task 1004504)');
2295
2402
  });
@@ -257,7 +257,9 @@ test('a repaint never lands on top of an open Retry — from this watch or a fre
257
257
  });
258
258
 
259
259
  test('both surfaces use it: the done panel and the project page', () => {
260
- assert.match(HUB, /if \(box\) watchApp\(instanceId, box, function \(\) \{ return state\.step === 13; \}, 5000\);/);
260
+ // the done panel is screen 6 of the six blended screens (task 1004504)
261
+ assert.match(HUB, /6: 'panel-done' \};/);
262
+ assert.match(HUB, /if \(box\) watchApp\(instanceId, box, function \(\) \{ return state\.step === 6; \}, 5000\);/);
261
263
  assert.match(HUB, /<div id="doneApp" hidden><\/div>/);
262
264
  assert.match(HUB, /<div class="card mSec" id="mAppCard" hidden>\n\s*<h2>Where its app runs<\/h2>\n\s*<div id="mApp"><\/div>/);
263
265
  assert.match(HUB, /loadAppStatus\(inst\.id \|\| asked, inst\);/);