@bongos/core 1.21.4 → 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 (67) hide show
  1. package/.bongos-core.json +118 -63
  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 +2 -0
  14. package/docs/page-inventory.json +17 -25
  15. package/docs/page-readings.json +1008 -1246
  16. package/modules/autonomy/governor-docket.js +101 -0
  17. package/modules/autonomy/routes/autonomy.js +5 -0
  18. package/modules/discord/board-broadcast.js +3 -1
  19. package/modules/discord/craft-broadcast.js +107 -0
  20. package/modules/discord/routes/discord.js +10 -0
  21. package/modules/discord/ship-broadcast.js +30 -2
  22. package/modules/economy/reward.js +3 -0
  23. package/modules/government/board.js +20 -5
  24. package/modules/government/docket-vocab.js +82 -0
  25. package/modules/government/docket.js +458 -0
  26. package/modules/government/routes/government.js +34 -0
  27. package/modules/hall-ui/public/genesis-home.js +9 -10
  28. package/modules/ideas/governor-docket.js +93 -0
  29. package/modules/ideas/routes/blockers.js +5 -0
  30. package/modules/lifecycle/governor-docket.js +192 -0
  31. package/modules/lifecycle/routes/gate-approvals.js +78 -33
  32. package/modules/lifecycle/routes/lifecycle.js +3 -0
  33. package/modules/onboarding/routes/access-requests.js +68 -27
  34. package/modules/public-landing/public/assets/cosmos.css +6 -50
  35. package/modules/public-landing/public/projects.html +1170 -1659
  36. package/modules/public-landing/public/projects.probes.json +18 -18
  37. package/modules/public-landing/public/projects.states.json +23 -31
  38. package/modules/security/routes/reports.js +39 -0
  39. package/package-lock.json +2 -2
  40. package/package.json +1 -1
  41. package/release-notes.json +14 -0
  42. package/src/bongos/module-rechecks.js +77 -0
  43. package/src/bongos/route-rank-check.js +3 -0
  44. package/src/bongos/routes/core-update.js +41 -0
  45. package/src/bongos/routes/modules.js +17 -0
  46. package/src/module-api.js +1 -1
  47. package/tests/discord_craft_broadcast.mjs +162 -0
  48. package/tests/genesis_home.mjs +1 -1
  49. package/tests/government_routes.mjs +3 -1
  50. package/tests/governor_docket.mjs +552 -0
  51. package/tests/onboard.mjs +10 -4
  52. package/tests/platform_boot.mjs +1 -1
  53. package/tests/projects_hub.mjs +196 -89
  54. package/tests/projects_hub_app_status.mjs +3 -1
  55. package/tests/projects_hub_app_step.mjs +72 -48
  56. package/tests/projects_hub_dns_ready.mjs +10 -2
  57. package/tests/projects_hub_look.mjs +32 -22
  58. package/tests/projects_hub_module_picker.mjs +112 -62
  59. package/tests/projects_hub_pre_uat.mjs +48 -41
  60. package/tests/public_landing_projects.mjs +12 -6
  61. package/tests/wizard_demo.mjs +37 -11
  62. package/tests/wizard_draft_resume.mjs +34 -22
  63. package/tests/wizard_front_door.mjs +173 -103
  64. package/tests/wizard_intent_resume.mjs +28 -25
  65. package/tests/wizard_physics.mjs +94 -76
  66. package/tests/wizard_physics_more.mjs +409 -277
  67. package/tests/wizard_six_screens.mjs +126 -0
@@ -24,7 +24,7 @@
24
24
  // NOT covered here, deliberately: the team shape. The first draft of this task claimed the
25
25
  // OAuth hop dropped it; that was wrong — teamShape, projType and modules all survive a
26
26
  // reload (verified live). What looked like a drop was the documented "clicking the chosen
27
- // pill again UNSAYS it" toggle being re-clicked by hand.
27
+ // pill again UNSAYS it" toggle being re-clicked by hand (a toggle task 1004504 retired; slice 3).
28
28
  //
29
29
  // Run: node --test --test-reporter=tap tests/wizard_draft_resume.mjs
30
30
 
@@ -70,29 +70,33 @@ function runStep({ authKnown, draftStep }) {
70
70
  }
71
71
 
72
72
  test('pre-auth: the placeholder renders step 1 but the draft KEEPS the real step', () => {
73
- const { rendered, persisted } = runStep({ authKnown: false, draftStep: 5 });
74
- assert.deepEqual(rendered, [1], 'still renders step 1 (the terms, since task 1004417) before identity settles');
75
- assert.equal(persisted.step, 5, 'the draft must not be overwritten with the placeholder');
73
+ // step 4 is the review since task 1004504 — where the code row's repo access is granted
74
+ const { rendered, persisted } = runStep({ authKnown: false, draftStep: 4 });
75
+ assert.deepEqual(rendered, [1], 'still renders step 1 (the fork, since task 1004504) before identity settles');
76
+ assert.equal(persisted.step, 4, 'the draft must not be overwritten with the placeholder');
76
77
  });
77
78
 
78
79
  test('pre-auth: the in-memory step is restored too, so onAuth() can re-enter to it', () => {
79
80
  // onAuth() calls enter() again and reads state.step from MEMORY, not from storage — so
80
81
  // restoring only the stored copy would fix nothing.
81
- const { state } = runStep({ authKnown: false, draftStep: 6 });
82
- assert.equal(state.step, 6);
82
+ for (const step of [2, 3, 4, 5]) {
83
+ const { state } = runStep({ authKnown: false, draftStep: step });
84
+ assert.equal(state.step, step, `step ${step} comes back`);
85
+ }
83
86
  });
84
87
 
85
88
  test('post-auth: the real step is rendered', () => {
86
- const { rendered, persisted } = runStep({ authKnown: true, draftStep: 5 });
87
- assert.deepEqual(rendered, [5]);
88
- assert.equal(persisted.step, 5);
89
+ const { rendered, persisted } = runStep({ authKnown: true, draftStep: 4 });
90
+ assert.deepEqual(rendered, [4]);
91
+ assert.equal(persisted.step, 4);
89
92
  });
90
93
 
91
94
  test('a draft already at step 1 is unchanged, and an out-of-range step is not resurrected', () => {
92
95
  assert.equal(runStep({ authKnown: false, draftStep: 1 }).persisted.step, 1);
93
- // 12 is the done panel (11 before task 1004421's Look step, 9 before task 1004417's front door, 8 before task 1004351), reached by its own branch
94
- // above this slice; 0 is the gate. Neither is a resumable wizard position, so the placeholder is allowed to stand.
95
- for (const bad of [0, 13, 99]) {
96
+ // 6 is the done panel since task 1004504 (12 before it, 11 before task 1004421's Look step, 9 before task
97
+ // 1004417's front door, 8 before task 1004351), reached by its own branch above this slice; 0 is the gate.
98
+ // Neither is a resumable wizard position, so the placeholder is allowed to stand.
99
+ for (const bad of [0, 6, 13, 99]) {
96
100
  assert.equal(runStep({ authKnown: false, draftStep: bad }).persisted.step, 1, `step ${bad} is not resumed`);
97
101
  }
98
102
  });
@@ -153,9 +157,11 @@ test('while the connect offer is up, the way back is not', () => {
153
157
  // ── slice 3 · why this task's original team-shape claim was wrong ─────────────────
154
158
  // The bug was filed claiming a GitHub round-trip drops the team shape. It does not: a
155
159
  // planted draft reads teamShape back intact after a reload (verified live on 1.19.580).
156
- // The real explanation is this handler — the pill is an UNSAY toggle, so re-clicking the
157
- // already-chosen shape clears it. During the R24 walk that second click was mine. Pinning
158
- // it here so the claim rests on the handler's actual behaviour rather than on prose.
160
+ // The explanation then was this handler — the pill was an UNSAY toggle, so re-clicking the
161
+ // already-chosen shape cleared it. Since task 1004504 the pills live on the review's Team
162
+ // size row, where a team always has a shape ("solo" follows a team of one), so the pill is
163
+ // a plain pick: a second click keeps the answer, and nothing in the handler can drop it.
164
+ // Pinned here so the claim rests on the handler's actual behaviour rather than on prose.
159
165
  // Anchored on addEventListener: the same selector also appears inside paintTeam(), which
160
166
  // only writes aria-pressed. Matching that one instead yields a slice that will not parse.
161
167
  const TEAM_RE = /root\.querySelectorAll\('#teamCards \.tpill'\)\.forEach\(function \(b\) \{\s*b\.addEventListener[\s\S]*?\n {4}\}\);/;
@@ -166,17 +172,20 @@ test('the team-pill slice is still findable', () => {
166
172
  assert.match(teamSlice, /state\.teamShape/, 'the handler owns the team-shape answer');
167
173
  });
168
174
 
169
- test('the team pill UNSAYS on a second click — the answer is not dropped, it is toggled off', () => {
175
+ test('the team pill is a plain pick on the Team size row — a second click keeps the answer (task 1004504)', () => {
170
176
  const state = { teamShape: null };
171
177
  const handlers = [];
178
+ const unconfirmed = [];
179
+ let saves = 0, paints = 0;
172
180
  const pill = (team) => ({
173
181
  getAttribute: (a) => (a === 'data-team' ? team : null),
174
182
  addEventListener: (_e, fn) => handlers.push({ team, fn }),
175
183
  });
176
184
  const sandbox = {
177
185
  state,
178
- root: { querySelectorAll: () => [pill('solo'), pill('small-team'), pill('community')] },
179
- paintTeam() {}, paintDetail() {}, saveDraft() {},
186
+ root: { querySelectorAll: () => [pill('small-team'), pill('community')] },
187
+ paintTeam() {}, saveDraft() { saves += 1; }, paintSetup() { paints += 1; },
188
+ unconfirm(keys) { unconfirmed.push(...keys); },
180
189
  };
181
190
  sandbox.globalThis = sandbox;
182
191
  vm.createContext(sandbox);
@@ -186,9 +195,12 @@ test('the team pill UNSAYS on a second click — the answer is not dropped, it i
186
195
  click('small-team');
187
196
  assert.equal(state.teamShape, 'small-team', 'first click answers');
188
197
  click('small-team');
189
- assert.equal(state.teamShape, null, 'the SAME pill again unsays it — this is the mis-read bug');
190
- click('small-team');
191
- assert.equal(state.teamShape, 'small-team', 'and a third click answers again');
198
+ assert.equal(state.teamShape, 'small-team', 'the SAME pill again keeps it — a team always has a shape');
192
199
  click('community');
193
- assert.equal(state.teamShape, 'community', 'a DIFFERENT pill replaces rather than clears');
200
+ assert.equal(state.teamShape, 'community', 'a DIFFERENT pill replaces it');
201
+ assert.equal(saves, 3, 'every pick is saved into the draft, so the GitHub round-trip keeps it');
202
+ assert.deepEqual([...new Set(unconfirmed)], ['extras'], 'the suggested extras follow the team, so the review asks about them again');
203
+ assert.equal(paints, 3, 'and the review repaints');
204
+ assert.deepEqual([...HUB.matchAll(/data-team="([a-z-]+)"/g)].map((m) => m[1]), ['small-team', 'community'],
205
+ 'the row offers who can join a team; one person is the "One person" pill, not a team shape');
194
206
  });
@@ -1,16 +1,19 @@
1
1
  // tests/wizard_front_door.mjs — the create wizard's front door (task 1004417, BV2.PS05;
2
2
  // goal 1000121, docs/design/project-startup-direction.md "Front door").
3
3
  //
4
- // Two screens now stand in front of the wizard's numbered steps:
5
- // 1 · terms — "anything you want to make": one tick, recorded on the project against
6
- // the terms version the SERVER stamps (tests/provisioning_terms_ack.mjs holds that half);
7
- // 2 · fork — "you are now creating a digital planet": the world's door pill, two equal
8
- // doors, try the demo / begin planet creation. The demo is its own task (BV2.PS09), so
9
- // its door says the demo is not open yet rather than leading anywhere.
4
+ // Since the six blended screens (task 1004504, the design of record
5
+ // docs/design/mocks/combined-onboarding/) the front door is ONE screen:
6
+ // 1 · fork — "You’re about to start a digital planet.": the terms as its one tick, recorded
7
+ // on the project against the terms version the SERVER stamps
8
+ // (tests/provisioning_terms_ack.mjs holds that half), and the world's door pill, two
9
+ // equal doors, try the demo / start my project, both waiting for the tick. The demo is
10
+ // its own route (BV2.PS09). The separate terms screen is gone.
11
+ // The wizard carries no progress rail: an eyebrow names the screen, one Back walks back, and
12
+ // the night room (#wizRoom) stands behind every screen in place of the planet and bongo pair.
10
13
  // Sign-in stays where it was: the gate (step 0) comes first.
11
14
  //
12
15
  // Most of this RUNS the page's own script, the vm-slice idiom of tests/wizard_draft_resume.mjs:
13
- // validateStep(), the front door's wiring, and show()'s class toggles are sliced out of the
16
+ // validateStep(), the front door's wiring, and show()'s screen bookkeeping are sliced out of the
14
17
  // inline script and executed under a minimal DOM stub. Every slice boundary is asserted, so a
15
18
  // rename fails loudly instead of leaving this file quietly testing nothing.
16
19
  //
@@ -32,74 +35,73 @@ const SCRIPT = HUB.slice(HUB.lastIndexOf('<script>'), HUB.lastIndexOf('</script>
32
35
  const STYLE = HUB.slice(HUB.indexOf('<style>'), HUB.indexOf('</style>'));
33
36
 
34
37
  const panel = (id) => (HUB.match(new RegExp(`<section class="panel[^"]*" id="${id}"[\\s\\S]*?\\n {10}</section>`)) || [''])[0];
35
- const TERMS = panel('panel-terms');
36
38
  const FORK = panel('panel-fork');
39
+ const VIEW_NEW = (HUB.match(/<section id="view-new" hidden>[\s\S]*?\n<\/section>/) || [''])[0];
37
40
  const MAP = {};
38
41
  for (const m of ((SCRIPT.match(/var panels = \{([^}]*)\}/) || [, ''])[1]).matchAll(/(\d+):\s*'([a-z-]+)'/g)) MAP[m[2]] = Number(m[1]);
39
42
 
40
43
  // ── the order ───────────────────────────────────────────────────────────────────
41
- test('the front door stands between sign-in and the name step: gate 0, terms 1, fork 2, name 3', () => {
44
+ test('the front door stands between sign-in and the first question: gate 0, fork 1, what 2', () => {
42
45
  assert.equal(MAP['panel-gate'], 0, 'sign-in stays where it is today, first');
43
- assert.equal(MAP['panel-terms'], 1);
44
- assert.equal(MAP['panel-fork'], 2);
45
- // task 1004418: planet physics (questions 1 to 4) sits between the fork and the name step
46
- assert.equal(MAP['panel-physics'], 3, 'the fork opens planet creation with the physics questions');
47
- assert.equal(MAP['panel-name'], 4, 'and today\'s wizard continues unchanged after them');
48
- assert.ok(HUB.indexOf('id="panel-gate"') < HUB.indexOf('id="panel-terms"')
49
- && HUB.indexOf('id="panel-terms"') < HUB.indexOf('id="panel-fork"')
50
- && HUB.indexOf('id="panel-fork"') < HUB.indexOf('id="panel-physics"')
51
- && HUB.indexOf('id="panel-physics"') < HUB.indexOf('id="panel-name"'), 'the markup reads in the same order');
52
- assert.match(TERMS, /<form data-next="2">/, 'the terms go on to the fork');
53
- assert.match(FORK, /<form data-next="3">/, 'the fork\'s primary door goes on to planet physics');
54
- assert.match(FORK, /data-back="1"/, 'and the way back is the terms');
46
+ assert.equal(MAP['panel-fork'], 1, 'the fork, with the terms as its tick, is the first screen after sign-in');
47
+ assert.equal(MAP['panel-what'], 2, 'the real-project door goes on to "What are you making?"');
48
+ assert.equal(MAP['panel-checks'], 3, 'then the quick checks (task 1004504)');
49
+ assert.equal(MAP['panel-terms'], undefined, 'the separate terms screen is gone — the tick lives on the fork');
50
+ assert.doesNotMatch(HUB, /id="panel-terms"/, 'and no markup is left for it');
51
+ assert.ok(HUB.indexOf('id="panel-gate"') < HUB.indexOf('id="panel-fork"')
52
+ && HUB.indexOf('id="panel-fork"') < HUB.indexOf('id="panel-what"')
53
+ && HUB.indexOf('id="panel-what"') < HUB.indexOf('id="panel-checks"'), 'the markup reads in the same order');
54
+ assert.match(FORK, /<form data-next="2">/, 'the fork\'s primary door goes on to the first question');
55
+ assert.ok(VIEW_NEW, 'the ?view=new section is findable');
56
+ assert.doesNotMatch(VIEW_NEW, /data-back=/, 'no per-panel Back buttons — one #wizBack walks the screens');
57
+ assert.equal((VIEW_NEW.match(/id="wizBack"/g) || []).length, 1, 'one Back, in the wizard\'s top row');
55
58
  });
56
59
 
57
- test('the rail is the numbered steps\' — it steps aside on both front-door screens, statically too', () => {
60
+ test('the numbered rail is gone (task 1004504) — no steps list, no painter, no rule hiding it', () => {
58
61
  const beats = [...HUB.matchAll(/<li[^>]*data-step="(\d+)"/g)].map((m) => Number(m[1]));
59
- assert.deepEqual(beats, [3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 'the rail numbers physics…create, never the front door');
60
- assert.match(HUB, /<div class="card wizFront">/, 'a no-script visitor on the static first screen sees no rail');
61
- assert.match(STYLE, /#view-new \.card\.wizFront \.steps\{display:none;\}/);
62
+ assert.deepEqual(beats, [], 'no rail beat is left anywhere on the page');
63
+ assert.doesNotMatch(HUB, /class="steps"/, 'no <ol class="steps"> in the wizard');
64
+ assert.doesNotMatch(SCRIPT, /paintRail\(/, 'no rail painter, and no call to one');
65
+ assert.doesNotMatch(STYLE, /\.wizFront \.steps|#view-new \.steps/, 'no rule left styling or hiding a rail');
62
66
  });
63
67
 
64
- // ── screen 1 · the terms ────────────────────────────────────────────────────────
65
- test('screen 1 is the canvas\'s "anything you want to make": two ledes, one tick, a read-the-terms link, one door', () => {
66
- assert.ok(TERMS, '#panel-terms ships as real markup (C7)');
67
- assert.match(TERMS, /^<section class="panel on" id="panel-terms"/, 'it carries the static `on` — step 1 for a no-script visitor');
68
- assert.match(TERMS, /<h1 id="terms-h">Anything you want to make<\/h1>/);
69
- assert.equal((TERMS.match(/<p class="lede">/g) || []).length, 2, 'two lede paragraphs');
70
- assert.match(TERMS, /<input type="checkbox" id="termsAck" aria-describedby="h-terms">/, 'one checkbox, named by its hint');
71
- assert.match(TERMS, /<label class="termsAck">\s*<input[^>]*>\s*<span>I can sign legally, or can get a signature, for the use of this platform, and I acknowledge the terms and conditions\.<\/span>/,
72
- 'the whole row is its label, so the target is the sentence, not a 20px box');
73
- assert.match(TERMS, /<a class="termsRead" href="\/terms" target="_blank" rel="noopener">Read the terms<\/a>/,
74
- 'the platform\'s own /terms, in a new tab so the draft is not left behind');
75
- assert.equal((TERMS.match(/type="submit"/g) || []).length, 1, 'one primary door');
76
- assert.match(TERMS, /<span data-brand-product-inline>Cloud Bongos<\/span>/, 'the product name is the pack\'s, re-branded on a child');
68
+ // ── screen 1 · the fork, with the terms as its one tick ─────────────────────────
69
+ test('screen 1 is the fork: one short lede, one tick that links the terms, one submit door', () => {
70
+ assert.ok(FORK, '#panel-fork ships as real markup (C7)');
71
+ assert.match(FORK, /^<section class="panel on" id="panel-fork"/, 'it carries the static `on` — step 1 for a no-script visitor');
72
+ assert.match(FORK, /<h1 id="fork-h">You’re about to start a digital planet\.<\/h1>/);
73
+ assert.equal((FORK.match(/<div id="forkIntro">[\s\S]*?<\/form>/)?.[0].match(/<p class="lede">/g) || []).length, 1, 'one short lede paragraph');
74
+ assert.match(FORK, /<input type="checkbox" id="termsAck" aria-describedby="h-terms">/, 'one checkbox, named by its hint');
75
+ assert.match(FORK, /<label class="termsAck">\s*<input[^>]*>\s*<span>I can sign for this project, and I accept the <a class="termsRead" href="\/terms" target="_blank" rel="noopener">terms<\/a>\.<\/span>/,
76
+ 'the whole row is its label, so the target is the sentence, not a 20px box — and the terms are the platform\'s own /terms, in a new tab so the draft is not left behind');
77
+ assert.equal((FORK.match(/type="submit"/g) || []).length, 1, 'one primary door');
78
+ assert.doesNotMatch(FORK, /\bBongos\b/, 'no product name baked where a child instance would ship it');
77
79
  });
78
80
 
79
81
  test('the page never names a terms version — the server stamps which text was acknowledged', () => {
80
82
  // A version baked here would be a second copy of TERMS_VERSION that drifts, and worse, a
81
83
  // value a caller could send. The create request carries only the tick.
82
84
  assert.doesNotMatch(HUB, /terms_version|termsVersion|terms_ack_version/, 'no version travels from the page');
83
- assert.doesNotMatch(TERMS, /\b20\d\d-\d\d-\d\d\b/, 'and none is printed on the screen');
85
+ assert.doesNotMatch(FORK, /\b20\d\d-\d\d-\d\d\b/, 'and none is printed on the screen');
84
86
  });
85
87
 
86
- test('the resume offer moved with step 1 — it lives on the terms screen, once', () => {
88
+ test('the resume offer lives on step 1 — the fork, once', () => {
87
89
  assert.equal((HUB.match(/id="wizResume"/g) || []).length, 1);
88
- assert.ok(TERMS.includes('id="wizResume"'), 'a deliberate "New project" lands here, so the offer does too');
90
+ assert.ok(FORK.includes('id="wizResume"'), 'a deliberate "New project" lands here, so the offer does too');
89
91
  });
90
92
 
91
93
  // ── the validator, executed ────────────────────────────────────────────────────
92
94
  const VALIDATE_RE = /function validateStep\(step\) \{[\s\S]*?\n {4}\}/;
93
95
  const validateSlice = (SCRIPT.match(VALIDATE_RE) || [null])[0];
94
96
 
95
- function runValidate(step, termsAck) {
97
+ function runValidate(step, termsAck, extra = {}) {
96
98
  const els = {};
97
99
  const $ = (id) => (els[id] = els[id] || { textContent: '', className: '', classList: { add() {} }, focus() {} });
98
- const sandbox = { $, state: { termsAck, name: 'Mercury', projType: 'game' }, validateRender: () => true, repoAccess: true };
100
+ const sandbox = { $, state: { termsAck, name: 'Mercury', projType: 'game', demo: null, confirmed: {}, ...extra }, validateRender: () => true, repoAccess: true };
99
101
  sandbox.globalThis = sandbox;
100
102
  vm.createContext(sandbox);
101
103
  vm.runInContext(`${validateSlice}\nvar ok = validateStep(${step});`, sandbox);
102
- return { ok: sandbox.ok, hint: els['h-terms'] };
104
+ return { ok: sandbox.ok, hint: els['h-terms'], state: sandbox.state };
103
105
  }
104
106
 
105
107
  test('validateStep is still findable, with the terms clause inside it', () => {
@@ -107,35 +109,44 @@ test('validateStep is still findable, with the terms clause inside it', () => {
107
109
  assert.match(validateSlice, /if \(step === 1\) \{\s*if \(!state\.termsAck\)/, 'the clause is part of the slice');
108
110
  });
109
111
 
110
- test('Proceed is refused until the box is ticked, and the refusal says why in the hint', () => {
112
+ test('Start my project is refused until the box is ticked, and the refusal says why in the hint', () => {
111
113
  const refused = runValidate(1, false);
112
- assert.equal(refused.ok, false, 'no tick, no fork');
114
+ assert.equal(refused.ok, false, 'no tick, no way past the fork');
113
115
  assert.equal(refused.hint.className, 'hint err');
114
116
  assert.match(refused.hint.textContent, /Tick the box to go on/);
115
117
  const given = runValidate(1, true);
116
118
  assert.equal(given.ok, true, 'a tick goes on');
117
119
  });
118
120
 
119
- test('the fork refuses nothing — its doors are the answer, and Begin clears a demo started earlier', () => {
120
- assert.equal(runValidate(2, false).ok, true, 'the fork asks nothing a validator could refuse');
121
- // task 1004420: "Begin planet creation" is a real project, so a demo the founder
121
+ test('Start my project clears a demo started earlier — the submit door is a real project', () => {
122
+ // task 1004420: "Start my project" is a real project, so a demo the founder
122
123
  // set up and then walked back from must not ride the create
123
- assert.match(validateSlice, /if \(step === 2\) \{ state\.demo = null; return true; \}/);
124
+ const r = runValidate(1, true, { demo: { people: 3, hours_each: 4 }, confirmed: { team: true } });
125
+ assert.equal(r.ok, true);
126
+ assert.equal(r.state.demo, null, 'the walked-back demo does not ride the create');
127
+ assert.deepEqual(JSON.parse(JSON.stringify(r.state.confirmed)), {}, 'and its rows are asked again');
128
+ assert.equal(runValidate(1, false, { demo: { people: 3 } }).state.demo !== null, true, 'an unticked box changes nothing');
129
+ });
130
+
131
+ test('"What are you making?" asks a name and a kind (task 1004504)', () => {
132
+ assert.equal(runValidate(2, true).ok, true, 'a name and a kind go on');
133
+ assert.equal(runValidate(2, true, { name: ' ' }).ok, false, 'no name, no way on');
134
+ assert.equal(runValidate(2, true, { projType: null }).ok, false, 'no kind, no way on — Not sure is a fine answer');
124
135
  });
125
136
 
126
137
  // ── the wiring, executed ───────────────────────────────────────────────────────
127
- const WIRE_START = " $('termsAck').checked = !!state.termsAck;";
138
+ const WIRE_START = ' function paintDoors() {';
128
139
  const WIRE_END = ' /* ── the demo route (task 1004420, BV2.PS09) ──';
129
- const wireSlice = SCRIPT.includes(WIRE_START) ? SCRIPT.slice(SCRIPT.indexOf(WIRE_START), SCRIPT.indexOf(WIRE_END)) : null;
140
+ const wireSlice = SCRIPT.includes(WIRE_START) && SCRIPT.includes(WIRE_END) ? SCRIPT.slice(SCRIPT.indexOf(WIRE_START), SCRIPT.indexOf(WIRE_END)) : null;
130
141
 
131
142
  function runWiring(initialAck) {
132
143
  const handlers = {};
133
144
  const mk = (id) => ({
134
- id, checked: false, hidden: false, textContent: '', className: '', attrs: {},
145
+ id, checked: false, hidden: false, disabled: true, textContent: '', className: '', attrs: {},
135
146
  addEventListener(type, fn) { handlers[id + ':' + type] = fn; },
136
147
  setAttribute(k, v) { this.attrs[k] = v; },
137
148
  });
138
- const els = { termsAck: mk('termsAck'), 'h-terms': mk('h-terms') };
149
+ const els = { termsAck: mk('termsAck'), 'h-terms': mk('h-terms'), wizDemo: mk('wizDemo'), wizStart: mk('wizStart') };
139
150
  let saves = 0;
140
151
  const sandbox = { $: (id) => els[id], state: { termsAck: initialAck }, saveDraft: () => { saves += 1; } };
141
152
  sandbox.globalThis = sandbox;
@@ -147,6 +158,25 @@ function runWiring(initialAck) {
147
158
  test('the front door\'s wiring is still findable', () => {
148
159
  assert.ok(wireSlice && wireSlice.length > 100, 'the wiring block was not found in projects.html');
149
160
  assert.match(wireSlice, /\$\('termsAck'\)\.addEventListener\('change'/, 'the tick is part of the slice');
161
+ assert.match(wireSlice, /\$\('wizStart'\)\.disabled = !state\.termsAck;/, 'and so is the door painter');
162
+ });
163
+
164
+ test('both doors wait for the tick: disabled in the markup, and painted from the answer', () => {
165
+ assert.match(FORK, /<button type="button" class="btn big ghost" id="wizDemo"[^>]*\bdisabled>/, 'the demo door ships disabled');
166
+ assert.match(FORK, /<button type="submit" class="btn big" id="wizStart" disabled>/, 'and so does Start my project');
167
+ const fresh = runWiring(false);
168
+ assert.equal(fresh.els.wizDemo.disabled, true, 'an unticked draft keeps the demo door shut');
169
+ assert.equal(fresh.els.wizStart.disabled, true, 'and the real-project door');
170
+ fresh.els.termsAck.checked = true; fresh.handlers['termsAck:change'].call(fresh.els.termsAck);
171
+ assert.equal(fresh.els.wizDemo.disabled, false, 'ticking opens both doors');
172
+ assert.equal(fresh.els.wizStart.disabled, false);
173
+ fresh.els.termsAck.checked = false; fresh.handlers['termsAck:change'].call(fresh.els.termsAck);
174
+ assert.ok(fresh.els.wizDemo.disabled && fresh.els.wizStart.disabled, 'unticking shuts them again');
175
+ const resumed = runWiring(true);
176
+ assert.ok(!resumed.els.wizDemo.disabled && !resumed.els.wizStart.disabled, 'a draft that ticked comes back with its doors open');
177
+ assert.match(STYLE, /#view-new \.doorPill \.btn\[disabled\]\{opacity:\.35;cursor:default;\}/, 'a shut door reads as shut');
178
+ assert.match(SCRIPT, /\$\('wizDemo'\)\.addEventListener\('click', function \(\) \{ if \(state\.termsAck\) demoShow\('setup'\); \}\);/,
179
+ 'and the demo door refuses an unticked box even if the attribute is stripped');
150
180
  });
151
181
 
152
182
  test('the tick mirrors into the draft both ways, and a resumed draft paints its own answer', () => {
@@ -167,14 +197,14 @@ test('the demo door names what it opens: the demo route\'s setup screen (task 10
167
197
  assert.doesNotMatch(FORK, /demoSoon|isn’t open yet/, 'the placeholder that said the demo was not open is gone');
168
198
  });
169
199
 
170
- // ── screen 2 · the fork ─────────────────────────────────────────────────────────
171
- test('screen 2 is the door pill: two equal doors in one object, the accent only on the one that goes on', () => {
172
- assert.match(FORK, /<h1 id="fork-h">You are now creating a digital planet<\/h1>/);
200
+ // ── the fork's door pill ────────────────────────────────────────────────────────
201
+ test('the fork is the door pill: two equal doors in one object, the accent only on the one that goes on', () => {
202
+ assert.match(FORK, /<h1 id="fork-h">You’re about to start a digital planet\.<\/h1>/);
173
203
  const pill = (FORK.match(/<div class="doorPill" role="group" aria-label="How to begin">[\s\S]*?<\/div>/) || [''])[0];
174
204
  assert.ok(pill, 'one grouped pill');
175
205
  const doors = [...pill.matchAll(/<button type="(button|submit)" class="([^"]+)"[^>]*>([^<]+)<\/button>/g)];
176
206
  assert.equal(doors.length, 2, 'exactly two doors');
177
- assert.deepEqual(doors.map((d) => d[3]), ['Try the demo', 'Begin planet creation']);
207
+ assert.deepEqual(doors.map((d) => d[3]), ['Try the demo', 'Start my project']);
178
208
  assert.equal(doors[0][2], 'btn big ghost', 'the demo door is the secondary: transparent at rest');
179
209
  assert.equal(doors[1][2], 'btn big', 'begin carries the accent');
180
210
  assert.equal(doors[1][1], 'submit', 'and is the form\'s one submit, so Enter goes on');
@@ -183,58 +213,94 @@ test('screen 2 is the door pill: two equal doors in one object, the accent only
183
213
  assert.match(STYLE, /@media \(max-width:520px\)\{\s*\.doorPill\{display:grid;grid-template-columns:1fr;/, 'at a phone width the two stack, still one object');
184
214
  });
185
215
 
186
- test('the seed stands on the world\'s own field plate — nothing drawn but hairlines', () => {
187
- assert.match(FORK, /<div class="forkStage" id="forkStage" aria-hidden="true">/, 'decorative, so hidden from a screen reader');
188
- assert.match(FORK, /<img class="forkSeed" src="\/assets\/world\/r2-chrome-blob-one-cut\.webp" alt="">/, 'a real plate, root-absolute so it is stamped');
216
+ test('the night room stands behind every screen — the planet stage and the bongo pair are gone (task 1004504)', () => {
217
+ assert.match(VIEW_NEW, /<div class="wizRoom" id="wizRoom" data-screen="fork" aria-hidden="true">/, 'decorative, so hidden from a screen reader, and baked at the fork for a no-script visitor');
218
+ assert.match(VIEW_NEW, /<span class="rmSeed"><img src="\/assets\/world\/r2-chrome-blob-one-cut\.webp" alt=""><\/span>/, 'the seed is a real plate, root-absolute so it is stamped');
189
219
  assert.ok(fs.existsSync(path.join(LANDING, 'assets', 'world', 'r2-chrome-blob-one-cut.webp')), 'and it exists on disk');
190
- assert.match(STYLE, /\.forkStage\{[^}]*background:var\(--sky-bg\) var\(--field-plate\) center\/cover no-repeat;/, 'the field is the plate token, not a new image');
191
- assert.match(STYLE, /\.forkOrbit\{[^}]*border:1px solid var\(--rule\);border-radius:50%;/, 'the orbits are hairline circles');
192
- const at = STYLE.indexOf('the front door (task 1004417');
193
- const block = STYLE.slice(at, STYLE.indexOf('/* a field:', at));
194
- assert.ok(at > -1 && block.includes('.forkStage{'), 'the front door\'s CSS block is findable');
195
- assert.doesNotMatch(block, /#[0-9a-f]{3,8}\b|box-shadow:(?!none)|linear-gradient/i, 'every colour a token, no shadow, no gradient');
220
+ assert.doesNotMatch(HUB, /id="forkStage"|class="forkSeed"|class="forkRoutes"|bongoCell|id="physStage"/, 'no fork stage, seed orbit, route list, bongo pair or forming planet left in the markup');
221
+ assert.doesNotMatch(STYLE, /\.forkStage\{|\.forkOrbit\{|\.bongoCell|\.physStage/, 'and no rule left for them');
222
+ const at = STYLE.indexOf('#view-new .wizRoom{');
223
+ const end = STYLE.indexOf('/* under 900 the window', at);
224
+ assert.ok(at > -1 && end > at, 'the night room\'s CSS block is findable');
225
+ const block = STYLE.slice(at, end);
226
+ const hexes = [...block.matchAll(/#[0-9a-f]{3,8}\b/gi)].map((m) => m[0].toLowerCase());
227
+ assert.ok(hexes.length > 0 && hexes.every((h) => h === '#000' || h === '#fff'), `every colour a mix of the fifteen with black or white (${[...new Set(hexes)]})`);
228
+ for (const m of block.matchAll(/--rm-[a-z0-9-]+:([^;]+);/g)) {
229
+ assert.match(m[1], /^color-mix\(in srgb,var\(--(sea|accent)\)|^rgba\(255,255,255,/, `a room token is derived, never a new literal: ${m[0]}`);
230
+ }
231
+ const motion = block.indexOf('@media (prefers-reduced-motion:no-preference){');
232
+ assert.ok(motion > -1, 'the room\'s motion sits inside the kill switch');
233
+ assert.doesNotMatch(block.slice(0, motion), /animation:|transition:/, 'and none outside it: the rest frame is the whole composition');
234
+ assert.doesNotMatch(block, /:root/, 'room tokens live on the room, never a page :root');
196
235
  });
197
236
 
198
237
  test('the two routes are described honestly for today: the demo is open (task 1004420), creation promises no questions that do not exist yet', () => {
199
- const routes = (FORK.match(/<div class="forkRoutes">[\s\S]*$/) || [''])[0];
200
- assert.match(routes, /Route 1 · demo<\/p>/);
201
- assert.doesNotMatch(routes, /coming soon/, 'the demo is built, so the route no longer says it is coming');
202
- assert.match(routes, /Route 2 · planet creation/);
203
- assert.doesNotMatch(routes, /seven questions/i, 'the seven planet questions are PS07\'s — the copy must not promise them before they ship');
204
- assert.doesNotMatch(FORK, /\bBongos\b(?! pair)/, 'no product name baked where a child instance would ship it');
238
+ const intro = (FORK.match(/<div id="forkIntro">[\s\S]*?<\/form>/) || [''])[0];
239
+ assert.match(intro, /<p class="lede">Try a small demo first, or start your real project\.<\/p>/);
240
+ assert.doesNotMatch(FORK, /coming soon/, 'the demo is built, so nothing says it is coming');
241
+ assert.doesNotMatch(FORK, /seven questions/i, 'the copy promises no question count');
242
+ assert.doesNotMatch(FORK, /\bBongos\b/, 'no product name baked where a child instance would ship it');
205
243
  });
206
244
 
207
- // ── show(): the class toggles, executed ────────────────────────────────────────
245
+ // ── show(): the room, the eyebrow and the one Back, executed ───────────────────
246
+ // The slice runs from the SCREEN map through show(), so eyebrowFor() and prevStep() ride
247
+ // along; both ends are asserted.
248
+ const SHOW_START = ' var SCREEN = {';
208
249
  const SHOW_RE = /function show\(step, moveFocus\) \{[\s\S]*?\n {4}\}/;
209
- const showSlice = (SCRIPT.match(SHOW_RE) || [null])[0];
250
+ const showMatch = SCRIPT.match(SHOW_RE);
251
+ const showSlice = showMatch && SCRIPT.includes(SHOW_START) && SCRIPT.indexOf(SHOW_START) < showMatch.index
252
+ ? SCRIPT.slice(SCRIPT.indexOf(SHOW_START), showMatch.index + showMatch[0].length) : null;
210
253
 
211
- function runShow(step) {
254
+ function runShow(step, { demo = null, demoOpen = false } = {}) {
212
255
  const cls = (name) => { const set = new Set(); return { name, set, toggle(c, on) { if (on) set.add(c); else set.delete(c); } }; };
213
- const card = cls('card'), split = cls('split');
214
- const panels = {};
215
- const $ = (id) => (panels[id] = panels[id] || { classList: cls(id), querySelector: () => null });
256
+ const els = {};
257
+ const $ = (id) => (els[id] = els[id] || {
258
+ id, classList: cls(id), hidden: id === 'demoSetup' ? !demoOpen : false, textContent: '', attrs: {},
259
+ setAttribute(k, v) { this.attrs[k] = v; }, querySelector: () => null,
260
+ });
216
261
  const sandbox = {
217
- $, state: {}, panels: Object.fromEntries(Object.entries(MAP).map(([id, n]) => [n, id])),
218
- root: { querySelector: (sel) => (sel === '.card' ? { classList: card } : { classList: split }) },
219
- paintRail() {}, saveDraft() {}, authKnown: false, repoAccess: null,
262
+ $, state: { demo }, panels: Object.fromEntries(Object.entries(MAP).map(([id, n]) => [n, id])),
263
+ saveDraft() {}, authKnown: false, repoAccess: null,
264
+ paintDoors() {}, physLoadAccountOnce() {}, paintChecks() {}, physDefaults() {},
220
265
  renderModules() {}, syncHome() {}, ensureRepoAccess() {}, checkSlugAvailable() {}, renderAddrPanel() {},
221
- syncAppStep() {}, syncRenderSetup() {}, renderReview() {}, renderLook() {}, paintPhysics() {},
266
+ syncAppStep() {}, syncRenderSetup() {}, paintSetup() {}, renderLook() {}, renderRecap() {},
222
267
  };
223
268
  sandbox.globalThis = sandbox;
224
269
  vm.createContext(sandbox);
225
270
  vm.runInContext(`${showSlice}\nshow(${step}, false);`, sandbox);
226
- return { card: card.set, split: split.set, on: Object.keys(panels).filter((id) => panels[id].classList.set.has('on')) };
271
+ return {
272
+ on: Object.keys(els).filter((id) => els[id].classList.set.has('on')),
273
+ screen: els.wizRoom.attrs['data-screen'], eyebrow: els.wizEyebrow.textContent,
274
+ back: !els.wizBack.hidden, tag: !els.demoTag.hidden,
275
+ };
227
276
  }
228
277
 
229
- test('show() takes the rail away on the two front-door screens only, and gives the fork the whole card', () => {
230
- assert.ok(showSlice, 'show() was not found in projects.html');
231
- const terms = runShow(1), fork = runShow(2), physics = runShow(3), name = runShow(4), done = runShow(MAP['panel-done']);
232
- assert.deepEqual(terms.on, ['panel-terms']);
233
- assert.ok(terms.card.has('wizFront') && !terms.split.has('wizFork'), 'terms: no rail, the bongo pair stays');
234
- assert.ok(fork.card.has('wizFront') && fork.split.has('wizFork'), 'fork: no rail, and the seed takes the pair\'s place');
235
- assert.ok(!physics.card.has('wizFront') && physics.split.has('wizFork'), 'physics (task 1004418): the rail is back, and the forming planet keeps the whole card');
236
- assert.ok(!name.card.has('wizFront') && !name.split.has('wizFork'), 'the name step has its rail back');
237
- assert.ok(done.split.has('wizDone') && !done.card.has('wizFront'), 'the done panel is untouched by the front door');
278
+ test('show() turns one screen on, tells the room which screen it is, and names it in the eyebrow', () => {
279
+ assert.ok(showSlice, 'the SCREEN map through show() was not found in projects.html');
280
+ assert.match(showSlice, /function eyebrowFor\(step\)/, 'the eyebrow is part of the slice');
281
+ assert.doesNotMatch(showSlice, /paintRail|wizFront|wizFork|wizDone/, 'show() paints no rail and toggles no front-door or done layout (task 1004504)');
282
+ const screens = { 0: 'signin', 1: 'fork', 2: 'what', 3: 'checks', 4: 'review', 5: 'look', 6: 'done' };
283
+ for (const [n, screen] of Object.entries(screens)) {
284
+ const r = runShow(Number(n));
285
+ const id = Object.keys(MAP).find((k) => MAP[k] === Number(n));
286
+ assert.deepEqual(r.on, [id], `step ${n}: only ${id} is on`);
287
+ assert.equal(r.screen, screen, `step ${n}: the room grows to "${screen}"`);
288
+ }
289
+ assert.equal(runShow(1).eyebrow, 'New project');
290
+ assert.equal(runShow(1, { demoOpen: true }).eyebrow, 'Demo', 'the demo\'s own screen says so');
291
+ assert.equal(runShow(2, { demo: { people: 3 } }).eyebrow, 'New demo', 'and a demo past the fork stays marked');
292
+ assert.equal(runShow(6).eyebrow, 'Founding');
293
+ });
294
+
295
+ test('one Back: hidden at sign-in, on the fork and on done; shown on every screen between', () => {
296
+ assert.equal(runShow(0).back, false, 'sign-in has nowhere to go back to');
297
+ assert.equal(runShow(1).back, false, 'the fork is the first screen');
298
+ assert.equal(runShow(1, { demoOpen: true }).back, true, 'the demo screen walks back to the fork');
299
+ for (const n of [2, 3, 4, 5]) assert.equal(runShow(n).back, true, `step ${n} walks back`);
300
+ assert.equal(runShow(6).back, false, 'a created project is not walked back into the wizard');
301
+ assert.equal(runShow(2).tag, false, 'no demo tag on a real project');
302
+ assert.equal(runShow(2, { demo: { people: 3 } }).tag, true, 'the demo tag rides every screen of a demo');
303
+ assert.match(SCRIPT, /function prevStep\(step\) \{\s*if \(step === 4 && state\.demo\) return 2;/, 'a demo, which skipped the checks, walks back past them too');
238
304
  });
239
305
 
240
306
  // ── the create ──────────────────────────────────────────────────────────────────
@@ -243,29 +309,33 @@ test('the create carries the tick, and is never filed without it', () => {
243
309
  assert.match(create, /terms_acknowledged: state\.termsAck \? true : undefined,/,
244
310
  'a real boolean when ticked, absent otherwise — the route refuses anything but a boolean');
245
311
  const guard = create.indexOf('if (!state.termsAck) { show(1, true); validateStep(1); return; }');
246
- assert.ok(guard > -1, 'an unticked draft is sent back to the terms');
312
+ assert.ok(guard > -1, 'an unticked draft is sent back to the fork\'s tick');
247
313
  assert.ok(guard < create.indexOf('ensureFreshRepo()'), 'BEFORE anything is created — the repo included');
248
314
  });
249
315
 
250
316
  test('the tick is part of the draft and of the reset, and the draft key moved with the renumbering', () => {
251
- assert.match(SCRIPT, /var state = \{ step: 0,[^}]*termsAck: false \};/, 'the initial state declares it');
252
- assert.match(SCRIPT, /state = \{ step: 1,[^}]*termsAck: false \};/, 'and "start another project" clears it — each project records its own');
253
- assert.match(SCRIPT, /var DRAFT_KEY = 'cb-create-draft-v8';/, 'a draft saved under the old numbering is ignored, not replayed onto the wrong screen');
317
+ assert.match(SCRIPT, /var state = \{ step: 0,[^}]*termsAck: false, confirmed: \{\} \};/, 'the initial state declares it');
318
+ assert.match(SCRIPT, /state = \{ step: 1,[^}]*termsAck: false, confirmed: \{\} \};/, 'and "start another project" clears it — each project records its own');
319
+ assert.match(SCRIPT, /\$\('termsAck'\)\.checked = false; paintDoors\(\);/, 'and the reset shuts the doors with it');
320
+ assert.match(SCRIPT, /var DRAFT_KEY = 'cb-create-draft-v9';/, 'a draft saved under the old numbering is ignored, not replayed onto the wrong screen');
254
321
  });
255
322
 
256
323
  // ── the look-before-you-ship kit walks it ─────────────────────────────────────
257
- test('the kit has a state for each front-door screen, and every walk past it ticks and chooses first', () => {
324
+ // Every walk past the fork opens the way a founder does: the tick, then a door — Start my
325
+ // project, or the demo door and the demo screen's Continue.
326
+ test('every kit walk past the fork ticks first, then takes a door', () => {
258
327
  const states = JSON.parse(fs.readFileSync(path.join(LANDING, 'projects.states.json'), 'utf8')).states;
259
- for (const s of ['in-new', 'in-new-terms-refused', 'in-new-fork', 'in-new-fork-demo', 'in-new-name']) assert.ok(states[s], `the ${s} state exists`);
260
- assert.deepEqual(states['in-new'].expect.visible, ['#panel-terms', '#termsAck'], 'arriving at ?view=new pictures the terms');
261
- const FRONT = ['#termsAck', '#panel-terms button[type=submit]', '#panel-fork button[type=submit]'];
328
+ assert.ok(states['in-new'], 'the in-new state exists');
329
+ for (const sel of ['#panel-fork', '#termsAck']) assert.ok((states['in-new'].expect.visible || []).includes(sel), `arriving at ?view=new pictures the fork and its tick (${sel})`);
330
+ const REAL = ['#termsAck', '#wizStart'];
331
+ const DEMO = ['#termsAck', '#wizDemo', '#demoStart'];
262
332
  const probes = JSON.parse(fs.readFileSync(path.join(LANDING, 'projects.probes.json'), 'utf8')).probes;
263
333
  const walks = [...Object.entries(states).map(([k, v]) => [k, v.actions || []]), ...probes.map((p) => [p.name, p.steps])]
264
334
  .filter(([, steps]) => steps.some((st) => st[1] === '#newName'));
265
335
  assert.ok(walks.length >= 12, `every wizard walk is found (${walks.length})`);
266
336
  for (const [name, steps] of walks) {
267
337
  const clicks = steps.filter((st) => st[0] === 'click').map((st) => st[1]);
268
- assert.deepEqual(clicks.slice(0, 3), FRONT, `${name}: the front door comes first`);
338
+ const door = clicks[1] === '#wizDemo' ? DEMO : REAL;
339
+ assert.deepEqual(clicks.slice(0, door.length), door, `${name}: the tick, then a door, come first`);
269
340
  }
270
- assert.ok(probes.some((p) => /the front door \(task 1004417\)/.test(p.name)), 'a probe drives the refusal, the demo door and the way on');
271
341
  });