@bongos/core 1.21.4 → 1.21.6

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 (126) hide show
  1. package/.bongos-core.json +233 -108
  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 +3 -1
  6. package/clients/bongos-client/index.mjs +2 -0
  7. package/docs/adr/0361-merges-publish-as-candidates-release-decides-what-others-are-offered.md +1 -0
  8. package/docs/adr/0362-the-governor-docket-is-collected-from-contributed-sources.md +34 -0
  9. package/docs/adr/README.md +1 -0
  10. package/docs/api/openapi.json +37 -2
  11. package/docs/api-reference.md +4 -3
  12. package/docs/copy-inventory.md +541 -658
  13. package/docs/copy-registry.json +1205 -2340
  14. package/docs/module-api-changelog.md +4 -0
  15. package/docs/onboarding/diagrams/03-drachmae-karma.mmd +1 -1
  16. package/docs/onboarding/diagrams/assertions.json +1 -1
  17. package/docs/page-inventory.json +47 -28
  18. package/docs/page-readings.json +1128 -1339
  19. package/modules/autonomy/config-idle.js +192 -0
  20. package/modules/autonomy/governor-docket.js +101 -0
  21. package/modules/autonomy/routes/autonomy.js +27 -0
  22. package/modules/autonomy/runner-health.js +22 -1
  23. package/modules/discord/board-broadcast.js +3 -1
  24. package/modules/discord/craft-broadcast.js +107 -0
  25. package/modules/discord/routes/discord.js +10 -0
  26. package/modules/discord/ship-broadcast.js +30 -2
  27. package/modules/economy/reward.js +3 -0
  28. package/modules/government/board.js +20 -5
  29. package/modules/government/docket-vocab.js +82 -0
  30. package/modules/government/docket.js +458 -0
  31. package/modules/government/routes/government.js +34 -0
  32. package/modules/hall-ui/public/city-draw.js +438 -0
  33. package/modules/hall-ui/public/city.css +110 -0
  34. package/modules/hall-ui/public/city.html +71 -0
  35. package/modules/hall-ui/public/city.js +177 -0
  36. package/modules/hall-ui/public/city.states.json +30 -0
  37. package/modules/hall-ui/public/gate.js +11 -4
  38. package/modules/hall-ui/public/genesis-home.js +9 -10
  39. package/modules/hall-ui/public/profile.css +4 -0
  40. package/modules/hall-ui/public/profile.js +25 -1
  41. package/modules/hall-ui/public/settings-autobongos.js +19 -1
  42. package/modules/hall-ui/public/settings.css +26 -0
  43. package/modules/hall-ui/public/settings.html +8 -0
  44. package/modules/hall-ui/public/settings.js +43 -3
  45. package/modules/ideas/governor-docket.js +93 -0
  46. package/modules/ideas/routes/blockers.js +5 -0
  47. package/modules/lifecycle/governor-docket.js +192 -0
  48. package/modules/lifecycle/routes/gate-approvals.js +78 -33
  49. package/modules/lifecycle/routes/lifecycle.js +3 -0
  50. package/modules/onboarding/routes/access-requests.js +68 -27
  51. package/modules/platform-identity/craft-rollup.js +72 -0
  52. package/modules/platform-identity/migrations/platform_identity_029_activity_crafts.sql +23 -0
  53. package/modules/platform-identity/platform-identity.js +12 -6
  54. package/modules/platform-identity/routes/sso.js +4 -0
  55. package/modules/platform-identity/tests/platform-identity.mjs +1 -1
  56. package/modules/provisioning/core-upgrade.js +34 -7
  57. package/modules/provisioning/module.json +2 -1
  58. package/modules/provisioning/seams.js +2 -0
  59. package/modules/public-landing/public/assets/cosmos.css +6 -50
  60. package/modules/public-landing/public/projects.html +1170 -1659
  61. package/modules/public-landing/public/projects.probes.json +18 -18
  62. package/modules/public-landing/public/projects.states.json +24 -31
  63. package/modules/security/routes/reports.js +39 -0
  64. package/package-lock.json +2 -2
  65. package/package.json +1 -1
  66. package/release-notes.json +72 -0
  67. package/scripts/gds/autobongos-grade-cap.js +187 -0
  68. package/scripts/gds/autobongos-loop.js +4 -0
  69. package/scripts/gds/autobongos-run.js +154 -13
  70. package/scripts/gds/autobongos-verify.js +211 -14
  71. package/scripts/gds/provision-core-upgrade.js +14 -2
  72. package/scripts/gds/provision-repo.js +77 -13
  73. package/scripts/gds/ship-flow.js +5 -0
  74. package/scripts/gds/ship-preflight-steps.js +2 -1
  75. package/scripts/gds/ship.js +10 -0
  76. package/scripts/gds/update-sweep.js +24 -8
  77. package/scripts/gds/upgrade-outcome.js +1 -0
  78. package/src/bongos/auth-admission.js +95 -13
  79. package/src/bongos/db-kernel.js +1 -1
  80. package/src/bongos/module-rechecks.js +77 -0
  81. package/src/bongos/route-rank-check.js +3 -0
  82. package/src/bongos/routes/core-update.js +68 -2
  83. package/src/bongos/routes/modules.js +17 -0
  84. package/src/bongos/serve-internal.js +14 -0
  85. package/src/bongos/software-update.js +3 -2
  86. package/src/module-api.js +1 -1
  87. package/tests/activity_rollup_order_db.mjs +3 -1
  88. package/tests/activity_snapshots_db.mjs +2 -0
  89. package/tests/autobongos_grade_cap.mjs +125 -0
  90. package/tests/autobongos_loop.mjs +230 -1
  91. package/tests/autobongos_verify.mjs +239 -3
  92. package/tests/autonomy_config_idle.mjs +272 -0
  93. package/tests/core_update_banner.mjs +29 -0
  94. package/tests/core_upgrade_door.mjs +31 -1
  95. package/tests/core_upgrade_runner.mjs +20 -0
  96. package/tests/discord_craft_broadcast.mjs +162 -0
  97. package/tests/genesis_home.mjs +1 -1
  98. package/tests/government_routes.mjs +3 -1
  99. package/tests/governor_docket.mjs +552 -0
  100. package/tests/hall_audit.mjs +10 -1
  101. package/tests/hall_city.mjs +369 -0
  102. package/tests/hall_page_gate_map.mjs +5 -0
  103. package/tests/hub_craft_rollup.mjs +350 -0
  104. package/tests/onboard.mjs +10 -4
  105. package/tests/platform_boot.mjs +1 -1
  106. package/tests/profile_rollup_consent.mjs +2 -0
  107. package/tests/profile_route.mjs +16 -2
  108. package/tests/projects_hub.mjs +196 -89
  109. package/tests/projects_hub_app_status.mjs +3 -1
  110. package/tests/projects_hub_app_step.mjs +72 -48
  111. package/tests/projects_hub_dns_ready.mjs +10 -2
  112. package/tests/projects_hub_look.mjs +32 -22
  113. package/tests/projects_hub_module_picker.mjs +112 -62
  114. package/tests/projects_hub_pre_uat.mjs +48 -41
  115. package/tests/provision.mjs +78 -0
  116. package/tests/public_landing_projects.mjs +12 -6
  117. package/tests/settings_main_role.mjs +190 -0
  118. package/tests/software_update.mjs +56 -1
  119. package/tests/update_subscription_engine.mjs +55 -3
  120. package/tests/wizard_demo.mjs +37 -11
  121. package/tests/wizard_draft_resume.mjs +34 -22
  122. package/tests/wizard_front_door.mjs +173 -103
  123. package/tests/wizard_intent_resume.mjs +28 -25
  124. package/tests/wizard_physics.mjs +94 -76
  125. package/tests/wizard_physics_more.mjs +428 -277
  126. package/tests/wizard_six_screens.mjs +126 -0
@@ -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
  });
@@ -15,12 +15,13 @@
15
15
  // ruled out the task's other option (drop the record at a terminal state).
16
16
  //
17
17
  // The reset fires only when there is a SECOND DRAFT to lose, and the draft's own step is
18
- // what says so. A landed create clears DRAFT_KEY — but show(12) then persists the done
18
+ // what says so. A landed create clears DRAFT_KEY — but show(6) then persists the done
19
19
  // panel's position straight back into it, so a plain "is there a draft?" test is always
20
20
  // yes and would skip the reset forever, leaving the finished project's name sitting in
21
21
  // the fresh form. (That is exactly what the first cut of this change shipped into a
22
22
  // render: "still watching Mercury" above a name field still reading "Mercury".) Only
23
- // steps 1-11 are wizard steps (1-10 before task 1004421's Look step, 1-8 before task 1004417's front door took 1 and 2);
23
+ // steps 1-5 are wizard steps since task 1004504's six blended screens (1-11 before them, 1-10
24
+ // before task 1004421's Look step, 1-8 before task 1004417's front door took 1 and 2);
24
25
  // anything else is the done panel's own bookkeeping.
25
26
  //
26
27
  // Harness: there is no jsdom in this repo, so this follows tests/wizard_draft_resume.mjs
@@ -58,8 +59,8 @@ test('both slices are still findable (a rename must fail loudly, not silently pa
58
59
  assert.ok(offerSlice, 'paintResumeOffer() was not found in projects.html');
59
60
  assert.match(enterSlice, /if \(focus && !resumeIntent\) \{/, 'the intent branch is part of the slice');
60
61
  assert.match(enterSlice, /startProgress\(created\.id\);/, 'so is the resume branch it guards');
61
- assert.match(enterSlice, /draft\.step >= 1 && draft\.step <= 12/,
62
- 'the STORED draft\'s step decides, not state.step — state.step is 13 for the whole life of a finished standup');
62
+ assert.match(enterSlice, /draft\.step >= 1 && draft\.step <= 5 \?/,
63
+ 'the STORED draft\'s step decides, not state.step — state.step is 6 for the whole life of a finished standup');
63
64
  assert.match(SCRIPT, /if \(created && created\.id\) \{ enterCreated\(created, !!focus\); return; \}/,
64
65
  'enter() still hands the created record straight to it');
65
66
  });
@@ -73,7 +74,7 @@ function runEnter({ focus, resumeIntent = false, draft = null, record }) {
73
74
  const calls = { show: [], resets: 0, offer: [], done: 0, progress: [], polls: 0, focused: 0 };
74
75
  // state.step is the DONE step for the whole life of a finished standup — the value
75
76
  // a draft-blind reading would have trusted
76
- const state = { step: draft ? draft.step : 13, name: 'Mercury', slug: 'mercury', domain: '', mode: 'greenfield', noAddress: false, modules: null };
77
+ const state = { step: draft ? draft.step : 6, name: 'Mercury', slug: 'mercury', domain: '', mode: 'greenfield', noAddress: false, modules: null };
77
78
  const sandbox = {
78
79
  record, focus, resumeIntent, state,
79
80
  DRAFT_KEY: 'draft',
@@ -99,7 +100,7 @@ const REC = { id: 'inst-1', name: 'Acme', slug: 'acme', domain: 'acme.example',
99
100
 
100
101
  test('a reload / Back / identity settle (focus false) still resumes the done panel, watch and all', () => {
101
102
  const { calls, state } = runEnter({ focus: false, record: REC });
102
- assert.deepEqual(calls.show, [[13, false]], 'the done panel, and no focus steal on a non-deliberate arrival');
103
+ assert.deepEqual(calls.show, [[6, false]], 'the done panel, and no focus steal on a non-deliberate arrival');
103
104
  assert.equal(calls.resets, 0, 'nothing is reset — this is the same standup continuing');
104
105
  assert.equal(calls.done, 1, 'the checklist is re-rendered');
105
106
  assert.deepEqual(calls.progress, ['inst-1'], 'the progress watch is restarted from the record');
@@ -114,7 +115,7 @@ test('a deliberate "New project" click after a finished standup opens a FRESH st
114
115
  assert.equal(calls.progress.length, 0, 'and its watch is not restarted');
115
116
  assert.equal(calls.resets, 1, 'with no second draft to lose, the wizard forgets the finished project');
116
117
  assert.deepEqual(calls.show, [[1, false]], 'step 1, and step 1 does not take the heading focus');
117
- assert.equal(calls.focused, 1, 'its one control, the terms tick, does — the deliberate-nav rule enter() already applies');
118
+ assert.equal(calls.focused, 1, 'its first control, the fork\'s terms tick, does — the deliberate-nav rule enter() already applies');
118
119
  assert.equal(state.name, '', 'the old name is gone from the form');
119
120
  });
120
121
 
@@ -127,7 +128,7 @@ test('…and the standup stays one click away, named, on the resume link', () =>
127
128
 
128
129
  test('the resume link gets the done panel back — the record was never cleared', () => {
129
130
  const { calls, sandbox } = runEnter({ focus: true, resumeIntent: true, record: REC });
130
- assert.deepEqual(calls.show, [[13, true]], 'a deliberate resume moves focus to the done panel');
131
+ assert.deepEqual(calls.show, [[6, true]], 'a deliberate resume moves focus to the done panel');
131
132
  assert.deepEqual(calls.progress, ['inst-1'], 'the watch comes back with it');
132
133
  assert.equal(sandbox.resumeIntent, false, 'the flag is ONE-SHOT — the next plain nav click must mean "new" again');
133
134
  assert.deepEqual(calls.offer, [null], 'and the offer comes down behind it');
@@ -137,29 +138,30 @@ test('a second draft already under way is NOT wiped by navigating back to the wi
137
138
  // The regression this guards: with the reset unconditional, typing a name, stepping
138
139
  // away to "Your projects" and clicking "New project" again would blank the draft —
139
140
  // trading the reported bug for a worse one.
140
- const { calls, state } = runEnter({ focus: true, record: REC, draft: { step: 5 } });
141
+ const { calls, state } = runEnter({ focus: true, record: REC, draft: { step: 3 } });
141
142
  assert.equal(calls.resets, 0, 'a live draft is never reset');
142
- assert.deepEqual(calls.show, [[5, true]], 'the second draft resumes where it was left, heading focused');
143
- assert.equal(state.step, 5);
143
+ assert.deepEqual(calls.show, [[3, true]], 'the second draft resumes where it was left, heading focused');
144
+ assert.equal(state.step, 3);
144
145
  assert.deepEqual(calls.offer, ['Acme'], 'the offer stays up beside it');
145
146
  });
146
147
 
147
148
  test('the done panel\'s own saved step is NOT a second draft — it must not block the reset', () => {
148
- // Caught in a render, not in a unit test: show(12) calls saveDraft(), so DRAFT_KEY is
149
+ // Caught in a render, not in a unit test: show(6) calls saveDraft(), so DRAFT_KEY is
149
150
  // non-empty again the moment a create lands. Reading "is there a draft?" left the
150
151
  // finished project's name in the fresh form, under a line offering to resume it.
151
- const { calls, state } = runEnter({ focus: true, record: REC, draft: { step: 13, name: 'Mercury' } });
152
+ const { calls, state } = runEnter({ focus: true, record: REC, draft: { step: 6, name: 'Mercury' } });
152
153
  assert.equal(calls.resets, 1, 'the done step is bookkeeping, not a project being typed');
153
154
  assert.deepEqual(calls.show, [[1, false]], 'out-of-range steps land on step 1, the front door');
154
155
  assert.equal(state.name, '', 'and the finished project\'s name does not survive into it');
155
156
  assert.equal(calls.done, 0, 'never back onto the done panel');
156
157
  });
157
158
 
158
- test('a draft on the app step, the Look step or the review is live too — the last three numbered steps (task 1004351)', () => {
159
- // The app step moved the review from 7 to 8, the front door (task 1004417) to 10 and the
160
- // Look step (task 1004421) to 11. A guard left behind would reset a second project
161
- // sitting on its review, silently wiping it.
162
- for (const step of [10, 11, 12]) {
159
+ test('a draft on the checks, the review or the look is live too — the last three screens (tasks 1004351, 1004504)', () => {
160
+ // Every renumbering has moved the review: 7 to 8 for the app step, 10 for the front door
161
+ // (task 1004417), 11 for the Look step (task 1004421), 4 for the six blended screens
162
+ // (task 1004504). A guard left behind would reset a second project sitting on its
163
+ // review, silently wiping it.
164
+ for (const step of [3, 4, 5]) {
163
165
  const { calls, state } = runEnter({ focus: true, record: REC, draft: { step } });
164
166
  assert.equal(calls.resets, 0, `step ${step} is a live draft`);
165
167
  assert.deepEqual(calls.show, [[step, true]]);
@@ -167,7 +169,7 @@ test('a draft on the app step, the Look step or the review is live too — the l
167
169
  }
168
170
  });
169
171
 
170
- test('a draft on the front door is live too — a founder part-way through it keeps their place (task 1004417)', () => {
172
+ test('a draft on the fork or the first question is live too — a founder part-way through keeps their place (tasks 1004417, 1004504)', () => {
171
173
  for (const step of [1, 2]) {
172
174
  const { calls, state } = runEnter({ focus: true, record: REC, draft: { step } });
173
175
  assert.equal(calls.resets, 0, `step ${step} is a live draft`);
@@ -175,8 +177,8 @@ test('a draft on the front door is live too — a founder part-way through it ke
175
177
  }
176
178
  });
177
179
 
178
- test('a draft whose step is missing or junk resets too — only 1-12 preserves anything', () => {
179
- for (const draft of [{ step: 0 }, { step: 13 }, { name: 'half-typed' }]) {
180
+ test('a draft whose step is missing or junk resets too — only 1-5 preserves anything', () => {
181
+ for (const draft of [{ step: 0 }, { step: 6 }, { name: 'half-typed' }]) {
180
182
  const { calls } = runEnter({ focus: true, record: REC, draft });
181
183
  assert.equal(calls.resets, 1, `step ${JSON.stringify(draft.step)} is not a wizard step`);
182
184
  assert.deepEqual(calls.show, [[1, false]]);
@@ -201,13 +203,14 @@ test('paintResumeOffer names the project by textContent and hides on null (no li
201
203
  });
202
204
 
203
205
  test('the offer ships as real markup inside step 1, hidden until there is something to resume', () => {
204
- // step 1 is the front door's terms screen since task 1004417 — the offer moved with it
205
- const panel = HUB.slice(HUB.indexOf('id="panel-terms"'), HUB.indexOf('id="panel-fork"'));
206
+ // step 1 is the fork since task 1004504 (the terms are its tick) — the offer lives there
207
+ const panel = HUB.slice(HUB.indexOf('id="panel-fork"'), HUB.indexOf('id="panel-what"'));
208
+ assert.ok(HUB.indexOf('id="panel-fork"') > -1 && HUB.indexOf('id="panel-what"') > HUB.indexOf('id="panel-fork"'), 'step 1 is findable');
206
209
  assert.match(panel, /<p class="resumeOffer" id="wizResume" hidden>still watching <strong id="wizResumeName">your project<\/strong> — <a href="#" id="wizResumeGo">resume it<\/a><\/p>/,
207
210
  'baked into the panel (C7), carrying its own hidden state');
208
211
  assert.ok(panel.indexOf('id="wizResume"') < panel.indexOf('<form data-next="2">'),
209
- 'it sits above the terms form, where a visitor reads it before anything else');
210
- assert.doesNotMatch(HUB.slice(HUB.indexOf('id="panel-name"'), HUB.indexOf('id="panel-type"')), /id="wizResume"/,
212
+ 'it sits above the fork\'s form, where a visitor reads it before anything else');
213
+ assert.equal((HUB.match(/id="wizResume"/g) || []).length, 1,
211
214
  'and only once — a second copy would be a duplicate id');
212
215
  assert.match(SCRIPT, /\$\('wizResumeGo'\)\.addEventListener\('click', function \(ev\) \{\n\s*ev\.preventDefault\(\);\n\s*resumeIntent = true;\n\s*enter\(true\);/,
213
216
  'the link raises the one-shot intent and re-enters, rather than href-navigating');