@bongos/core 1.21.91 → 1.21.93

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 (55) hide show
  1. package/.bongos-core.json +72 -62
  2. package/docs/adr/0363-who-decides-what-is-a-table-changed-only-by-amendment.md +1 -0
  3. package/docs/copy-inventory.md +357 -367
  4. package/docs/copy-registry.json +559 -641
  5. package/docs/module-api-changelog.md +9 -0
  6. package/docs/page-inventory.json +15 -18
  7. package/docs/page-readings.json +1235 -918
  8. package/modules/autonomy/routes/autonomy.js +14 -5
  9. package/modules/government/decision-board.js +13 -3
  10. package/modules/government/decision-rules.js +61 -13
  11. package/modules/lifecycle/routes/done-when.js +2 -2
  12. package/modules/lifecycle/routes/gate-approvals.js +1 -1
  13. package/modules/lifecycle/routes/goals.js +4 -4
  14. package/modules/lifecycle/routes/project-settings.js +2 -2
  15. package/modules/lifecycle/routes/tasks.js +1 -1
  16. package/modules/lifecycle/routes/versions.js +1 -1
  17. package/modules/npm-release/routes/release.js +1 -0
  18. package/modules/onboarding/routes/access-requests.js +3 -3
  19. package/modules/provisioning/routes/core-upgrade.js +11 -0
  20. package/modules/public-landing/public/projects-hall-live.states.json +6 -35
  21. package/modules/public-landing/public/projects.html +1019 -983
  22. package/modules/public-landing/public/projects.probes.json +15 -18
  23. package/modules/public-landing/public/projects.states.json +22 -25
  24. package/modules/render-deploy/routes/act.js +1 -1
  25. package/modules/security/routes/reports.js +3 -0
  26. package/package-lock.json +2 -2
  27. package/package.json +1 -1
  28. package/release-notes.json +12 -0
  29. package/scripts/gds/fitness.js +7 -0
  30. package/src/bongos/act-alone.js +84 -0
  31. package/src/bongos/routes/builders.js +6 -1
  32. package/src/module-api.js +7 -1
  33. package/tests/autonomy_fence_priority.mjs +3 -1
  34. package/tests/government_act_alone_buildings.mjs +430 -0
  35. package/tests/module_api.mjs +1 -0
  36. package/tests/npm_release_release.mjs +2 -1
  37. package/tests/onboard.mjs +8 -8
  38. package/tests/project_invite_ui.mjs +6 -5
  39. package/tests/projects_hub.mjs +77 -98
  40. package/tests/projects_hub_app_status.mjs +3 -3
  41. package/tests/projects_hub_app_step.mjs +8 -8
  42. package/tests/projects_hub_cloud_hop.mjs +29 -53
  43. package/tests/projects_hub_look.mjs +29 -39
  44. package/tests/projects_hub_module_picker.mjs +32 -39
  45. package/tests/projects_hub_pre_uat.mjs +11 -14
  46. package/tests/ship_mode.mjs +2 -1
  47. package/tests/wizard_demo.mjs +30 -25
  48. package/tests/wizard_draft_resume.mjs +24 -32
  49. package/tests/wizard_front_door.mjs +91 -243
  50. package/tests/wizard_intent_resume.mjs +28 -48
  51. package/tests/wizard_one_page.mjs +201 -0
  52. package/tests/wizard_physics.mjs +46 -41
  53. package/tests/wizard_physics_more.mjs +71 -106
  54. package/tests/wizard_ship_mode.mjs +1 -1
  55. package/tests/wizard_six_screens.mjs +0 -129
@@ -1,21 +1,16 @@
1
- // tests/wizard_front_door.mjs — the create wizard's front door (task 1004417, BV2.PS05;
2
- // goal 1000121, docs/design/project-startup-direction.md "Front door").
1
+ // tests/wizard_front_door.mjs — the create page's front door (task 1004417, BV2.PS05; reshaped
2
+ // by task 1004648, the one-page flow — docs/design/project-startup-direction.md "One-page flow").
3
3
  //
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.
13
- // Sign-in stays where it was: the gate (step 0) comes first.
4
+ // Sign-in stays the gate in front (step 0). After it there is ONE page (step 1) and, once the
5
+ // project exists, the same page with part 03 live (step 2). The fork screen and its two doors are
6
+ // gone: the demo is a small link under the heading, and the terms are one required tick above
7
+ // Create, recorded on the project against the terms version the SERVER stamps
8
+ // (tests/provisioning_terms_ack.mjs holds that half).
14
9
  //
15
10
  // Most of this RUNS the page's own script, the vm-slice idiom of tests/wizard_draft_resume.mjs:
16
- // validateStep(), the front door's wiring, and show()'s screen bookkeeping are sliced out of the
17
- // inline script and executed under a minimal DOM stub. Every slice boundary is asserted, so a
18
- // rename fails loudly instead of leaving this file quietly testing nothing.
11
+ // the tick's wiring and show()'s bookkeeping are sliced out of the inline script and executed
12
+ // under a minimal DOM stub. Every slice boundary is asserted, so a rename fails loudly instead of
13
+ // leaving this file quietly testing nothing.
19
14
  //
20
15
  // projects.html is CRLF on a Windows checkout; this file normalises at read time.
21
16
  //
@@ -32,238 +27,97 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
32
27
  const LANDING = path.join(ROOT, 'modules', 'public-landing', 'public');
33
28
  const HUB = fs.readFileSync(path.join(LANDING, 'projects.html'), 'utf8').replace(/\r\n/g, '\n');
34
29
  const SCRIPT = HUB.slice(HUB.lastIndexOf('<script>'), HUB.lastIndexOf('</script>'));
35
- const STYLE = HUB.slice(HUB.indexOf('<style>'), HUB.indexOf('</style>'));
36
-
37
- const panel = (id) => (HUB.match(new RegExp(`<section class="panel[^"]*" id="${id}"[\\s\\S]*?\\n {10}</section>`)) || [''])[0];
38
- const FORK = panel('panel-fork');
39
30
  const VIEW_NEW = (HUB.match(/<section id="view-new" hidden>[\s\S]*?\n<\/section>/) || [''])[0];
31
+ const PAGE = (VIEW_NEW.match(/<section class="panel on" id="panel-page"[\s\S]*?\n {10}<\/section>/) || [''])[0];
40
32
  const MAP = {};
41
- for (const m of ((SCRIPT.match(/var panels = \{([^}]*)\}/) || [, ''])[1]).matchAll(/(\d+):\s*'([a-z-]+)'/g)) MAP[m[2]] = Number(m[1]);
33
+ for (const m of ((SCRIPT.match(/var panels = \{([^}]*)\}/) || [, ''])[1]).matchAll(/(\d+):\s*'([a-z-]+)'/g)) MAP[Number(m[1])] = m[2];
42
34
 
43
35
  // ── the order ───────────────────────────────────────────────────────────────────
44
- test('the front door stands between sign-in and the first question: gate 0, fork 1, what 2', () => {
45
- assert.equal(MAP['panel-gate'], 0, 'sign-in stays where it is today, first');
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');
58
- });
59
-
60
- test('the numbered rail is gone (task 1004504) — no steps list, no painter, no rule hiding it', () => {
61
- const beats = [...HUB.matchAll(/<li[^>]*data-step="(\d+)"/g)].map((m) => Number(m[1]));
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');
36
+ test('sign-in stays the gate in front; then the page, which carries the static `on` for a no-script visitor', () => {
37
+ assert.deepEqual(MAP, { 0: 'panel-gate', 1: 'panel-page', 2: 'panel-page' });
38
+ assert.ok(VIEW_NEW.indexOf('id="panel-gate"') < VIEW_NEW.indexOf('id="panel-page"'), 'the markup reads in the same order');
39
+ assert.ok(PAGE, '#panel-page ships as real markup (C7)');
40
+ assert.doesNotMatch(VIEW_NEW, /id="panel-terms"|id="panel-fork"|id="wizStart"|id="wizBack"/, 'no terms screen, no fork, no doors, no Back');
41
+ assert.doesNotMatch(HUB, /<li[^>]*data-step="\d+"|class="steps"/, 'and no progress rail');
66
42
  });
67
43
 
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');
44
+ // ── the tick ────────────────────────────────────────────────────────────────────
45
+ test('the terms are one tick above Create: the whole row is the label, and the terms open in a new tab', () => {
46
+ assert.match(PAGE, /<input type="checkbox" id="termsAck" aria-describedby="h-terms">/, 'one checkbox, named by its hint');
47
+ assert.match(PAGE, /<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>/,
48
+ 'the target is the sentence, not an 18px box — and the platform\'s own /terms, in a new tab so the draft is not left behind');
49
+ assert.ok(PAGE.indexOf('id="termsAck"') < PAGE.indexOf('id="doCreate"'), 'the tick sits above Create');
50
+ assert.ok(PAGE.indexOf('id="part-run"') < PAGE.indexOf('id="termsAck"'), 'and below the summary');
51
+ assert.equal((HUB.match(/id="termsAck"/g) || []).length, 1);
79
52
  });
80
53
 
81
54
  test('the page never names a terms version — the server stamps which text was acknowledged', () => {
82
- // A version baked here would be a second copy of TERMS_VERSION that drifts, and worse, a
83
- // value a caller could send. The create request carries only the tick.
84
55
  assert.doesNotMatch(HUB, /terms_version|termsVersion|terms_ack_version/, 'no version travels from the page');
85
- assert.doesNotMatch(FORK, /\b20\d\d-\d\d-\d\d\b/, 'and none is printed on the screen');
56
+ assert.doesNotMatch(PAGE, /\b20\d\d-\d\d-\d\d\b/, 'and none is printed on the screen');
86
57
  });
87
58
 
88
- test('the resume offer lives on step 1 — the fork, once', () => {
59
+ test('the resume offer lives at the top of the page, once', () => {
89
60
  assert.equal((HUB.match(/id="wizResume"/g) || []).length, 1);
90
- assert.ok(FORK.includes('id="wizResume"'), 'a deliberate "New project" lands here, so the offer does too');
91
- });
92
-
93
- // ── the validator, executed ────────────────────────────────────────────────────
94
- const VALIDATE_RE = /function validateStep\(step\) \{[\s\S]*?\n {4}\}/;
95
- const validateSlice = (SCRIPT.match(VALIDATE_RE) || [null])[0];
96
-
97
- function runValidate(step, termsAck, extra = {}) {
98
- const els = {};
99
- const $ = (id) => (els[id] = els[id] || { textContent: '', className: '', classList: { add() {} }, focus() {} });
100
- const sandbox = { $, state: { termsAck, name: 'Mercury', projType: 'game', demo: null, confirmed: {}, ...extra }, validateRender: () => true, repoAccess: true };
101
- sandbox.globalThis = sandbox;
102
- vm.createContext(sandbox);
103
- vm.runInContext(`${validateSlice}\nvar ok = validateStep(${step});`, sandbox);
104
- return { ok: sandbox.ok, hint: els['h-terms'], state: sandbox.state };
105
- }
106
-
107
- test('validateStep is still findable, with the terms clause inside it', () => {
108
- assert.ok(validateSlice, 'validateStep() was not found in projects.html');
109
- assert.match(validateSlice, /if \(step === 1\) \{\s*if \(!state\.termsAck\)/, 'the clause is part of the slice');
110
- });
111
-
112
- test('Start my project is refused until the box is ticked, and the refusal says why in the hint', () => {
113
- const refused = runValidate(1, false);
114
- assert.equal(refused.ok, false, 'no tick, no way past the fork');
115
- assert.equal(refused.hint.className, 'hint err');
116
- assert.match(refused.hint.textContent, /Tick the box to go on/);
117
- const given = runValidate(1, true);
118
- assert.equal(given.ok, true, 'a tick goes on');
119
- });
120
-
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
123
- // set up and then walked back from must not ride the create
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');
61
+ assert.ok(PAGE.indexOf('id="wizResume"') > -1 && PAGE.indexOf('id="wizResume"') < PAGE.indexOf('id="page-h"'), 'above the heading, where a deliberate "New project" lands');
135
62
  });
136
63
 
137
- // ── the wiring, executed ───────────────────────────────────────────────────────
138
- const WIRE_START = ' function paintDoors() {';
139
- const WIRE_END = ' /* ── the demo route (task 1004420, BV2.PS09) ──';
140
- const wireSlice = SCRIPT.includes(WIRE_START) && SCRIPT.includes(WIRE_END) ? SCRIPT.slice(SCRIPT.indexOf(WIRE_START), SCRIPT.indexOf(WIRE_END)) : null;
64
+ // ── the tick's wiring, executed ─────────────────────────────────────────────────
65
+ const WIRE_START = " $('termsAck').checked = !!state.termsAck;";
66
+ const WIRE_END = ' /* ── the demo route';
67
+ const wireSlice = SCRIPT.includes(WIRE_START) && SCRIPT.indexOf(WIRE_END) > SCRIPT.indexOf(WIRE_START)
68
+ ? SCRIPT.slice(SCRIPT.indexOf(WIRE_START), SCRIPT.indexOf(WIRE_END)) : null;
141
69
 
142
- function runWiring(initialAck) {
143
- const handlers = {};
144
- const mk = (id) => ({
145
- id, checked: false, hidden: false, disabled: true, textContent: '', className: '', attrs: {},
146
- addEventListener(type, fn) { handlers[id + ':' + type] = fn; },
147
- setAttribute(k, v) { this.attrs[k] = v; },
70
+ function runWiring(termsAck) {
71
+ const els = {}, handlers = {};
72
+ let saves = 0, paints = 0;
73
+ const $ = (id) => (els[id] = els[id] || {
74
+ id, checked: false, textContent: '', className: '',
75
+ addEventListener(type, fn) { handlers[`${id}:${type}`] = fn; },
148
76
  });
149
- const els = { termsAck: mk('termsAck'), 'h-terms': mk('h-terms'), wizDemo: mk('wizDemo'), wizStart: mk('wizStart') };
150
- let saves = 0;
151
- const sandbox = { $: (id) => els[id], state: { termsAck: initialAck }, saveDraft: () => { saves += 1; } };
77
+ const sandbox = { $, state: { termsAck }, saveDraft() { saves += 1; }, paintCreate() { paints += 1; } };
152
78
  sandbox.globalThis = sandbox;
153
79
  vm.createContext(sandbox);
154
80
  vm.runInContext(wireSlice, sandbox);
155
- return { els, handlers, sandbox, saves: () => saves };
81
+ return { els, handlers, sandbox, saves: () => saves, paints: () => paints };
156
82
  }
157
83
 
158
- test('the front door\'s wiring is still findable', () => {
84
+ test('the tick\'s wiring is findable', () => {
159
85
  assert.ok(wireSlice && wireSlice.length > 100, 'the wiring block was not found in projects.html');
160
86
  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');
180
87
  });
181
88
 
182
- test('the tick mirrors into the draft both ways, and a resumed draft paints its own answer', () => {
183
- const { els, handlers, sandbox, saves } = runWiring(true);
89
+ test('the tick mirrors into the draft both ways, repaints Create, and a resumed draft paints its own answer', () => {
90
+ const { els, handlers, sandbox, saves, paints } = runWiring(true);
184
91
  assert.equal(els.termsAck.checked, true, 'a draft that ticked comes back ticked (the GitHub round-trip keeps it)');
185
92
  els.termsAck.checked = false; handlers['termsAck:change'].call(els.termsAck);
186
93
  assert.equal(sandbox.state.termsAck, false, 'unticking is an answer too');
187
- els['h-terms'].className = 'hint err';
94
+ sandbox.$('h-terms').className = 'hint err';
188
95
  els.termsAck.checked = true; handlers['termsAck:change'].call(els.termsAck);
189
96
  assert.equal(sandbox.state.termsAck, true);
190
- assert.equal(els['h-terms'].className, 'hint', 'ticking clears the refusal');
97
+ assert.equal(els['h-terms'].className, 'hint', 'ticking clears a refusal');
191
98
  assert.equal(saves(), 2, 'each change is saved');
99
+ assert.equal(paints(), 2, 'and Create repaints with it — it waits for the tick');
100
+ assert.equal(runWiring(false).els.termsAck.checked, false);
192
101
  });
193
102
 
194
- test('the demo door names what it opens: the demo route\'s setup screen (task 1004420; run in tests/wizard_demo.mjs)', () => {
195
- assert.match(FORK, /id="wizDemo" aria-controls="demoSetup" aria-expanded="false"/, 'the door names what it opens');
196
- assert.match(FORK, /<div id="demoSetup" class="demoView" hidden>/, 'baked markup, hidden at rest, never built by script');
197
- assert.doesNotMatch(FORK, /demoSoon|isn’t open yet/, 'the placeholder that said the demo was not open is gone');
198
- });
199
-
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>/);
203
- const pill = (FORK.match(/<div class="doorPill" role="group" aria-label="How to begin">[\s\S]*?<\/div>/) || [''])[0];
204
- assert.ok(pill, 'one grouped pill');
205
- const doors = [...pill.matchAll(/<button type="(button|submit)" class="([^"]+)"[^>]*>([^<]+)<\/button>/g)];
206
- assert.equal(doors.length, 2, 'exactly two doors');
207
- assert.deepEqual(doors.map((d) => d[3]), ['Try the demo', 'Start my project']);
208
- assert.equal(doors[0][2], 'btn big ghost', 'the demo door is the secondary: transparent at rest');
209
- assert.equal(doors[1][2], 'btn big', 'begin carries the accent');
210
- assert.equal(doors[1][1], 'submit', 'and is the form\'s one submit, so Enter goes on');
211
- assert.match(STYLE, /\.doorPill\{display:inline-grid;grid-template-columns:1fr 1fr;/, 'two EQUAL tracks — the two routes are equal ways in');
212
- assert.match(STYLE, /\.doorPill\{[^}]*border-radius:999px;/, 'pill or nothing');
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');
214
- });
215
-
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');
219
- assert.ok(fs.existsSync(path.join(LANDING, 'assets', 'world', 'r2-chrome-blob-one-cut.webp')), 'and it exists on disk');
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');
235
- });
236
-
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', () => {
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');
243
- });
244
-
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.
103
+ // ── show(), executed ────────────────────────────────────────────────────────────
248
104
  const SHOW_START = ' var SCREEN = {';
249
105
  const SHOW_RE = /function show\(step, moveFocus\) \{[\s\S]*?\n {4}\}/;
250
106
  const showMatch = SCRIPT.match(SHOW_RE);
251
107
  const showSlice = showMatch && SCRIPT.includes(SHOW_START) && SCRIPT.indexOf(SHOW_START) < showMatch.index
252
108
  ? SCRIPT.slice(SCRIPT.indexOf(SHOW_START), showMatch.index + showMatch[0].length) : null;
253
109
 
254
- function runShow(step, { demo = null, demoOpen = false } = {}) {
255
- const cls = (name) => { const set = new Set(); return { name, set, toggle(c, on) { if (on) set.add(c); else set.delete(c); } }; };
110
+ function runShow(step, { demo = null } = {}) {
111
+ const cls = () => { const set = new Set(); return { set, toggle(c, on) { if (on) set.add(c); else set.delete(c); } }; };
256
112
  const els = {};
257
113
  const $ = (id) => (els[id] = els[id] || {
258
- id, classList: cls(id), hidden: id === 'demoSetup' ? !demoOpen : false, textContent: '', attrs: {},
114
+ id, classList: cls(), hidden: false, textContent: '', attrs: {},
259
115
  setAttribute(k, v) { this.attrs[k] = v; }, querySelector: () => null,
260
116
  });
117
+ let painted = '';
261
118
  const sandbox = {
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() {},
265
- renderModules() {}, syncHome() {}, ensureRepoAccess() {}, checkSlugAvailable() {}, renderAddrPanel() {},
266
- syncAppStep() {}, syncRenderSetup() {}, paintSetup() {}, renderLook() {}, renderRecap() {},
119
+ $, state: { demo }, panels: MAP, saveDraft() {},
120
+ paintPage() { painted = 'page'; }, paintCreate() { painted = 'create'; },
267
121
  };
268
122
  sandbox.globalThis = sandbox;
269
123
  vm.createContext(sandbox);
@@ -271,36 +125,34 @@ function runShow(step, { demo = null, demoOpen = false } = {}) {
271
125
  return {
272
126
  on: Object.keys(els).filter((id) => els[id].classList.set.has('on')),
273
127
  screen: els.wizRoom.attrs['data-screen'], eyebrow: els.wizEyebrow.textContent,
274
- back: !els.wizBack.hidden, tag: !els.demoTag.hidden,
128
+ parts: !els.wizParts.hidden, tag: !els.demoTag.hidden, painted,
129
+ done: [...els['panel-done'].classList.set].sort(),
275
130
  };
276
131
  }
277
132
 
278
- test('show() turns one screen on, tells the room which screen it is, and names it in the eyebrow', () => {
133
+ test('show() turns the gate or the page on, tells the room where it is, and names it in the eyebrow', () => {
279
134
  assert.ok(showSlice, 'the SCREEN map through show() was not found in projects.html');
280
135
  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');
136
+ const gate = runShow(0);
137
+ assert.deepEqual(gate.on, ['panel-gate'], 'signed out: only the gate');
138
+ assert.equal(gate.screen, 'signin');
139
+ assert.equal(gate.eyebrow, 'Sign in');
140
+ const page = runShow(1);
141
+ assert.deepEqual(page.on, ['panel-page'], 'the page, and part 03 still waiting');
142
+ assert.equal(page.screen, 'page');
143
+ assert.equal(page.eyebrow, 'New project');
144
+ assert.equal(page.parts, true, 'parts 01 and 02 on show');
145
+ assert.deepEqual(page.done, ['waiting'], 'part 03 waits until the project exists');
146
+ assert.equal(page.painted, 'page', 'the page paints every control on entry');
147
+ const made = runShow(2);
148
+ assert.deepEqual(made.on.sort(), ['panel-done', 'panel-page'], 'created: the same page, part 03 live');
149
+ assert.deepEqual(made.done, ['live', 'on']);
150
+ assert.equal(made.parts, false, 'parts 01 and 02 stand down behind the recap');
151
+ assert.equal(made.screen, 'done');
152
+ assert.equal(made.eyebrow, 'Founding');
153
+ assert.equal(runShow(1, { demo: { people: 3 } }).eyebrow, 'New demo', 'a demo is marked');
154
+ assert.equal(runShow(1, { demo: { people: 3 } }).tag, true, 'and its heading carries the demo tag');
155
+ assert.equal(runShow(1).tag, false, 'a real project carries none');
304
156
  });
305
157
 
306
158
  // ── the create ──────────────────────────────────────────────────────────────────
@@ -308,34 +160,30 @@ test('the create carries the tick, and is never filed without it', () => {
308
160
  const create = SCRIPT.slice(SCRIPT.indexOf("$('doCreate').addEventListener('click'"));
309
161
  assert.match(create, /terms_acknowledged: state\.termsAck \? true : undefined,/,
310
162
  'a real boolean when ticked, absent otherwise — the route refuses anything but a boolean');
311
- const guard = create.indexOf('if (!state.termsAck) { show(1, true); validateStep(1); return; }');
312
- assert.ok(guard > -1, 'an unticked draft is sent back to the fork\'s tick');
163
+ const guard = create.indexOf('if (createWhy()) {');
164
+ assert.ok(guard > -1, 'an unticked page is refused, with its reason said in place');
313
165
  assert.ok(guard < create.indexOf('ensureFreshRepo()'), 'BEFORE anything is created — the repo included');
166
+ assert.match(SCRIPT, /if \(!state\.termsAck\) return 'Tick the terms to create your project\.';/);
314
167
  });
315
168
 
316
169
  test('the tick is part of the draft and of the reset, and the draft key moved with the renumbering', () => {
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');
170
+ assert.match(SCRIPT, /var state = \{ step: 0,[^}]*termsAck: false \};/, 'the initial state declares it');
171
+ assert.match(SCRIPT, /state = \{ step: 1,[^}]*termsAck: false \};/, 'and "start another project" clears it — each project records its own');
172
+ assert.match(SCRIPT, /\$\('termsAck'\)\.checked = false; demoShow\(false\); checksShow\('main'\);/, 'and the reset unticks the box');
173
+ assert.match(SCRIPT, /var DRAFT_KEY = 'cb-create-draft-v10';/, 'a draft saved under the old numbering is ignored, not replayed');
321
174
  });
322
175
 
323
176
  // ── the look-before-you-ship kit walks it ─────────────────────────────────────
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', () => {
177
+ test('every kit walk that creates a project ticks the terms before it presses Create', () => {
327
178
  const states = JSON.parse(fs.readFileSync(path.join(LANDING, 'projects.states.json'), 'utf8')).states;
328
179
  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'];
180
+ for (const sel of ['#panel-page', '#termsAck', '#doCreate']) assert.ok((states['in-new'].expect.visible || []).includes(sel), `arriving at ?view=new pictures the page (${sel})`);
332
181
  const probes = JSON.parse(fs.readFileSync(path.join(LANDING, 'projects.probes.json'), 'utf8')).probes;
333
182
  const walks = [...Object.entries(states).map(([k, v]) => [k, v.actions || []]), ...probes.map((p) => [p.name, p.steps])]
334
- .filter(([, steps]) => steps.some((st) => st[1] === '#newName'));
335
- assert.ok(walks.length >= 12, `every wizard walk is found (${walks.length})`);
183
+ .filter(([, steps]) => steps.some((st) => st[0] === 'click' && st[1] === '#doCreate'));
184
+ assert.ok(walks.length >= 2, `every creating walk is found (${walks.length})`);
336
185
  for (const [name, steps] of walks) {
337
186
  const clicks = steps.filter((st) => st[0] === 'click').map((st) => st[1]);
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`);
187
+ assert.ok(clicks.indexOf('#termsAck') > -1 && clicks.indexOf('#termsAck') < clicks.indexOf('#doCreate'), `${name}: the tick comes before Create`);
340
188
  }
341
189
  });
@@ -59,8 +59,8 @@ test('both slices are still findable (a rename must fail loudly, not silently pa
59
59
  assert.ok(offerSlice, 'paintResumeOffer() was not found in projects.html');
60
60
  assert.match(enterSlice, /if \(focus && !resumeIntent\) \{/, 'the intent branch is part of the slice');
61
61
  assert.match(enterSlice, /startProgress\(created\.id\);/, 'so is the resume branch it guards');
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');
62
+ assert.match(enterSlice, /if \(!\(draft && draft\.step === 1\)\) resetForNewProject\(\);/,
63
+ 'the STORED draft\'s step decides, not state.step — state.step is 2 for the whole life of a finished standup');
64
64
  // since task 1004549 a second draft in flight keeps its place on a non-deliberate
65
65
  // arrival (tests/wizard_draft_resume.mjs slice 4); a deliberate nav still comes here
66
66
  assert.match(SCRIPT, /if \(created && created\.id && \(focus \|\| !draftInFlight \|\| resumeIntent\)\) \{ enterCreated\(created, !!focus\); return; \}/,
@@ -76,7 +76,7 @@ function runEnter({ focus, resumeIntent = false, draft = null, record, handbackS
76
76
  const calls = { show: [], resets: 0, offer: [], done: 0, progress: [], polls: 0, focused: 0, remembered: [] };
77
77
  // state.step is the DONE step for the whole life of a finished standup — the value
78
78
  // a draft-blind reading would have trusted
79
- const state = { step: draft ? draft.step : 6, name: 'Mercury', slug: 'mercury', domain: '', mode: 'greenfield', noAddress: false, modules: null };
79
+ const state = { step: draft ? draft.step : 2, name: 'Mercury', slug: 'mercury', domain: '', mode: 'greenfield', noAddress: false, modules: null };
80
80
  const sandbox = {
81
81
  record, focus, resumeIntent, state,
82
82
  DRAFT_KEY: 'draft',
@@ -104,7 +104,7 @@ const REC = { id: 'inst-1', name: 'Acme', slug: 'acme', domain: 'acme.example',
104
104
 
105
105
  test('a reload / Back / identity settle (focus false) still resumes the done panel, watch and all', () => {
106
106
  const { calls, state } = runEnter({ focus: false, record: REC });
107
- assert.deepEqual(calls.show, [[6, false]], 'the done panel, and no focus steal on a non-deliberate arrival');
107
+ assert.deepEqual(calls.show, [[2, false]], 'the created page, part 03 live, and no focus steal on a non-deliberate arrival');
108
108
  assert.equal(calls.resets, 0, 'nothing is reset — this is the same standup continuing');
109
109
  assert.equal(calls.done, 1, 'the checklist is re-rendered');
110
110
  assert.deepEqual(calls.progress, ['inst-1'], 'the progress watch is restarted from the record');
@@ -115,7 +115,7 @@ test('a reload / Back / identity settle (focus false) still resumes the done pan
115
115
 
116
116
  test('a hand-back from the GitHub callback page resumes watching THAT attempt (task 1004548)', () => {
117
117
  const { calls, sandbox } = runEnter({ focus: false, record: { ...REC }, handbackState: 'ab'.repeat(32) });
118
- assert.deepEqual(calls.show, [[6, false]], 'the founder lands on their checklist');
118
+ assert.deepEqual(calls.show, [[2, false]], 'the founder lands on their checklist');
119
119
  assert.equal(sandbox.manifestState, 'ab'.repeat(32), 'the sign-in step polls the attempt GitHub just approved');
120
120
  assert.equal(sandbox.manifestPending, true, 'and the poll is queued for resumePolls()');
121
121
  assert.equal(sandbox.live.manifest, 'code_received', 'it shows the wait, not "Set up GitHub sign-in" again');
@@ -126,13 +126,13 @@ test('a hand-back from the GitHub callback page resumes watching THAT attempt (t
126
126
  assert.equal(done.sandbox.manifestPending, false, 'and nothing is polled for it');
127
127
  });
128
128
 
129
- test('a deliberate "New project" click after a finished standup opens a FRESH step 1', () => {
129
+ test('a deliberate "New project" click after a finished standup opens a FRESH page', () => {
130
130
  const { calls, state } = runEnter({ focus: true, record: REC });
131
131
  assert.equal(calls.done, 0, 'the previous project\'s done panel is not painted');
132
132
  assert.equal(calls.progress.length, 0, 'and its watch is not restarted');
133
133
  assert.equal(calls.resets, 1, 'with no second draft to lose, the wizard forgets the finished project');
134
- assert.deepEqual(calls.show, [[1, false]], 'step 1, and step 1 does not take the heading focus');
135
- assert.equal(calls.focused, 1, 'its first control, the fork\'s terms tick, does — the deliberate-nav rule enter() already applies');
134
+ assert.deepEqual(calls.show, [[1, false]], 'the page, and it does not take the heading focus');
135
+ assert.equal(calls.focused, 1, 'its first control, the name, does — the deliberate-nav rule enter() already applies');
136
136
  assert.equal(state.name, '', 'the old name is gone from the form');
137
137
  });
138
138
 
@@ -145,7 +145,7 @@ test('…and the standup stays one click away, named, on the resume link', () =>
145
145
 
146
146
  test('the resume link gets the done panel back — the record was never cleared', () => {
147
147
  const { calls, sandbox } = runEnter({ focus: true, resumeIntent: true, record: REC });
148
- assert.deepEqual(calls.show, [[6, true]], 'a deliberate resume moves focus to the done panel');
148
+ assert.deepEqual(calls.show, [[2, true]], 'a deliberate resume moves focus to part 03');
149
149
  assert.deepEqual(calls.progress, ['inst-1'], 'the watch comes back with it');
150
150
  assert.equal(sandbox.resumeIntent, false, 'the flag is ONE-SHOT — the next plain nav click must mean "new" again');
151
151
  assert.deepEqual(calls.offer, [null], 'and the offer comes down behind it');
@@ -155,47 +155,27 @@ test('a second draft already under way is NOT wiped by navigating back to the wi
155
155
  // The regression this guards: with the reset unconditional, typing a name, stepping
156
156
  // away to "Your projects" and clicking "New project" again would blank the draft —
157
157
  // trading the reported bug for a worse one.
158
- const { calls, state } = runEnter({ focus: true, record: REC, draft: { step: 3 } });
158
+ const { calls, state } = runEnter({ focus: true, record: REC, draft: { step: 1 } });
159
159
  assert.equal(calls.resets, 0, 'a live draft is never reset');
160
- assert.deepEqual(calls.show, [[3, true]], 'the second draft resumes where it was left, heading focused');
161
- assert.equal(state.step, 3);
160
+ assert.deepEqual(calls.show, [[1, false]], 'the second draft resumes on the page');
161
+ assert.equal(state.step, 1);
162
+ assert.equal(state.name, 'Mercury', 'with what was typed');
162
163
  assert.deepEqual(calls.offer, ['Acme'], 'the offer stays up beside it');
163
164
  });
164
165
 
165
- test('the done panel\'s own saved step is NOT a second draft — it must not block the reset', () => {
166
- // Caught in a render, not in a unit test: show(6) calls saveDraft(), so DRAFT_KEY is
166
+ test('the created page\'s own saved step is NOT a second draft — it must not block the reset', () => {
167
+ // Caught in a render, not in a unit test: show(2) calls saveDraft(), so DRAFT_KEY is
167
168
  // non-empty again the moment a create lands. Reading "is there a draft?" left the
168
169
  // finished project's name in the fresh form, under a line offering to resume it.
169
- const { calls, state } = runEnter({ focus: true, record: REC, draft: { step: 6, name: 'Mercury' } });
170
- assert.equal(calls.resets, 1, 'the done step is bookkeeping, not a project being typed');
171
- assert.deepEqual(calls.show, [[1, false]], 'out-of-range steps land on step 1, the front door');
170
+ const { calls, state } = runEnter({ focus: true, record: REC, draft: { step: 2, name: 'Mercury' } });
171
+ assert.equal(calls.resets, 1, 'the created step is bookkeeping, not a project being typed');
172
+ assert.deepEqual(calls.show, [[1, false]], 'a fresh page');
172
173
  assert.equal(state.name, '', 'and the finished project\'s name does not survive into it');
173
- assert.equal(calls.done, 0, 'never back onto the done panel');
174
+ assert.equal(calls.done, 0, 'never back onto the finished project\'s part 03');
174
175
  });
175
176
 
176
- test('a draft on the checks, the review or the look is live too — the last three screens (tasks 1004351, 1004504)', () => {
177
- // Every renumbering has moved the review: 7 to 8 for the app step, 10 for the front door
178
- // (task 1004417), 11 for the Look step (task 1004421), 4 for the six blended screens
179
- // (task 1004504). A guard left behind would reset a second project sitting on its
180
- // review, silently wiping it.
181
- for (const step of [3, 4, 5]) {
182
- const { calls, state } = runEnter({ focus: true, record: REC, draft: { step } });
183
- assert.equal(calls.resets, 0, `step ${step} is a live draft`);
184
- assert.deepEqual(calls.show, [[step, true]]);
185
- assert.equal(state.step, step);
186
- }
187
- });
188
-
189
- test('a draft on the fork or the first question is live too — a founder part-way through keeps their place (tasks 1004417, 1004504)', () => {
190
- for (const step of [1, 2]) {
191
- const { calls, state } = runEnter({ focus: true, record: REC, draft: { step } });
192
- assert.equal(calls.resets, 0, `step ${step} is a live draft`);
193
- assert.equal(state.step, step);
194
- }
195
- });
196
-
197
- test('a draft whose step is missing or junk resets too — only 1-5 preserves anything', () => {
198
- for (const draft of [{ step: 0 }, { step: 6 }, { name: 'half-typed' }]) {
177
+ test('a draft whose step is missing or junk resets too — only the page (step 1) preserves anything', () => {
178
+ for (const draft of [{ step: 0 }, { step: 2 }, { step: 6 }, { name: 'half-typed' }]) {
199
179
  const { calls } = runEnter({ focus: true, record: REC, draft });
200
180
  assert.equal(calls.resets, 1, `step ${JSON.stringify(draft.step)} is not a wizard step`);
201
181
  assert.deepEqual(calls.show, [[1, false]]);
@@ -219,14 +199,14 @@ test('paintResumeOffer names the project by textContent and hides on null (no li
219
199
  assert.doesNotMatch(offerSlice, /innerHTML/, 'this surface builds no live DOM from strings');
220
200
  });
221
201
 
222
- test('the offer ships as real markup inside step 1, hidden until there is something to resume', () => {
223
- // step 1 is the fork since task 1004504 (the terms are its tick) — the offer lives there
224
- const panel = HUB.slice(HUB.indexOf('id="panel-fork"'), HUB.indexOf('id="panel-what"'));
225
- assert.ok(HUB.indexOf('id="panel-fork"') > -1 && HUB.indexOf('id="panel-what"') > HUB.indexOf('id="panel-fork"'), 'step 1 is findable');
202
+ test('the offer ships as real markup at the top of the page, hidden until there is something to resume', () => {
203
+ // the page is step 1 since task 1004648 — the offer sits above its heading
204
+ const panel = HUB.slice(HUB.indexOf('id="panel-page"'), HUB.indexOf('id="part-project"'));
205
+ assert.ok(HUB.indexOf('id="panel-page"') > -1 && HUB.indexOf('id="part-project"') > HUB.indexOf('id="panel-page"'), 'the page is findable');
226
206
  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>/,
227
- 'baked into the panel (C7), carrying its own hidden state');
228
- assert.ok(panel.indexOf('id="wizResume"') < panel.indexOf('<form data-next="2">'),
229
- 'it sits above the fork\'s form, where a visitor reads it before anything else');
207
+ 'baked into the page (C7), carrying its own hidden state');
208
+ assert.ok(panel.indexOf('id="wizResume"') < panel.indexOf('id="page-h"'),
209
+ 'it sits above the heading, where a visitor reads it before anything else');
230
210
  assert.equal((HUB.match(/id="wizResume"/g) || []).length, 1,
231
211
  'and only once — a second copy would be a duplicate id');
232
212
  assert.match(SCRIPT, /\$\('wizResumeGo'\)\.addEventListener\('click', function \(ev\) \{\n\s*ev\.preventDefault\(\);\n\s*resumeIntent = true;\n\s*enter\(true\);/,