@bongos/core 1.20.74 → 1.20.76

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 (45) hide show
  1. package/.bongos-core.json +68 -48
  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 +4 -0
  6. package/clients/bongos-client/index.mjs +2 -0
  7. package/docs/api/openapi.json +79 -3
  8. package/docs/api-reference.md +3 -2
  9. package/docs/copy-inventory.md +482 -409
  10. package/docs/copy-registry.json +1230 -560
  11. package/docs/module-api-changelog.md +4 -0
  12. package/docs/page-inventory.json +9 -1
  13. package/docs/page-readings.json +1189 -765
  14. package/modules/hall-ui/public/profile-roles.js +170 -0
  15. package/modules/hall-ui/public/profile.css +42 -5
  16. package/modules/hall-ui/public/profile.html +12 -5
  17. package/modules/hall-ui/public/profile.js +66 -27
  18. package/modules/hall-ui/public/profile.states.json +39 -1
  19. package/modules/hall-ui/records/profile-roles.md +32 -0
  20. package/modules/provisioning/routes/provisioning.js +35 -0
  21. package/modules/public-landing/public/assets/cosmos.css +1 -1
  22. package/modules/public-landing/public/projects.html +589 -63
  23. package/modules/public-landing/public/projects.probes.json +8 -5
  24. package/modules/public-landing/public/projects.states.json +19 -14
  25. package/modules/ui-design/kit/lib.js +4 -2
  26. package/modules/ui-design/kit/serve.js +5 -0
  27. package/package-lock.json +2 -2
  28. package/package.json +1 -1
  29. package/release-notes.json +12 -0
  30. package/src/module-api.js +1 -1
  31. package/tests/founding_builders.mjs +2 -1
  32. package/tests/helpers.mjs +4 -0
  33. package/tests/profile_role_headline.mjs +201 -0
  34. package/tests/profile_ui_cross_project.mjs +3 -0
  35. package/tests/projects_hub.mjs +9 -5
  36. package/tests/projects_hub_app_status.mjs +1 -1
  37. package/tests/projects_hub_app_step.mjs +10 -10
  38. package/tests/projects_hub_look.mjs +7 -7
  39. package/tests/projects_hub_module_picker.mjs +9 -9
  40. package/tests/projects_hub_pre_uat.mjs +2 -1
  41. package/tests/wizard_demo.mjs +1 -1
  42. package/tests/wizard_draft_resume.mjs +1 -1
  43. package/tests/wizard_front_door.mjs +11 -7
  44. package/tests/wizard_intent_resume.mjs +9 -9
  45. package/tests/wizard_physics.mjs +264 -0
@@ -696,7 +696,10 @@ test('the About step is OFFERED by every form and can be walked past — the ski
696
696
  test('the About answers travel on the create request, and an empty one is OMITTED not blanked', () => {
697
697
  assert.match(SCRIPT, /description: String\(state\.description \|\| ''\)\.trim\(\) \|\| undefined/,
698
698
  'a skipped description is absent from the body — an empty string would read as answered');
699
- assert.match(SCRIPT, /detail: state\.teamShape \? \{ team_shape: state\.teamShape \} : undefined/,
699
+ // task 1004418: createDetail() builds the detail, adding the team shape only when it
700
+ // was answered and omitting the whole detail when nothing was
701
+ assert.match(SCRIPT, /detail: createDetail\(\),/);
702
+ assert.match(SCRIPT, /if \(state\.teamShape\) d\.team_shape = state\.teamShape;\r?\n\s*return Object\.keys\(d\)\.length \? d : undefined;/,
700
703
  'and an unanswered team shape is never guessed into the request');
701
704
  // the review is the last screen before Create, so the skip's cost is restated there
702
705
  const review = (SCRIPT.match(/function renderReview\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
@@ -716,8 +719,9 @@ test('a renumbering of the wizard invalidates old drafts — the key carries the
716
719
  const count = [...panels.matchAll(/\d+:\s*'panel-/g)].length;
717
720
  // task 1004417 put the front door's two screens (terms, fork) in front: the sixth renumbering
718
721
  // task 1004421 put the Look step between App and Review: the seventh
719
- assert.equal(count, 13, 'gate + the front door\'s two + nine numbered steps + done — bump DRAFT_KEY when this changes');
720
- assert.equal(key, 'cb-create-draft-v7', 'and the key moved with the Look step');
722
+ // task 1004418 put planet physics right after the fork: the eighth
723
+ assert.equal(count, 14, 'gate + the front door\'s two + ten numbered steps + done — bump DRAFT_KEY when this changes');
724
+ assert.equal(key, 'cb-create-draft-v8', 'and the key moved with the physics step');
721
725
  });
722
726
 
723
727
  test('every row lays the same five parts — the ledger has no ragged row', () => {
@@ -2138,7 +2142,7 @@ test('the wizard takes the world by scope: no card ground, the six-step strip as
2138
2142
  assert.match(block, /#view-new \.card\{background:none;border-radius:0;box-shadow:none;/, 'no card ground');
2139
2143
  // eight tracks since task 1004351 added the App step (seven since 1002339); the count is the
2140
2144
  // rail's own, so it moves with the panels map rather than being a magic number
2141
- assert.match(block, /#view-new \.steps\{display:grid;grid-template-columns:repeat\(9,1fr\);/, 'the strip lays a track per numbered step');
2145
+ assert.match(block, /#view-new \.steps\{display:grid;grid-template-columns:repeat\(10,1fr\);/, 'the strip lays a track per numbered step');
2142
2146
  assert.match(block, /#view-new \.steps li::before\{content:"";position:absolute;top:-4px;left:0;right:auto;width:7px;height:7px;/, 'the connector becomes the 7px dot');
2143
2147
  assert.match(block, /#view-new \.steps li\.now::before\{background:var\(--accent\);border-color:var\(--accent\);outline:4px solid/, 'the lit step carries the accent and an outline halo');
2144
2148
  assert.match(block, /#view-new \.panel h1\{font-weight:300;[^}]*text-transform:lowercase;/, 'the step title in the display voice, lowercase');
@@ -2185,7 +2189,7 @@ test('the wizard\'s own structures wear the world: hairline fields, cells of one
2185
2189
  assert.match(block, /\.addrBig code\{display:block;font-family:var\(--font-display\);/, 'the address stands in the display voice');
2186
2190
  assert.doesNotMatch(block, /linear-gradient/, 'no tinted plates');
2187
2191
  assert.doesNotMatch(block, /box-shadow:(?!none)/, 'no box shadow in the wizard block');
2188
- assert.match(HUB, /<div class="nav">\s*<button type="button" class="btn ghost" data-back="10">← Back<\/button>\s*<button type="button" class="btn big" id="doCreate">Create my project<\/button>/, 'the review\'s doors sit on one row, back then the primary');
2192
+ assert.match(HUB, /<div class="nav">\s*<button type="button" class="btn ghost" data-back="11">← Back<\/button>\s*<button type="button" class="btn big" id="doCreate">Create my project<\/button>/, 'the review\'s doors sit on one row, back then the primary');
2189
2193
  assert.match(HUB, /<h2 class="stepsHead" id="doneStepsHead">Your steps — the parts only you can play<\/h2>/, 'the steps heading is the label face by class, not an inline style (the id is where a skipped invite step hands focus on, task 1002346)');
2190
2194
  // what is enqueued is unchanged (ADR 0111): the filing body the control plane reads
2191
2195
  assert.match(SCRIPT, /body: JSON\.stringify\(\{\s*slug: state\.slug,/, 'the filing still carries the slug first');
@@ -240,7 +240,7 @@ test('a repaint never lands on top of an open Retry — from this watch or a fre
240
240
  });
241
241
 
242
242
  test('both surfaces use it: the done panel and the project page', () => {
243
- assert.match(HUB, /if \(box\) watchApp\(instanceId, box, function \(\) \{ return state\.step === 12; \}, 5000\);/);
243
+ assert.match(HUB, /if \(box\) watchApp\(instanceId, box, function \(\) \{ return state\.step === 13; \}, 5000\);/);
244
244
  assert.match(HUB, /<div id="doneApp" hidden><\/div>/);
245
245
  assert.match(HUB, /<div class="card mSec" id="mAppCard" hidden>\n\s*<h2>Where its app runs<\/h2>\n\s*<div id="mApp"><\/div>/);
246
246
  assert.match(HUB, /loadAppStatus\(inst\.id \|\| asked, inst\);/);
@@ -144,18 +144,18 @@ test('the panel is real markup: Render and Decide later, Decide later the defaul
144
144
  assert.doesNotMatch(PANEL, /Coming soon/, 'and is no longer marked soon');
145
145
  assert.match(PANEL, /<input type="radio" name="appPlatform" value="later" checked>/, 'Decide later is the default');
146
146
  assert.match(PANEL, /We host your Builders' hall/, 'the step says the hall is ours to host (ADR 0345 D1), in the page\'s one spelling of it');
147
- assert.match(PANEL, /<form data-next="10">/, 'Next leads to the Look step (task 1004421), then the review');
148
- assert.match(PANEL, /data-back="8"/, 'Back returns to the address step');
147
+ assert.match(PANEL, /<form data-next="11">/, 'Next leads to the Look step (task 1004421), then the review');
148
+ assert.match(PANEL, /data-back="9"/, 'Back returns to the address step');
149
149
  });
150
150
 
151
151
  test('the step sits between Address and the Look step in the panels map and on the rail', () => {
152
- assert.match(HUB, /8: 'panel-address', 9: 'panel-app', 10: 'panel-look', 11: 'panel-review', 12: 'panel-done'/);
153
- assert.match(HUB, /<li data-step="9"><span class="snum">7<\/span><span class="slbl">App<\/span><\/li>/);
154
- assert.match(HUB, /<li data-step="10"><span class="snum">8<\/span><span class="slbl">Look<\/span><\/li>/);
155
- assert.match(HUB, /<li data-step="11"><span class="snum">9<\/span><span class="slbl">Create<\/span><\/li>/);
156
- assert.match(HUB, /data-back="10">← Back<\/button>\n\s*<button type="button" class="btn big" id="doCreate">/, "the review's Back returns to the Look step");
157
- assert.match(HUB, /if \(step === 9\) \{ syncAppStep\(\); syncRenderSetup\(\); \}\n\s*if \(step === 10\) renderLook\(\);\n\s*if \(step === 11\) renderReview\(\);/, 'entering the step repaints it, Render setup included');
158
- assert.match(HUB, /if \(step === 9\) return validateRender\(\);/, 'Next on the step runs the Render check');
152
+ assert.match(HUB, /9: 'panel-address', 10: 'panel-app', 11: 'panel-look', 12: 'panel-review', 13: 'panel-done'/);
153
+ assert.match(HUB, /<li data-step="10"><span class="snum">8<\/span><span class="slbl">App<\/span><\/li>/);
154
+ assert.match(HUB, /<li data-step="11"><span class="snum">9<\/span><span class="slbl">Look<\/span><\/li>/);
155
+ assert.match(HUB, /<li data-step="12"><span class="snum">10<\/span><span class="slbl">Create<\/span><\/li>/);
156
+ assert.match(HUB, /data-back="11">← Back<\/button>\r?\n\s*<button type="button" class="btn big" id="doCreate">/, "the review's Back returns to the Look step");
157
+ assert.match(HUB, /if \(step === 10\) \{ syncAppStep\(\); syncRenderSetup\(\); \}\r?\n\s*if \(step === 11\) renderLook\(\);\r?\n\s*if \(step === 12\) renderReview\(\);/, 'entering the step repaints it, Render setup included');
158
+ assert.match(HUB, /if \(step === 10\) return validateRender\(\);/, 'Next on the step runs the Render check');
159
159
  });
160
160
 
161
161
  test('a fresh draft reads "decide later", and the Render setup stays hidden', () => {
@@ -489,5 +489,5 @@ test('the review names exactly what was chosen, prices included', () => {
489
489
  });
490
490
 
491
491
  test('Create hands the app to Render straight after the project is filed', () => {
492
- assert.match(HUB, /show\(12, true\);\n\s*sendRenderSetup\(data\.instance && data\.instance\.id\);/);
492
+ assert.match(HUB, /show\(13, true\);\r?\n\s*sendRenderSetup\(data\.instance && data\.instance\.id\);/);
493
493
  });
@@ -106,17 +106,17 @@ test('every colour the previews paint is its pack\'s, and the night shares are t
106
106
  });
107
107
 
108
108
  test('the step sits between App and Review: look 10, review 11, done 12, draft v7, a beat on the rail', () => {
109
- assert.match(HUB, /9: 'panel-app', 10: 'panel-look', 11: 'panel-review', 12: 'panel-done' \};/);
110
- assert.match(HUB, /var DRAFT_KEY = 'cb-create-draft-v7';/);
111
- assert.match(HUB, /<li data-step="10"><span class="snum">8<\/span><span class="slbl">Look<\/span><\/li>\n\s*<li data-step="11"><span class="snum">9<\/span><span class="slbl">Create<\/span><\/li>/);
109
+ assert.match(HUB, /10: 'panel-app', 11: 'panel-look', 12: 'panel-review', 13: 'panel-done' \};/);
110
+ assert.match(HUB, /var DRAFT_KEY = 'cb-create-draft-v8';/);
111
+ assert.match(HUB, /<li data-step="11"><span class="snum">9<\/span><span class="slbl">Look<\/span><\/li>\r?\n\s*<li data-step="12"><span class="snum">10<\/span><span class="slbl">Create<\/span><\/li>/);
112
112
  const panel = (HUB.match(/<section class="panel" id="panel-look"[\s\S]*?<\/section>/) || [''])[0];
113
113
  assert.match(panel, /<h1 id="look-h">Give it a look<\/h1>/);
114
- assert.match(panel, /<form data-next="11">/);
115
- assert.match(panel, /data-back="9"/);
114
+ assert.match(panel, /<form data-next="12">/);
115
+ assert.match(panel, /data-back="10"/);
116
116
  assert.match(panel, /<div class="lookPick" id="wizLook"><\/div>/);
117
117
  assert.ok(HUB.indexOf('id="panel-app"') < HUB.indexOf('id="panel-look"') && HUB.indexOf('id="panel-look"') < HUB.indexOf('id="panel-review"'), 'in document order too');
118
- assert.match(HUB, /if \(step === 10\) renderLook\(\);\n\s*if \(step === 11\) renderReview\(\);/);
119
- assert.match(fs.readFileSync(path.join(LANDING, 'assets', 'cosmos.css'), 'utf8'), /#view-new \.steps\{display:grid;grid-template-columns:repeat\(9,1fr\);/, 'a track per numbered step');
118
+ assert.match(HUB, /if \(step === 11\) renderLook\(\);\r?\n\s*if \(step === 12\) renderReview\(\);/);
119
+ assert.match(fs.readFileSync(path.join(LANDING, 'assets', 'cosmos.css'), 'utf8'), /#view-new \.steps\{display:grid;grid-template-columns:repeat\(10,1fr\);/, 'a track per numbered step');
120
120
  });
121
121
 
122
122
  test('every kit walk past the App step passes the Look step, and the kit has a state on it', () => {
@@ -41,7 +41,7 @@ const LANDING = path.join(ROOT, 'modules', 'public-landing', 'public');
41
41
  const HUB = fs.readFileSync(path.join(LANDING, 'projects.html'), 'utf8');
42
42
 
43
43
  const START = '/* ── the module picker (task 1002339, BV1.R20) ──';
44
- const END = "$('skipModules').addEventListener('click', function () { show(7, true); });";
44
+ const END = "$('skipModules').addEventListener('click', function () { show(8, true); });";
45
45
  const from = HUB.indexOf(START);
46
46
  const to = HUB.indexOf(END);
47
47
  assert.ok(from > -1, 'the picker block opens with its sentinel comment');
@@ -160,7 +160,7 @@ function boot({ state: seed = {}, fetchOk = true, payload = PAYLOAD } = {}) {
160
160
  const els = new Map();
161
161
  const $ = (id) => { if (!els.has(id)) els.set(id, makeEl(id)); return els.get(id); };
162
162
  const form = makeEl('panel-modules-form');
163
- const state = { step: 6, projType: null, teamShape: null, modules: null, ...seed };
163
+ const state = { step: 7, projType: null, teamShape: null, modules: null, ...seed };
164
164
  const shown = [];
165
165
  let saves = 0;
166
166
  let fetches = 0;
@@ -312,7 +312,7 @@ test('Skip walks past WITHOUT answering, so the server applies the bundle the pa
312
312
 
313
313
  // Assert — skipping is the one path that must leave nothing said, because the
314
314
  // preselect and the server's fallback are the same declaration (ADR 0237)
315
- assert.deepEqual(h.shown, [7], 'it advances to the Home step (7 since task 1004417)');
315
+ assert.deepEqual(h.shown, [8], 'it advances to the Home step (8 since task 1004418, 7 since 1004417)');
316
316
  assert.equal(h.state.modules, null, 'and answers nothing');
317
317
  assert.equal(h.forCreate(), undefined);
318
318
  assert.deepEqual(h.picked(), ['npm-release', 'discord'], 'while the set they were shown is still the set they get');
@@ -377,7 +377,7 @@ test('a catalog that will not load degrades to a clause — it never blocks a pr
377
377
  assert.match(h.coreNote(), /couldn’t load/, 'the panel says so in the owner\'s words');
378
378
  assert.equal(h.state.modules, null, 'nothing is answered on their behalf');
379
379
  h.skip();
380
- assert.deepEqual(h.shown, [7], 'and the wizard still walks on — the server applies the bundle');
380
+ assert.deepEqual(h.shown, [8], 'and the wizard still walks on — the server applies the bundle');
381
381
  });
382
382
 
383
383
  test('every rendered value goes through the page escaper', async () => {
@@ -415,7 +415,7 @@ test('the review names the set even when the picker was never opened (task 10023
415
415
  // and the module NAMES are not known. "None — just the core" would be a false
416
416
  // promise on the last screen before Create: the server is about to apply the
417
417
  // type's bundle. The row says that instead, then repaints when the read lands.
418
- const h = boot({ state: { projType: 'game', step: 11 } }); // 11 = the review since task 1004421 (10 since 1004417, 8 since 1004351)
418
+ const h = boot({ state: { projType: 'game', step: 12 } }); // 12 = the review since task 1004418 (11 since 1004421, 10 since 1004417, 8 since 1004351)
419
419
 
420
420
  // Arrange + Act — the review asks for the value before the read can answer
421
421
  const first = h.extrasRow();
@@ -430,14 +430,14 @@ test('the review names the set even when the picker was never opened (task 10023
430
430
  });
431
431
 
432
432
  test('the review reads an answered picker back, empty included', async () => {
433
- const chosen = boot({ state: { projType: 'game', modules: ['agents'], step: 7 } });
433
+ const chosen = boot({ state: { projType: 'game', modules: ['agents'], step: 8 } });
434
434
  // before the catalog lands the row says the true thing without the names —
435
435
  // never the raw module keys, which are jargon on an owner-facing screen
436
436
  assert.equal(chosen.extrasRow(), 'The extras you picked');
437
437
  await chosen.settle();
438
438
  assert.equal(chosen.extrasRow(), 'Extra agents', 'and names them once it has');
439
439
 
440
- const none = boot({ state: { projType: 'game', modules: [], step: 7 } });
440
+ const none = boot({ state: { projType: 'game', modules: [], step: 8 } });
441
441
  none.extrasRow();
442
442
  await none.settle();
443
443
  assert.equal(none.extrasRow(), 'None — just the core', 'a deliberate "none" is stated as one');
@@ -510,7 +510,7 @@ test('Skip after an answered About step still yields the set the panel showed',
510
510
 
511
511
  h.skip();
512
512
 
513
- assert.deepEqual(h.shown, [7]);
513
+ assert.deepEqual(h.shown, [8]);
514
514
  assert.equal(h.state.modules, null, 'skipping answers nothing, tailored or not');
515
515
  assert.equal(h.forCreate(), undefined, 'so the create request still carries no modules key');
516
516
  assert.deepEqual(h.picked(), ['discord'], 'while the set they were shown is still the set they get');
@@ -821,7 +821,7 @@ test('with nothing switched on the count says only what is shown', async () => {
821
821
  });
822
822
 
823
823
  test('the review names the hidden picks too — a filter never reaches the create request', async () => {
824
- const h = boot({ state: { projType: 'game', step: 11 } }); // 11 = the review since task 1004421 (10 since 1004417, 8 since 1004351)
824
+ const h = boot({ state: { projType: 'game', step: 12 } }); // 12 = the review since task 1004418 (11 since 1004421, 10 since 1004417, 8 since 1004351)
825
825
  await h.open();
826
826
  h.click('agents');
827
827
  assert.deepEqual(h.forCreate(), ['agents', 'npm-release', 'discord']);
@@ -552,7 +552,8 @@ test('the rail at the done panel keeps its marker on Create until the project an
552
552
  'the done-panel poll repaints when the status moves');
553
553
  // the first NUMBERED beat carries it — since task 1004417 the front door's two screens come
554
554
  // before the rail, and the card statically wears wizFront so a no-script visitor sees no rail there
555
- assert.match(HUB, new RegExp(`<li class="now" data-step="${STEP['panel-name']}" aria-current="step"><span class="snum">1</span>`), 'the static rail starts with the same marker');
555
+ // (since task 1004418 that beat is planet physics, the first numbered step)
556
+ assert.match(HUB, new RegExp(`<li class="now" data-step="${STEP['panel-physics']}" aria-current="step"><span class="snum">1</span>`), 'the static rail starts with the same marker');
556
557
  assert.match(HUB, /<section id="view-new" hidden>\s*<div class="wrap viewWrap">\s*<div class="card wizFront">/, 'and steps aside on the static first screen');
557
558
  });
558
559
 
@@ -251,7 +251,7 @@ test('Name my demo planet carries the demo into the ordinary wizard: the idea, t
251
251
  assert.deepEqual(JSON.parse(JSON.stringify(h.state.demo)), { people: 3, hours_each: 4, idea: 'A shared reading list.', invites: ['avery'] });
252
252
  assert.equal(h.state.description, 'A shared reading list.', 'the idea is the description');
253
253
  assert.equal(h.state.teamShape, 'small-team');
254
- assert.deepEqual(h.shown, [3], 'on to the name step');
254
+ assert.deepEqual(h.shown, [4], 'on to the name step, past planet physics (task 1004418)');
255
255
  });
256
256
 
257
257
  test('the create sends the demo, the review names it, and the project page asks the question', () => {
@@ -92,7 +92,7 @@ test('a draft already at step 1 is unchanged, and an out-of-range step is not re
92
92
  assert.equal(runStep({ authKnown: false, draftStep: 1 }).persisted.step, 1);
93
93
  // 12 is the done panel (11 before task 1004421's Look step, 9 before task 1004417's front door, 8 before task 1004351), reached by its own branch
94
94
  // above this slice; 0 is the gate. Neither is a resumable wizard position, so the placeholder is allowed to stand.
95
- for (const bad of [0, 12, 99]) {
95
+ for (const bad of [0, 13, 99]) {
96
96
  assert.equal(runStep({ authKnown: false, draftStep: bad }).persisted.step, 1, `step ${bad} is not resumed`);
97
97
  }
98
98
  });
@@ -42,18 +42,21 @@ test('the front door stands between sign-in and the name step: gate 0, terms 1,
42
42
  assert.equal(MAP['panel-gate'], 0, 'sign-in stays where it is today, first');
43
43
  assert.equal(MAP['panel-terms'], 1);
44
44
  assert.equal(MAP['panel-fork'], 2);
45
- assert.equal(MAP['panel-name'], 3, 'and today\'s wizard continues unchanged after it');
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');
46
48
  assert.ok(HUB.indexOf('id="panel-gate"') < HUB.indexOf('id="panel-terms"')
47
49
  && HUB.indexOf('id="panel-terms"') < HUB.indexOf('id="panel-fork"')
48
- && HUB.indexOf('id="panel-fork"') < HUB.indexOf('id="panel-name"'), 'the markup reads in the same order');
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');
49
52
  assert.match(TERMS, /<form data-next="2">/, 'the terms go on to the fork');
50
- assert.match(FORK, /<form data-next="3">/, 'the fork\'s primary door goes on to the name step');
53
+ assert.match(FORK, /<form data-next="3">/, 'the fork\'s primary door goes on to planet physics');
51
54
  assert.match(FORK, /data-back="1"/, 'and the way back is the terms');
52
55
  });
53
56
 
54
57
  test('the rail is the numbered steps\' — it steps aside on both front-door screens, statically too', () => {
55
58
  const beats = [...HUB.matchAll(/<li[^>]*data-step="(\d+)"/g)].map((m) => Number(m[1]));
56
- assert.deepEqual(beats, [3, 4, 5, 6, 7, 8, 9, 10, 11], 'the rail numbers name…create, never the front door');
59
+ assert.deepEqual(beats, [3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 'the rail numbers physics…create, never the front door');
57
60
  assert.match(HUB, /<div class="card wizFront">/, 'a no-script visitor on the static first screen sees no rail');
58
61
  assert.match(STYLE, /#view-new \.card\.wizFront \.steps\{display:none;\}/);
59
62
  });
@@ -215,7 +218,7 @@ function runShow(step) {
215
218
  root: { querySelector: (sel) => (sel === '.card' ? { classList: card } : { classList: split }) },
216
219
  paintRail() {}, saveDraft() {}, authKnown: false, repoAccess: null,
217
220
  renderModules() {}, syncHome() {}, ensureRepoAccess() {}, checkSlugAvailable() {}, renderAddrPanel() {},
218
- syncAppStep() {}, syncRenderSetup() {}, renderReview() {},
221
+ syncAppStep() {}, syncRenderSetup() {}, renderReview() {}, renderLook() {}, paintPhysics() {},
219
222
  };
220
223
  sandbox.globalThis = sandbox;
221
224
  vm.createContext(sandbox);
@@ -225,10 +228,11 @@ function runShow(step) {
225
228
 
226
229
  test('show() takes the rail away on the two front-door screens only, and gives the fork the whole card', () => {
227
230
  assert.ok(showSlice, 'show() was not found in projects.html');
228
- const terms = runShow(1), fork = runShow(2), name = runShow(3), done = runShow(MAP['panel-done']);
231
+ const terms = runShow(1), fork = runShow(2), physics = runShow(3), name = runShow(4), done = runShow(MAP['panel-done']);
229
232
  assert.deepEqual(terms.on, ['panel-terms']);
230
233
  assert.ok(terms.card.has('wizFront') && !terms.split.has('wizFork'), 'terms: no rail, the bongo pair stays');
231
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');
232
236
  assert.ok(!name.card.has('wizFront') && !name.split.has('wizFork'), 'the name step has its rail back');
233
237
  assert.ok(done.split.has('wizDone') && !done.card.has('wizFront'), 'the done panel is untouched by the front door');
234
238
  });
@@ -246,7 +250,7 @@ test('the create carries the tick, and is never filed without it', () => {
246
250
  test('the tick is part of the draft and of the reset, and the draft key moved with the renumbering', () => {
247
251
  assert.match(SCRIPT, /var state = \{ step: 0,[^}]*termsAck: false \};/, 'the initial state declares it');
248
252
  assert.match(SCRIPT, /state = \{ step: 1,[^}]*termsAck: false \};/, 'and "start another project" clears it — each project records its own');
249
- assert.match(SCRIPT, /var DRAFT_KEY = 'cb-create-draft-v7';/, 'a draft saved under the old numbering is ignored, not replayed onto the wrong screen');
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');
250
254
  });
251
255
 
252
256
  // ── the look-before-you-ship kit walks it ─────────────────────────────────────
@@ -58,8 +58,8 @@ test('both slices are still findable (a rename must fail loudly, not silently pa
58
58
  assert.ok(offerSlice, 'paintResumeOffer() was not found in projects.html');
59
59
  assert.match(enterSlice, /if \(focus && !resumeIntent\) \{/, 'the intent branch is part of the slice');
60
60
  assert.match(enterSlice, /startProgress\(created\.id\);/, 'so is the resume branch it guards');
61
- assert.match(enterSlice, /draft\.step >= 1 && draft\.step <= 11/,
62
- 'the STORED draft\'s step decides, not state.step — state.step is 12 for the whole life of a finished standup');
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');
63
63
  assert.match(SCRIPT, /if \(created && created\.id\) \{ enterCreated\(created, !!focus\); return; \}/,
64
64
  'enter() still hands the created record straight to it');
65
65
  });
@@ -73,7 +73,7 @@ function runEnter({ focus, resumeIntent = false, draft = null, record }) {
73
73
  const calls = { show: [], resets: 0, offer: [], done: 0, progress: [], polls: 0, focused: 0 };
74
74
  // state.step is the DONE step for the whole life of a finished standup — the value
75
75
  // a draft-blind reading would have trusted
76
- const state = { step: draft ? draft.step : 12, name: 'Mercury', slug: 'mercury', domain: '', mode: 'greenfield', noAddress: false, modules: null };
76
+ const state = { step: draft ? draft.step : 13, name: 'Mercury', slug: 'mercury', domain: '', mode: 'greenfield', noAddress: false, modules: null };
77
77
  const sandbox = {
78
78
  record, focus, resumeIntent, state,
79
79
  DRAFT_KEY: 'draft',
@@ -99,7 +99,7 @@ const REC = { id: 'inst-1', name: 'Acme', slug: 'acme', domain: 'acme.example',
99
99
 
100
100
  test('a reload / Back / identity settle (focus false) still resumes the done panel, watch and all', () => {
101
101
  const { calls, state } = runEnter({ focus: false, record: REC });
102
- assert.deepEqual(calls.show, [[12, false]], 'the done panel, and no focus steal on a non-deliberate arrival');
102
+ assert.deepEqual(calls.show, [[13, false]], 'the done panel, and no focus steal on a non-deliberate arrival');
103
103
  assert.equal(calls.resets, 0, 'nothing is reset — this is the same standup continuing');
104
104
  assert.equal(calls.done, 1, 'the checklist is re-rendered');
105
105
  assert.deepEqual(calls.progress, ['inst-1'], 'the progress watch is restarted from the record');
@@ -127,7 +127,7 @@ test('…and the standup stays one click away, named, on the resume link', () =>
127
127
 
128
128
  test('the resume link gets the done panel back — the record was never cleared', () => {
129
129
  const { calls, sandbox } = runEnter({ focus: true, resumeIntent: true, record: REC });
130
- assert.deepEqual(calls.show, [[12, true]], 'a deliberate resume moves focus to the done panel');
130
+ assert.deepEqual(calls.show, [[13, true]], 'a deliberate resume moves focus to the done panel');
131
131
  assert.deepEqual(calls.progress, ['inst-1'], 'the watch comes back with it');
132
132
  assert.equal(sandbox.resumeIntent, false, 'the flag is ONE-SHOT — the next plain nav click must mean "new" again');
133
133
  assert.deepEqual(calls.offer, [null], 'and the offer comes down behind it');
@@ -148,7 +148,7 @@ test('the done panel\'s own saved step is NOT a second draft — it must not blo
148
148
  // Caught in a render, not in a unit test: show(12) calls saveDraft(), so DRAFT_KEY is
149
149
  // non-empty again the moment a create lands. Reading "is there a draft?" left the
150
150
  // 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: 12, name: 'Mercury' } });
151
+ const { calls, state } = runEnter({ focus: true, record: REC, draft: { step: 13, name: 'Mercury' } });
152
152
  assert.equal(calls.resets, 1, 'the done step is bookkeeping, not a project being typed');
153
153
  assert.deepEqual(calls.show, [[1, false]], 'out-of-range steps land on step 1, the front door');
154
154
  assert.equal(state.name, '', 'and the finished project\'s name does not survive into it');
@@ -159,7 +159,7 @@ test('a draft on the app step, the Look step or the review is live too — the l
159
159
  // The app step moved the review from 7 to 8, the front door (task 1004417) to 10 and the
160
160
  // Look step (task 1004421) to 11. A guard left behind would reset a second project
161
161
  // sitting on its review, silently wiping it.
162
- for (const step of [9, 10, 11]) {
162
+ for (const step of [10, 11, 12]) {
163
163
  const { calls, state } = runEnter({ focus: true, record: REC, draft: { step } });
164
164
  assert.equal(calls.resets, 0, `step ${step} is a live draft`);
165
165
  assert.deepEqual(calls.show, [[step, true]]);
@@ -175,8 +175,8 @@ test('a draft on the front door is live too — a founder part-way through it ke
175
175
  }
176
176
  });
177
177
 
178
- test('a draft whose step is missing or junk resets too — only 1-11 preserves anything', () => {
179
- for (const draft of [{ step: 0 }, { step: 12 }, { name: 'half-typed' }]) {
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
180
  const { calls } = runEnter({ focus: true, record: REC, draft });
181
181
  assert.equal(calls.resets, 1, `step ${JSON.stringify(draft.step)} is not a wizard step`);
182
182
  assert.deepEqual(calls.show, [[1, false]]);