@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
@@ -17,8 +17,8 @@
17
17
  // projects.html's one big inline <script> — so the block is sliced out by its
18
18
  // own comment sentinel (up to the repo picker that follows it) and run with the
19
19
  // things it closes over ($, root, esc, state, saveDraft, paintSetup) supplied by the
20
- // harness. The review's row machinery (ROWS … confirmRow and its delegated click) is
21
- // sliced out beside it, so confirming the row runs the page's own code. Both slices
20
+ // harness. Part 02's row machinery (ROWS … openRow/closeRow and the delegated click) is
21
+ // sliced out beside it, so opening and closing the row runs the page's own code. Both slices
22
22
  // are assertions: rename an edge and this file fails loudly rather than quietly
23
23
  // testing nothing.
24
24
  //
@@ -61,7 +61,8 @@ const REND = ' /* what each row says: its value';
61
61
  const rfrom = HUB.indexOf(RSTART), rto = HUB.indexOf(REND, rfrom);
62
62
  assert.ok(rfrom > -1 && rto > rfrom, 'the review rows are sliced by ROWS and rowText');
63
63
  const REVIEW = HUB.slice(rfrom, rto).replace(/\r\n/g, '\n');
64
- assert.ok(REVIEW.includes('function confirmRow(key)'), 'and they carry confirmRow');
64
+ assert.ok(REVIEW.includes('function openRow(key)') && REVIEW.includes('function firstInvalidRow()'), 'and they carry the rows\' Change and Create\'s check');
65
+ assert.doesNotMatch(REVIEW, /function confirmRow\(|function confirmAllRows\(/, 'and nothing that confirms a row (task 1004648)');
65
66
 
66
67
  // The payload GET /provisioning/starter-bundles serves (ADR 0237 + task 1002339).
67
68
  // Deliberately a FIXTURE and not a live require: this asserts the picker reads
@@ -185,8 +186,8 @@ function whyOf(html) {
185
186
  function boot({ state: seed = {}, fetchOk = true, payload = PAYLOAD } = {}) {
186
187
  const els = new Map();
187
188
  const $ = (id) => { if (!els.has(id)) els.set(id, makeEl(id)); return els.get(id); };
188
- // step 4 is the review, where the Extras row lives (task 1004504)
189
- const state = { step: 4, projType: null, teamShape: null, modules: null, confirmed: {}, ...seed };
189
+ // step 1 is the page, where part 02's Extras row lives (task 1004648)
190
+ const state = { step: 1, projType: null, teamShape: null, modules: null, ...seed };
190
191
  const rows = {};
191
192
  const rowFor = (sel) => { const m = /data-row="([a-z]+)"/.exec(sel); return m ? (rows[m[1]] = rows[m[1]] || makeRow(m[1])) : null; };
192
193
  let saves = 0;
@@ -207,6 +208,7 @@ function boot({ state: seed = {}, fetchOk = true, payload = PAYLOAD } = {}) {
207
208
  phys: () => ({ answers: { framing: 'managed' } }),
208
209
  validateCode: () => true, validateAddress: () => true, validateRender: () => true,
209
210
  checkSlugAvailable: () => {}, renderAddrPanel: () => {}, syncHome: () => {}, syncAppStep: () => {}, syncRenderSetup: () => {},
211
+ syncShipStep: () => {}, renderLook: () => {}, paintCreate: () => {},
210
212
  API: '/api/bongos',
211
213
  fetch: () => { fetches++; return Promise.resolve(fetchOk
212
214
  ? { ok: true, json: () => Promise.resolve(payload) }
@@ -247,13 +249,11 @@ function boot({ state: seed = {}, fetchOk = true, payload = PAYLOAD } = {}) {
247
249
  };
248
250
  band._handlers.click({ target });
249
251
  },
250
- // the page's "Looks right, continue" (every row through its own check), Edit to
251
- // open the Extras row, and Done — the same control, open — to confirm it
252
- looksRight: () => sandbox.confirmAllRows(),
252
+ // Change opens the Extras row, and Done — the same control, open — closes it;
253
+ // Create's own check is firstInvalidRow (task 1004648)
253
254
  change: () => rowClick('data-row-chg'),
254
255
  use: () => rowClick('data-row-chg'),
255
- confirmed: () => !!(state.confirmed && state.confirmed.extras),
256
- unconfirmed: () => [...sandbox.unconfirmedRows()],
256
+ blocking: () => sandbox.firstInvalidRow(),
257
257
  forCreate: () => sandbox.modulesForCreate(),
258
258
  picked: () => sandbox.pickedModules(),
259
259
  // BV1.R21 (task 1002340) — the search box. type() is the real `input` event
@@ -344,21 +344,21 @@ test('switching every module off is a real answer, not a reset to the bundle', a
344
344
  assert.deepEqual(h.rows().filter((r) => r.pressed), [], 'and the panel shows it');
345
345
  });
346
346
 
347
- test('an unconfirmed row answers NOTHING, and Create waits for it (no Skip since task 1004504)', async () => {
347
+ test('an untouched row answers NOTHING, and Create does not wait for it (task 1004648)', async () => {
348
348
  // Arrange
349
349
  const h = boot({ state: { projType: 'game' } });
350
350
  await h.open();
351
351
 
352
- // Assert — the row is shown prefilled, and until it is confirmed nothing is said:
352
+ // Assert — the row is shown prefilled, and nothing is said on the owner's behalf:
353
353
  // the preselect and the server's fallback are the same declaration (ADR 0237)
354
354
  assert.equal(h.state.modules, null, 'nothing is answered on the owner\'s behalf');
355
355
  assert.equal(h.forCreate(), undefined, 'so a create now would carry no modules key');
356
356
  assert.deepEqual(h.picked(), ['npm-release', 'discord'], 'while the set they are shown is still the set they get');
357
- assert.ok(h.unconfirmed().includes('extras'), 'and the review holds Continue until the row is confirmed');
357
+ assert.equal(h.blocking(), null, 'and no row needs confirming before Create: the summary wins');
358
358
  });
359
359
 
360
- test('Edit, a toggle, then Done: the toggle stands as the answer', async () => {
361
- // A visitor who changed something and then confirmed meant "this", not "throw that away".
360
+ test('Change, a toggle, then Done: the toggle stands as the answer', async () => {
361
+ // A visitor who changed something and then closed the row meant "this", not "throw that away".
362
362
  const h = boot({ state: { projType: 'game' } });
363
363
  await h.open();
364
364
  h.change();
@@ -367,19 +367,16 @@ test('Edit, a toggle, then Done: the toggle stands as the answer', async () => {
367
367
  h.use();
368
368
 
369
369
  assert.deepEqual(h.state.modules, ['agents', 'npm-release', 'discord']);
370
- assert.equal(h.confirmed(), true, 'and the row is confirmed');
370
+ assert.deepEqual(h.forCreate(), ['agents', 'npm-release', 'discord'], 'and rides the create request');
371
371
  });
372
372
 
373
- test('Looks right, continue CONFIRMS the set on screen — looking at it and accepting it is not the same as never being asked', async () => {
373
+ test('opening and closing the row without a toggle answers nothing: the suggestion stays the server\'s', async () => {
374
374
  const h = boot({ state: { projType: 'game' } });
375
375
  await h.open();
376
-
377
- h.looksRight();
378
-
379
- assert.deepEqual(h.state.modules, ['npm-release', 'discord'], 'the preselect becomes the answer');
380
- assert.deepEqual(h.forCreate(), ['npm-release', 'discord'], 'and rides the create request');
381
- assert.equal(h.confirmed(), true);
382
- assert.ok(!h.unconfirmed().includes('extras'), 'and Create no longer waits on it');
376
+ h.change();
377
+ h.use();
378
+ assert.equal(h.state.modules, null, 'looking is not answering');
379
+ assert.equal(h.forCreate(), undefined, 'so the server applies the same bundle the row showed');
383
380
  });
384
381
 
385
382
  test('changing the project type re-preselects while nothing has been said, and never after', async () => {
@@ -419,14 +416,12 @@ test('a catalog that will not load degrades to a clause — it never blocks a pr
419
416
  assert.deepEqual(h.rows(), [], 'no rows are invented');
420
417
  assert.match(h.coreNote(), /couldn’t load/, 'the panel says so in the owner\'s words');
421
418
  assert.equal(h.state.modules, null, 'nothing is answered on their behalf');
422
- // The row still has to be confirmable, or a failed read would block Create. But the row
423
- // said "the suggested set", so confirming it must not file an EMPTY list — that would
424
- // switch off the very bundle the server would otherwise apply.
425
- h.looksRight();
426
- assert.equal(h.confirmed(), true, 'the wizard still walks on');
419
+ // A failed read must never block Create, and the row said "the suggested set", so Create
420
+ // must not file an EMPTY list — that would switch off the very bundle the server applies.
421
+ assert.equal(h.blocking(), null, 'Create still goes on');
427
422
  // (through JSON: a vm-born array never strictly equals one born here, so a bare
428
423
  // notDeepEqual would pass whatever the page did)
429
- assert.notEqual(JSON.stringify(h.forCreate()), '[]', 'and confirming "the suggested set" never files "none"');
424
+ assert.notEqual(JSON.stringify(h.forCreate()), '[]', 'and "the suggested set" never files "none"');
430
425
  });
431
426
 
432
427
  test('every rendered value goes through the page escaper', async () => {
@@ -460,11 +455,11 @@ test('the catalog read is LAZY and asked once — /projects serves four views fr
460
455
  });
461
456
 
462
457
  test('the Extras row names the set even when the picker was never opened (task 1002339)', async () => {
463
- // A resumed draft lands straight on the review, so the catalog has not loaded
458
+ // A resumed draft lands straight on the page, so the catalog has not loaded
464
459
  // and the module NAMES are not known. "None — just the core" would be a false
465
460
  // promise before Create: the server is about to apply the type's bundle. The row
466
461
  // says that instead, then repaints when the read lands.
467
- const h = boot({ state: { projType: 'game', step: 4 } }); // 4 = the review since task 1004504 (12 since 1004418)
462
+ const h = boot({ state: { projType: 'game', step: 1 } }); // 1 = the page since task 1004648 (4 = the review before it)
468
463
 
469
464
  // Arrange + Act — the review asks for the value before the read can answer
470
465
  const first = h.extrasRow();
@@ -554,18 +549,16 @@ test('changing the team shape later re-preselects, with no second read', async (
554
549
  assert.equal(h.fetches(), 1, 'and the answer cost no round trip');
555
550
  });
556
551
 
557
- test('after a team answer the unconfirmed row still answers nothing, and confirming takes the tailored set', async () => {
552
+ test('after a team answer the untouched row still answers nothing, and shows the tailored set', async () => {
558
553
  // ADR 0240 §2's property, extended to the interview's answer (ADR 0243): the
559
- // preselect and the server's fallback are the same declaration, so an unconfirmed
560
- // row must still answer NOTHING, and confirming it must take the tailored set.
554
+ // preselect and the server's fallback are the same declaration, so an untouched
555
+ // row must still answer NOTHING, while showing the tailored set the server will apply.
561
556
  const h = boot({ state: { projType: 'game', teamShape: 'solo' }, payload: TAILORED });
562
557
  await h.open();
563
558
 
564
- assert.equal(h.state.modules, null, 'unconfirmed answers nothing, tailored or not');
559
+ assert.equal(h.state.modules, null, 'untouched answers nothing, tailored or not');
565
560
  assert.equal(h.forCreate(), undefined, 'so the create request would carry no modules key');
566
561
  assert.deepEqual(h.picked(), ['discord'], 'while the set they were shown is still the set they get');
567
- h.looksRight();
568
- assert.deepEqual(h.forCreate(), ['discord'], 'and confirming files exactly that set');
569
562
  });
570
563
 
571
564
  test('once the owner has answered, the reasons go — the panel stops arguing', async () => {
@@ -873,7 +866,7 @@ test('with nothing switched on the count says only what is shown', async () => {
873
866
  });
874
867
 
875
868
  test('the Extras row names the hidden picks too — a filter never reaches the create request', async () => {
876
- const h = boot({ state: { projType: 'game', step: 4 } }); // 4 = the review since task 1004504 (12 since 1004418)
869
+ const h = boot({ state: { projType: 'game', step: 1 } }); // 1 = the page since task 1004648 (4 = the review before it)
877
870
  await h.open();
878
871
  h.click('agents');
879
872
  assert.deepEqual(h.forCreate(), ['agents', 'npm-release', 'discord']);
@@ -232,16 +232,13 @@ test('the wizard’s 401 recoveries keep the repo scope they already granted', (
232
232
  });
233
233
 
234
234
  test('a resumed draft past the code row still probes repo access before Create', () => {
235
- // task 1004504: the Home step is the review's code row, so the review probes on entry —
236
- // and a draft that resumes on the look screen (past the review, before done) must still
237
- // probe, or Create files a repo it promised to make but never did (ensureFreshRepo skips
238
- // the create whenever repoAccess is not true)
239
- const REVIEW = STEP['panel-review'];
240
- assert.match(SCRIPT, new RegExp(`if \\(step === ${REVIEW}\\) \\{[\\s\\S]*?if \\(authKnown\\) ensureRepoAccess\\(\\);`),
241
- 'the review screen probes on entry');
242
- assert.ok(SCRIPT.includes(
243
- `else if (step > ${REVIEW} && step < ${DONE} && authKnown && repoAccess === null) ensureRepoAccess();`),
244
- 'every screen between the review and the done panel re-probes');
235
+ // task 1004648: the Home step is part 02's Code row on the one page, so the page probes on
236
+ // entry — Create would otherwise file a repo it promised to make but never did
237
+ // (ensureFreshRepo skips the create whenever repoAccess is not true). There is no screen
238
+ // between the page and Create any more for a resumed draft to land past it on.
239
+ assert.match(SCRIPT, /if \(step === 1\) paintPage\(\);/, 'the page paints on entry');
240
+ assert.match(SCRIPT, /function paintPage\(\) \{[\s\S]*?if \(authKnown\) ensureRepoAccess\(\);/, 'and the page probes on entry');
241
+ assert.match(SCRIPT, /function onAuth\(\) \{[\s\S]*?if \(currentView === 'new'\) enter\(false\);/, 'and again once /me settles, so a pre-auth paint is caught up');
245
242
  });
246
243
 
247
244
  test('a half-typed custom domain says why Done refuses it; a taken handle silences the format cheer', () => {
@@ -257,7 +254,7 @@ test('no wall-clock promises, no operator vocabulary on the stranger’s path',
257
254
  /* Since ADR 0345 (task 1004351) the APP has its own row; since task 1004504 the review is
258
255
  the row-by-row confirm, and that row speaks the owner's words. The assertion still
259
256
  guards the same property this test is about: owner vocabulary, and no wall-clock promise. */
260
- assert.match(HUB, /<div class="setRow" data-row="app">\s*<span class="k">App hosting<\/span>/, 'the review names the app\'s hosting in plain words');
257
+ assert.match(HUB, /<div class="setRow" data-row="app">\s*<span class="k">Hosting<\/span>/, 'part 02 names the app\'s hosting in plain words');
261
258
  assert.ok(SCRIPT.includes("return { v: appPlatformChoice() === 'render' ? 'Render' : 'Decide later', s: appRowDetail() };"), 'its value is a place or "Decide later", never a release name');
262
259
  assert.doesNotMatch(SCRIPT, /current release|release channel/, 'no release vocabulary on the stranger\'s path');
263
260
  assert.doesNotMatch(HUB, /\/new-project<\/code> assistant/, 'the done panel’s closing line');
@@ -480,9 +477,9 @@ test('decision 5 — the Type step says where the type can be changed (task 1003
480
477
  // design of record's words, which make no promise about changing it. What must hold: the
481
478
  // narrowed promise never comes back, and any change-later promise carries its address.
482
479
  assert.doesNotMatch(HUB, /id="panel-type"/, 'the Type step is gone');
483
- const what = (HUB.match(/<section class="panel" id="panel-what"[\s\S]*?<\/section>/) || [''])[0];
484
- assert.ok(what, '#panel-what exists');
485
- assert.match(what, /<span id="typeLbl">Kind<\/span>/, 'the kind is asked on the What screen');
480
+ const what = (HUB.match(/<div class="part" id="part-project"[\s\S]*?\n {14}<\/div>\n/) || [''])[0];
481
+ assert.ok(what, 'part 01 exists (task 1004648)');
482
+ assert.match(what, /<span id="typeLbl">Kind<\/span>/, 'the kind is asked in part 01, Your project');
486
483
  assert.doesNotMatch(what, /stays as you pick it/, 'the narrowed promise must not survive');
487
484
  if (/change it later/.test(what)) {
488
485
  assert.match(what, /change it later from the project’s own page/, 'a change-later promise names WHERE — a promise with an address');
@@ -114,7 +114,8 @@ test('GET /project-settings/ship-mode is open to any signed-in builder, not gate
114
114
  const names = route('get', '/project-settings/ship-mode').stack.map((l) => l.name);
115
115
  assert.equal(names.length, 2, 'requireBuilder + the handler, and no permission gate');
116
116
  const write = route('patch', '/project-settings/ship-mode').stack;
117
- assert.equal(write.length, 3, 'the write carries requireBuilder + the settings permission + the handler');
117
+ assert.equal(write.length, 4, 'the write carries requireBuilder + the settings permission + Rules\' decision rule (task 1004657) + the handler');
118
+ assert.equal(write[2].handle.name, 'actAloneGate');
118
119
  } finally { restore(); }
119
120
  });
120
121
 
@@ -238,42 +238,46 @@ function bootHub() {
238
238
  const b = new El(`step-${k}${by}`); b.attrs['data-demo-step'] = k; b.attrs['data-by'] = by; return b;
239
239
  });
240
240
  const shown = [];
241
- // the doors wait for the fork's tick (task 1004504), so the founder here has given it
242
- const state = { step: 1, demo: null, description: '', teamShape: null, termsAck: true, confirmed: { team: true } };
243
- const sandbox = { $, state, saveDraft() {}, show: (n) => shown.push(n), eyebrowFor: () => 'Demo', root: { querySelectorAll: () => steppers } };
241
+ // the demo is a small link under the heading (task 1004648): it waits for nothing
242
+ const state = { step: 1, demo: null, description: '', teamShape: null, termsAck: false };
243
+ const sandbox = { $, state, saveDraft() {}, show: (n) => shown.push(n), root: { querySelectorAll: () => steppers } };
244
244
  vm.createContext(sandbox);
245
245
  vm.runInContext(SLICE, sandbox);
246
246
  const click = (id) => $(id).listeners.click.call($(id));
247
247
  return { $, state, shown, steppers, click, sandbox };
248
248
  }
249
249
 
250
- test('the door opens ONE setup screen, the steppers stay in their limits, and the first read follows them', () => {
250
+ test('the small link opens the demo in place, the steppers stay in their limits, and the first read follows them', () => {
251
251
  const h = bootHub();
252
- h.state.termsAck = false;
252
+ h.$('demoSetup').hidden = true;
253
253
  h.click('wizDemo');
254
- assert.equal(h.$('wizDemo').attrs['aria-expanded'], undefined, 'an unticked box opens nothing (task 1004504)');
255
- assert.equal(h.$('forkIntro').hidden, false, 'the doors stay');
256
- h.state.termsAck = true;
257
- h.click('wizDemo');
258
- assert.equal(h.$('forkIntro').hidden, true);
259
- assert.equal(h.$('demoSetup').hidden, false);
254
+ assert.equal(h.$('demoSetup').hidden, false, 'the demo opens at the top of part 01');
260
255
  assert.equal(h.$('wizDemo').attrs['aria-expanded'], 'true');
261
- assert.equal(h.$('wizBack').hidden, false, 'the one Back walks back to the doors');
262
256
  for (let i = 0; i < 20; i += 1) h.steppers[0].listeners.click.call(h.steppers[0]);
263
257
  assert.equal(h.$('demoPeople').textContent, '12', 'people stop at twelve');
264
258
  for (let i = 0; i < 20; i += 1) h.steppers[3].listeners.click.call(h.steppers[3]);
265
259
  assert.equal(h.$('demoHours').textContent, '1', 'hours stop at one');
266
260
  assert.equal(h.$('demoFitSum').textContent, '12 people × 1 hour = 12 builder-hours.');
267
261
  assert.equal(h.$('demoFitVerdict').textContent, demo.fitCheck(12, 1).sentence);
268
- h.sandbox.demoShow('intro');
269
- assert.equal(h.$('forkIntro').hidden, false, 'Back returns to the two doors');
262
+ h.click('wizDemo');
263
+ assert.equal(h.$('demoSetup').hidden, true, 'the same link closes it again');
270
264
  assert.equal(h.$('wizDemo').attrs['aria-expanded'], 'false');
271
- assert.equal(h.$('wizBack').hidden, true);
272
- assert.match(HUB, /if \(state\.step === 1 && !\$\('demoSetup'\)\.hidden\) \{ demoShow\('intro'\); \$\('wizDemo'\)\.focus\(\); return; \}/, 'which is what #wizBack does from the demo screen');
273
- assert.doesNotMatch(HUB, /id="demoReady"|id="demoRing"|id="demoNext1"|id="demoBack1"|id="demoBack2"|id="demoStageCap"/, 'the second demo screen, its ring and its own Back buttons are gone');
265
+ assert.doesNotMatch(HUB, /id="demoReady"|id="demoRing"|id="demoNext1"|id="demoBack1"|id="demoBack2"|id="demoStageCap"|id="forkIntro"/, 'no second demo screen and no fork');
266
+ assert.match(HUB, /<p class="pageSub" id="pageSub">Three parts, one page\. Just looking\? <button type="button" class="linklike" id="wizDemo" aria-controls="demoSetup" aria-expanded="false">Try a small demo instead<\/button>\.<\/p>/,
267
+ 'the demo is a small link under the heading, in the mockup\'s words');
268
+ });
269
+
270
+ test('"Make a real project instead" drops the demo and its answers, and the page stays where it is', () => {
271
+ const h = bootHub();
272
+ h.state.demo = { people: 3, hours_each: 4 };
273
+ h.click('wizDemo');
274
+ h.click('demoCancel');
275
+ assert.equal(h.state.demo, null, 'a walked-back demo does not ride the create');
276
+ assert.equal(h.$('demoSetup').hidden, true);
277
+ assert.deepEqual(h.shown, [1], 'the page repaints: the quick checks come back');
274
278
  });
275
279
 
276
- test('the demo screen\'s Continue carries the demo into the ordinary wizard: the idea, the team, the invites', () => {
280
+ test('"Use these" marks the page a demo: the idea, the team, the invites — the rest of the page unchanged', () => {
277
281
  const h = bootHub();
278
282
  h.click('wizDemo');
279
283
  assert.equal(h.$('demoInvites').hidden, false, 'the invites sit on the same screen');
@@ -286,14 +290,15 @@ test('the demo screen\'s Continue carries the demo into the ordinary wizard: the
286
290
  assert.equal(h.state.description, 'A shared reading list.', 'the idea is the description');
287
291
  assert.equal(h.$('f-desc').value, 'A shared reading list.', 'and the sentence field shows it');
288
292
  assert.equal(h.state.teamShape, 'small-team');
289
- assert.deepEqual(JSON.parse(JSON.stringify(h.state.confirmed)), {}, 'the demo\'s own rows are asked again on the review');
290
- assert.deepEqual(h.shown, [2], 'on to "What are you making?" (task 1004504)');
291
- assert.match(HUB, /<input type="text" id="demoIdea" maxlength="600"/, 'the idea is one line now');
292
- assert.match(HUB, /<button type="button" class="btn" id="demoStart">Continue<\/button>/);
293
+ assert.equal(h.$('demoSetup').hidden, true, 'the demo closes once used');
294
+ assert.deepEqual(h.shown, [1], 'and the page repaints, marked demo (task 1004648)');
295
+ assert.match(HUB, /<input type="text" id="demoIdea" maxlength="600"/, 'the idea is one line');
296
+ assert.match(HUB, /<button type="button" class="btn small" id="demoStart">Use these<\/button>/);
293
297
  });
294
298
 
295
- test('a demo skips the quick checks and sends only its team shape', () => {
296
- assert.match(HUB, /if \(next === 3 && state\.demo\) \{ physDefaults\(\); show\(4, true\); return; \}/, 'the What screen goes straight to the review for a demo');
299
+ test('a demo\'s quick checks stand down and it sends only its team shape', () => {
300
+ assert.match(HUB, /\$\('checksMain'\)\.hidden = physStopped \|\| !!state\.demo;/, 'the checks stand down for a demo');
301
+ assert.match(HUB, /function physScreen\(\) \{\s*if \(state\.demo\) return Promise\.resolve\(true\);/, 'and there is nothing of its own to screen');
297
302
  const fn = (HUB.match(/function createDetail\(\) \{[\s\S]*?\n {4}\}/) || [null])[0];
298
303
  assert.ok(fn, 'createDetail() was found');
299
304
  const box = { physDetail: () => ({ framing: 'hybrid', reward_intents: ['cash'] }), state: {} };
@@ -308,7 +313,7 @@ test('a demo skips the quick checks and sends only its team shape', () => {
308
313
 
309
314
  test('the create sends the demo, the recap names it, and the project page asks the question', () => {
310
315
  assert.match(HUB, /demo: state\.demo \? \{ people: state\.demo\.people, hours_each: state\.demo\.hours_each \} : undefined,/);
311
- assert.match(HUB, /if \(state\.demo\) bits\.push\('demo'\);/, 'the look screen\'s recap says it is a demo');
316
+ assert.match(HUB, /if \(state\.demo\) bits\.push\('demo'\);/, 'the recap beside Create says it is a demo');
312
317
  assert.match(HUB, /<p class="demoKeepH">Keep your planet\?<\/p>/);
313
318
  assert.match(HUB, /data-demo-settle="carried">Carry it over</);
314
319
  assert.match(HUB, /data-demo-settle="kept">Keep it as a demo</);
@@ -39,7 +39,7 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
39
39
  const HUB = fs.readFileSync(path.join(ROOT, 'modules', 'public-landing', 'public', 'projects.html'), 'utf8');
40
40
 
41
41
  // ── slice 1 · the step-resolution block inside enter() ────────────────────────────
42
- const STEP_RE = /var step = authKnown &&[\s\S]*?show\(step, !!focus\);\r?\n\s*\}/;
42
+ const STEP_RE = /var resumeStep = state\.step;[\s\S]*?if \(focus\) \$\('newName'\)\.focus\(\);/;
43
43
  const stepSlice = (HUB.match(STEP_RE) || [null])[0];
44
44
 
45
45
  test('the step-resolution slice is still findable (a rename must fail loudly, not silently pass)', () => {
@@ -69,35 +69,28 @@ function runStep({ authKnown, draftStep }) {
69
69
  return { rendered, persisted: JSON.parse(store.get('draft')), state };
70
70
  }
71
71
 
72
- test('pre-auth: the placeholder renders step 1 but the draft KEEPS the real step', () => {
73
- // step 4 is the review since task 1004504 — where the code row's repo access is granted
74
- const { rendered, persisted } = runStep({ authKnown: false, draftStep: 4 });
75
- assert.deepEqual(rendered, [1], 'still renders step 1 (the fork, since task 1004504) before identity settles');
76
- assert.equal(persisted.step, 4, 'the draft must not be overwritten with the placeholder');
72
+ test('pre-auth: the placeholder renders the page but the draft KEEPS the real step', () => {
73
+ // step 2 is the created page since task 1004648 — its own branch above this slice takes it
74
+ // once /me answers, so the placeholder must not overwrite it
75
+ const { rendered, persisted } = runStep({ authKnown: false, draftStep: 2 });
76
+ assert.deepEqual(rendered, [1], 'renders the page (step 1) before identity settles');
77
+ assert.equal(persisted.step, 2, 'the draft must not be overwritten with the placeholder');
77
78
  });
78
79
 
79
80
  test('pre-auth: the in-memory step is restored too, so onAuth() can re-enter to it', () => {
80
81
  // onAuth() calls enter() again and reads state.step from MEMORY, not from storage — so
81
82
  // restoring only the stored copy would fix nothing.
82
- for (const step of [2, 3, 4, 5]) {
83
+ for (const step of [0, 1, 2]) {
83
84
  const { state } = runStep({ authKnown: false, draftStep: step });
84
85
  assert.equal(state.step, step, `step ${step} comes back`);
85
86
  }
86
87
  });
87
88
 
88
- test('post-auth: the real step is rendered', () => {
89
- const { rendered, persisted } = runStep({ authKnown: true, draftStep: 4 });
90
- assert.deepEqual(rendered, [4]);
91
- assert.equal(persisted.step, 4);
92
- });
93
-
94
- test('a draft already at step 1 is unchanged, and an out-of-range step is not resurrected', () => {
95
- assert.equal(runStep({ authKnown: false, draftStep: 1 }).persisted.step, 1);
96
- // 6 is the done panel since task 1004504 (12 before it, 11 before task 1004421's Look step, 9 before task
97
- // 1004417's front door, 8 before task 1004351), reached by its own branch above this slice; 0 is the gate.
98
- // Neither is a resumable wizard position, so the placeholder is allowed to stand.
99
- for (const bad of [0, 6, 13, 99]) {
100
- assert.equal(runStep({ authKnown: false, draftStep: bad }).persisted.step, 1, `step ${bad} is not resumed`);
89
+ test('post-auth: the page is rendered, and the draft says so', () => {
90
+ for (const step of [0, 1, 2, 13]) {
91
+ const { rendered, persisted } = runStep({ authKnown: true, draftStep: step });
92
+ assert.deepEqual(rendered, [1], `from step ${step}: one page`);
93
+ assert.equal(persisted.step, 1);
101
94
  }
102
95
  });
103
96
 
@@ -216,6 +209,7 @@ test('the team pill is a plain pick on the Team size row — a second click keep
216
209
  paintTeam() {}, saveDraft() { saves += 1; }, paintSetup() { paints += 1; },
217
210
  unconfirm(keys) { unconfirmed.push(...keys); },
218
211
  };
212
+ // task 1004648: nothing is confirmed any more, so nothing is "asked again" — the row repaints
219
213
  sandbox.globalThis = sandbox;
220
214
  vm.createContext(sandbox);
221
215
  vm.runInContext(teamSlice, sandbox);
@@ -228,8 +222,8 @@ test('the team pill is a plain pick on the Team size row — a second click keep
228
222
  click('community');
229
223
  assert.equal(state.teamShape, 'community', 'a DIFFERENT pill replaces it');
230
224
  assert.equal(saves, 3, 'every pick is saved into the draft, so the GitHub round-trip keeps it');
231
- assert.deepEqual([...new Set(unconfirmed)], ['extras'], 'the suggested extras follow the team, so the review asks about them again');
232
- assert.equal(paints, 3, 'and the review repaints');
225
+ assert.deepEqual(unconfirmed, [], 'no row is un-confirmed: the summary wins (task 1004648)');
226
+ assert.equal(paints, 3, 'and the rows repaint, the suggested extras with them');
233
227
  assert.deepEqual([...HUB.matchAll(/data-team="([a-z-]+)"/g)].map((m) => m[1]), ['small-team', 'community'],
234
228
  'the row offers who can join a team; one person is the "One person" pill, not a team shape');
235
229
  });
@@ -268,17 +262,15 @@ function runEnter({ draftStep, focus = false, resumeIntent = false, created = {
268
262
  }
269
263
 
270
264
  test('back from GitHub mid-draft, with an earlier project stored: the draft step, not the old done panel', () => {
271
- for (const step of [2, 3, 4, 5]) {
272
- const log = runEnter({ draftStep: step });
273
- assert.deepEqual(log.created, [], `step ${step}: the earlier project's panel is not entered`);
274
- assert.deepEqual(log.shown, [step], `step ${step}: the draft keeps its place`);
275
- assert.equal(log.offer && log.offer.id, 101, 'the earlier project stays one click away on the resume line');
276
- }
265
+ const log = runEnter({ draftStep: 1 });
266
+ assert.deepEqual(log.created, [], 'the earlier project\'s part 03 is not entered');
267
+ assert.deepEqual(log.shown, [1], 'the draft keeps its place: the page');
268
+ assert.equal(log.offer && log.offer.id, 101, 'the earlier project stays one click away on the resume line');
277
269
  });
278
270
 
279
- test('the done panel still comes back when no second draft is in flight, or on a deliberate nav', () => {
280
- assert.equal(runEnter({ draftStep: 6 }).created.length, 1, 'a reload on the done panel stays on it');
271
+ test('the created page still comes back when no second draft is in flight, or on a deliberate nav', () => {
272
+ assert.equal(runEnter({ draftStep: 2 }).created.length, 1, 'a reload on the created page stays on it');
281
273
  assert.equal(runEnter({ draftStep: 0 }).created.length, 1, 'no draft position: the watched project');
282
- assert.deepEqual(runEnter({ draftStep: 4, focus: true }).created, [{ id: 101, focus: true }], 'a deliberate New project nav is enterCreated\'s to decide');
283
- assert.equal(runEnter({ draftStep: 4, resumeIntent: true }).created.length, 1, 'the resume link still resumes');
274
+ assert.deepEqual(runEnter({ draftStep: 1, focus: true }).created, [{ id: 101, focus: true }], 'a deliberate New project nav is enterCreated\'s to decide');
275
+ assert.equal(runEnter({ draftStep: 1, resumeIntent: true }).created.length, 1, 'the resume link still resumes');
284
276
  });