@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
@@ -46,13 +46,12 @@ const SCRIPT = slice('/* ── planet physics (tasks 1004418 and 1004419, BV2.P
46
46
  const TEAM = slice(' function teamLabel(t) {', " $('f-desc').value = state.description || '';", 'the team pills');
47
47
  const APP = slice(' function appPlatformChoice() {', ' var ROWS = [', 'the app choice');
48
48
  const REVIEW = slice(' var ROWS = [', ' function renderRecap() {', 'the review rows');
49
- const VALIDATE = slice(' /* per-screen validation on Continue */', " root.querySelectorAll('.panel form')", 'validateStep');
50
- for (const [name, region] of [['checksAdvance', SCRIPT], ['paintChecks', SCRIPT], ['physDefaults', SCRIPT], ['confirmRow', REVIEW],
51
- ['rowValid', REVIEW], ['rowFixed', REVIEW], ['paintSetup', REVIEW], ['validateStep', VALIDATE], ['paintTeam', TEAM]]) {
49
+ for (const [name, region] of [['physScreen', SCRIPT], ['paintChecks', SCRIPT], ['physDefaults', SCRIPT], ['firstInvalidRow', REVIEW],
50
+ ['rowValid', REVIEW], ['rowFixed', REVIEW], ['paintSetup', REVIEW], ['paintTeam', TEAM]]) {
52
51
  assert.ok(region.includes(`function ${name}(`), `the slice carries ${name}()`);
53
52
  }
54
- // the screens the controller paints: the quick checks and the review
55
- const PANEL = slice('<section class="panel" id="panel-checks"', '<!-- 5 · a look, then Create', 'the checks and review markup');
53
+ // what the controller paints: part 01 (the quick checks) and part 02 (the rows), task 1004648
54
+ const PANEL = slice('<div class="part" id="part-project"', '<!-- the terms (tasks', 'parts 01 and 02');
56
55
  const plain = (v) => JSON.parse(JSON.stringify(v)); // values born in the vm
57
56
 
58
57
  // ── a small DOM: the screens' own elements, as a tree, matched by selector ────
@@ -60,7 +59,7 @@ class El {
60
59
  constructor(tag, attrs = {}) {
61
60
  this.tagName = tag; this.attrs = { ...attrs }; this.hidden = 'hidden' in attrs; this.disabled = 'disabled' in attrs;
62
61
  this.value = attrs.value || ''; this.textContent = ''; this.title = ''; this.className = attrs.class || '';
63
- this.listeners = {}; this.children = []; this.options = [{}]; this.parent = null; this.checked = 'checked' in attrs;
62
+ this.listeners = {}; this.children = []; this.options = [{}]; this.parent = null; this.checked = 'checked' in attrs; this.style = {};
64
63
  const self = this;
65
64
  this.classes = new Set(this.className.split(/\s+/).filter(Boolean));
66
65
  this.classList = { toggle(c, on) { if (on) self.classes.add(c); else self.classes.delete(c); }, contains: (c) => self.classes.has(c),
@@ -154,12 +153,15 @@ function boot({ check = (body) => ({ stop: false, soft_limits: softLimitsFor(bod
154
153
  const $ = (id) => (els[id] = els[id] || new El('div', { id }));
155
154
  const shown = [];
156
155
  const fetched = [];
157
- const state = Object.assign({ step: 4, name: 'Kestrel', description: '', teamShape: null, physics: null, projType: 'game', mode: 'greenfield',
158
- appPlatform: 'later', demo: null, modules: null, confirmed: {}, termsAck: true }, draft);
156
+ const state = Object.assign({ step: 1, name: 'Kestrel', description: '', teamShape: null, physics: null, projType: 'game', mode: 'greenfield',
157
+ appPlatform: 'later', demo: null, modules: null, termsAck: true }, draft);
159
158
  const sandbox = {
160
159
  $, state, API: '/api/bongos', root: dom.root, Intl,
161
160
  document: { createElement: (t) => new El(t) },
162
- saveDraft() {}, show(n) { shown.push(n); state.step = n; },
161
+ saveDraft() {}, show(n) { shown.push(n); state.step = n; }, paintCreate() {}, renderLook() {},
162
+ // the Look row's words come from the shared picker's catalogue (tests/projects_hub_look.mjs)
163
+ lookKit: { LOOKS: { 'chrome-world': { title: 'The chrome world', colors: { accent: '#fb9569' } } } },
164
+ defaultLook: () => ({ look: 'chrome-world', nightTint: true, logo: null }),
163
165
  setMode(m) { state.mode = m; }, authKnown: true,
164
166
  // the code, address and Render checks are their own files' to run (projects_hub_app_step,
165
167
  // public_landing_projects); here they answer as told
@@ -175,7 +177,7 @@ function boot({ check = (body) => ({ stop: false, soft_limits: softLimitsFor(bod
175
177
  },
176
178
  };
177
179
  vm.createContext(sandbox);
178
- vm.runInContext([TEAM, SCRIPT, APP, REVIEW, VALIDATE].join('\n'), sandbox, { filename: 'projects.html#physics' });
180
+ vm.runInContext([TEAM, SCRIPT, APP, REVIEW].join('\n'), sandbox, { filename: 'projects.html#physics' });
179
181
  const q = (sel) => dom.root.querySelectorAll(sel);
180
182
  const row = (key) => q(`#reviewRows .setRow[data-row="${key}"]`)[0];
181
183
  return {
@@ -184,21 +186,17 @@ function boot({ check = (body) => ({ stop: false, soft_limits: softLimitsFor(bod
184
186
  a: () => state.physics.answers,
185
187
  pick: (key, val) => q(`[data-phys="${key}"][data-val="${val}"]`)[0].click(),
186
188
  rowSays: (key) => [row(key).querySelector('[data-row-v]').textContent, row(key).querySelector('[data-row-s]').textContent],
187
- // a row's own check and confirmation, as the page's one "Looks right, continue" runs it
188
- // per row (task 1004550): there is no per-row Looks right any more
189
- ok: (key) => vm.runInContext(`confirmRow('${key}')`, sandbox),
190
- // Edit opens a row; the same control reads Done while it is open, and confirms it
189
+ // Change opens a row; the same control reads Done while it is open, and closes it (task 1004648)
191
190
  change: (key) => row(key).querySelector('[data-row-chg]').click(),
192
191
  done: (key) => row(key).querySelector('[data-row-chg]').click(),
193
192
  isOpen: (key) => !row(key).querySelector('.opts').hidden,
194
- go: () => $('reviewGo'),
195
193
  settle: async () => { for (let i = 0; i < 4; i += 1) await tick(); },
196
194
  };
197
195
  }
198
196
  const tick = () => new Promise((r) => setTimeout(r, 0));
199
197
  // the quick checks, answered (the Kestrel sample of the canvas)
200
198
  const CHECKS = { home_country: 'US', home_region: 'US-CA', builder_geography: 'us-eu', minors_on_board: false, entity: 'intent-solo' };
201
- // on the review, with the checks answered, the prefills written and the table asked
199
+ // on the page, with the checks answered, the prefills written and the table asked
202
200
  async function onReview(answers = {}, opts = {}) {
203
201
  const w = boot(opts);
204
202
  w.run('phys()');
@@ -245,9 +243,9 @@ test('the kit stub\'s physics hide rules are the table\'s own, word for word', (
245
243
  }
246
244
  });
247
245
 
248
- // ── screen 3, the quick checks ───────────────────────────────────────────────
246
+ // ── part 01, the quick checks ────────────────────────────────────────────────
249
247
  test('the quick checks reveal what an answer opens: the age tiers, the legal forms, a state or province', () => {
250
- const w = boot({ draft: { step: 3 } });
248
+ const w = boot();
251
249
  w.run('paintChecks()');
252
250
  assert.equal(w.$('physAges').hidden, true, 'no tiers before a yes');
253
251
  assert.equal(w.$('physForms').hidden, true, 'the plain intents first');
@@ -277,11 +275,11 @@ test('the quick checks reveal what an answer opens: the age tiers, the legal for
277
275
  });
278
276
 
279
277
  test('the quick checks fill in from the account\'s last project, never its state, and offer nothing without one', async () => {
280
- const none = boot({ draft: { step: 3 } });
278
+ const none = boot();
281
279
  none.run('physLoadAccountOnce()');
282
280
  await none.settle();
283
281
  assert.equal(none.$('physFillOffer').hidden, true, 'no earlier answers: no offer');
284
- const w = boot({ draft: { step: 3 }, instances: { instances: [
282
+ const w = boot({ instances: { instances: [
285
283
  { created_at: '2026-01-01', detail: { home_country: 'GB', entity: 'intent-unsure' } },
286
284
  { created_at: '2026-06-01', detail: { home_country: 'NL', builder_geography: 'global', minors_on_board: false, entity: 'for-profit', project_state: 'existing', framing: 'local' } },
287
285
  ] } });
@@ -293,28 +291,31 @@ test('the quick checks fill in from the account\'s last project, never its state
293
291
  assert.equal(w.$('physCountry').value, 'NL', 'painted where the founder can see it');
294
292
  });
295
293
 
296
- test('Continue on the checks writes the review\'s prefills, asks the real table, and opens the review', async () => {
297
- const w = boot({ draft: { step: 3, description: 'A field guide app for birdwatchers' } });
294
+ test('the page writes part 02\'s prefills, and Create\'s screen asks the real table with them', async () => {
295
+ const w = boot({ draft: { description: 'A field guide app for birdwatchers' } });
298
296
  w.run('phys()');
299
297
  Object.assign(w.a(), CHECKS);
300
- assert.equal(w.run('checksAdvance()'), false, 'the screen never says yes itself');
298
+ w.run('physDefaults()');
299
+ w.run('var screened = null; physScreen().then(function (ok) { screened = ok; });');
301
300
  await w.settle();
302
- assert.deepEqual(w.shown, [4]);
301
+ assert.equal(w.run('screened'), true, 'no stop: Create may go on');
302
+ assert.deepEqual(w.shown, [], 'the screen moves nothing itself');
303
303
  const sent = w.fetched.find((f) => f.url.endsWith('/provisioning/physics-check')).body;
304
304
  assert.equal(sent.description, 'A field guide app for birdwatchers');
305
305
  assert.deepEqual({ s: sent.detail.project_state, f: sent.detail.framing, n: [sent.detail.builders_at_start, sent.detail.builders_expected] },
306
306
  { s: 'new', f: 'managed', n: [1, 1] }, 'a new project, all managed, one person');
307
307
  assert.equal(w.state.teamShape, 'solo');
308
- assert.ok(w.run('physLimits').some((l) => l.option === 'reward_intents' && l.effect === 'hide'), 'the table\'s verdicts shape the review');
308
+ assert.ok(w.run('physLimits').some((l) => l.option === 'reward_intents' && l.effect === 'hide'), 'the table\'s verdicts shape the rows');
309
309
  });
310
310
 
311
- test('a stop from the server ends on the stop screen, before anything exists', async () => {
312
- const w = boot({ draft: { step: 3, description: 'ransomware' }, check: () => ({ stop: true, message: 'We cannot host this.' }) });
311
+ test('a stop from the server ends in place of the checks, before anything exists', async () => {
312
+ const w = boot({ draft: { description: 'ransomware' }, check: () => ({ stop: true, message: 'We cannot host this.' }) });
313
313
  w.run('phys()');
314
314
  Object.assign(w.a(), CHECKS);
315
- w.run('checksAdvance()');
315
+ w.run('var screened = null; physScreen().then(function (ok) { screened = ok; });');
316
316
  await w.settle();
317
- assert.deepEqual(w.shown, [], 'no review');
317
+ assert.equal(w.run('screened'), false, 'Create files nothing');
318
+ assert.deepEqual(w.shown, [], 'nothing moves on');
318
319
  assert.equal(w.$('physStopMsg').textContent, 'We cannot host this.');
319
320
  assert.equal(w.$('checksStop').hidden, false);
320
321
  assert.equal(w.$('checksMain').hidden, true);
@@ -323,7 +324,7 @@ test('a stop from the server ends on the stop screen, before anything exists', a
323
324
  // ── the Team size row (question 5) ───────────────────────────────────────────
324
325
  test('Team size: one or many, a start count, and a guessed growth the founder corrects', async () => {
325
326
  const w = await onReview();
326
- assert.deepEqual(w.rowSays('team'), ['One person', 'Just you.'], 'prefilled: one person');
327
+ assert.deepEqual(w.rowSays('team'), ['Just you to start', 'Open to invites.'], 'prefilled: one person');
327
328
  assert.equal(w.q('[data-phys-solo="one"]')[0].getAttribute('aria-pressed'), 'true');
328
329
  assert.equal(w.$('physTeam').hidden, true);
329
330
  w.change('team');
@@ -348,27 +349,19 @@ test('Team size: one or many, a start count, and a guessed growth the founder co
348
349
  assert.equal(w.rowSays('team')[1], 'About 12 by month three · anyone can apply.');
349
350
  assert.equal(w.row('team').querySelector('[data-row-chg]').textContent, 'Done', 'an open row\'s one control reads Done');
350
351
  w.done('team');
351
- assert.equal(w.state.confirmed.team, true);
352
352
  assert.equal(w.isOpen('team'), false, 'Done closes the row');
353
- assert.equal(w.row('team').querySelector('[data-row-chg]').textContent, 'Edit');
353
+ assert.equal(w.row('team').querySelector('[data-row-chg]').textContent, 'Change');
354
+ assert.equal(w.state.confirmed, undefined, 'and nothing is recorded as confirmed: the summary wins');
354
355
  });
355
356
 
356
- test('changing the team re-asks the rows it moves: rewards and extras', async () => {
357
- const w = await onReview({ builders_at_start: 3, builders_expected: 8 });
358
- w.state.confirmed = { rewards: true, extras: true, code: true };
359
- w.q('[data-phys-solo="one"]')[0].click();
360
- assert.deepEqual(Object.keys(w.state.confirmed), ['code'], 'rewards and extras come back to be confirmed; the repo does not');
361
- assert.equal(w.state.teamShape, 'solo');
362
- });
363
-
364
- test('a team row with no count cannot be confirmed: it opens instead', async () => {
357
+ test('a team row that cannot be filed keeps Create from filing it, and Done leaves it open with its controls', async () => {
365
358
  const w = await onReview();
359
+ w.change('team');
366
360
  w.state.physics.answers.builders_at_start = 4; w.state.physics.answers.builders_expected = 2; // fewer later than now
367
- assert.equal(w.run("confirmRow('team')"), false);
368
- assert.equal(w.isOpen('team'), true, 'the row opens with its controls');
369
- assert.equal(w.row('team').classList.contains('open'), true);
361
+ assert.equal(w.run('firstInvalidRow()'), 'team', 'Create\'s check names it');
362
+ w.done('team');
363
+ assert.equal(w.isOpen('team'), true, 'Done does not close a row whose answer fails');
370
364
  assert.equal(w.row('team').querySelector('[data-row-chg]').getAttribute('aria-expanded'), 'true');
371
- assert.equal(w.state.confirmed.team, undefined);
372
365
  });
373
366
 
374
367
  // ── the Rewards row (question 6) ─────────────────────────────────────────────
@@ -376,14 +369,12 @@ test('rewards are hidden for a solo founder: the real table hides the row, and t
376
369
  const w = await onReview({ reward_intents: ['cash'] });
377
370
  assert.equal(w.run("rowApplies('rewards')"), false, 'the REAL softLimitsFor hides reward_intents for one-and-one');
378
371
  assert.equal(w.row('rewards').hidden, true, 'and the row goes with it');
379
- assert.ok(!w.run('unconfirmedRows()').includes('rewards'), 'nothing to confirm for a question never asked');
380
- assert.equal(w.run('unconfirmedRows()').length, 7, 'seven rows apply');
381
372
  assert.equal(w.run('createDetail()').reward_intents, undefined);
382
373
  w.change('team');
383
374
  w.q('[data-phys-solo="many"]')[0].click();
384
375
  await w.settle();
385
376
  assert.equal(w.row('rewards').hidden, false, 'a team brings the question back, once the table is asked again');
386
- assert.equal(w.run('unconfirmedRows()').length, 8);
377
+ assert.equal(w.run("rowApplies('rewards')"), true);
387
378
  });
388
379
 
389
380
  test('rewards: a greyed option is disabled and leaves the list, and each reason is said once', async () => {
@@ -412,11 +403,9 @@ test('rewards: a greyed option is disabled and leaves the list, and each reason
412
403
  test('We run: hybrid and local open the suite pre-picked; managed and open source hide it with the table\'s reason', async () => {
413
404
  const w = await onReview({ builders_at_start: 3, builders_expected: 8, reward_intents: ['non-monetary'] });
414
405
  assert.deepEqual(w.rowSays('run'), ['Everything, for you', 'Hosting, payouts and the starter kit, managed.'], 'prefilled: all managed');
415
- w.state.confirmed.app = true;
416
406
  w.change('run');
417
407
  w.pick('framing', 'hybrid');
418
408
  await w.settle();
419
- assert.equal(w.state.confirmed.app, undefined, 'a new framing re-asks the App hosting row');
420
409
  assert.deepEqual(plain(w.a().services), ['hosting', 'starter-kit', 'marketplace-buy', 'guild-access'], 'the canvas\'s hybrid suite');
421
410
  assert.equal(w.$('physSuite').hidden, false);
422
411
  assert.equal(w.$('physSuiteLbl').textContent, 'Your hybrid suite');
@@ -439,7 +428,7 @@ test('We run: hybrid and local open the suite pre-picked; managed and open sourc
439
428
  assert.equal(w.run('createDetail()').services, undefined, 'all managed sends no suite');
440
429
  w.pick('framing', 'managed');
441
430
  assert.equal(w.a().framing, null, 'a second click un-says it');
442
- assert.equal(w.run("rowValid('run')"), false, 'and a run row with no framing cannot be confirmed');
431
+ assert.equal(w.run("rowValid('run')"), false, 'and a run row with no framing cannot be filed');
443
432
  });
444
433
 
445
434
  test('a company already formed hides company creation from the suite', async () => {
@@ -450,14 +439,14 @@ test('a company already formed hides company creation from the suite', async ()
450
439
  });
451
440
 
452
441
  // ── fixed rows: the App hosting row on your own machines, and a demo's own answers ──
453
- test('a planet on its own machines fixes the App hosting row: it says so, confirms without the Render check, and never opens', async () => {
442
+ test('a planet on its own machines fixes the Hosting row: it says so, files without the Render check, and never opens', async () => {
454
443
  for (const [framing, fixed] of [['managed', false], ['hybrid', false], ['local', true], ['open-source', true]]) {
455
444
  const w = await onReview({ framing }, { renderOk: false });
456
445
  assert.equal(w.run("rowFixed('app')"), fixed, framing);
457
446
  assert.equal(w.row('app').querySelector('[data-row-chg]').hidden, fixed, `${framing}: Change is ${fixed ? 'gone' : 'offered'}`);
458
- assert.equal(w.run("confirmRow('app')"), fixed, `${framing}: ${fixed ? 'confirmed without' : 'held by'} the Render check`);
447
+ assert.equal(w.run("rowValid('app')"), fixed, `${framing}: ${fixed ? 'filed without' : 'held by'} the Render check`);
459
448
  if (fixed) assert.equal(w.rowSays('app')[0], 'Runs on your own machines');
460
- else assert.equal(w.isOpen('app'), true, 'a failed check opens the row with its reason');
449
+ else assert.equal(w.run('firstInvalidRow()'), 'app', 'a failed check is what Create opens');
461
450
  }
462
451
  const demo = await onReview({ framing: 'local' }, { renderOk: false, draft: { demo: { people: 3, hours_each: 4 } } });
463
452
  assert.equal(demo.run("rowFixed('app')"), false, 'a demo is all managed, whatever framing a walk left behind');
@@ -476,35 +465,26 @@ test('a demo\'s team, rewards and run are the demo\'s own: fixed, said plainly,
476
465
  assert.deepEqual(w.rowSays('run'), ['Everything, for you', 'A demo is all managed and private to your team.']);
477
466
  });
478
467
 
479
- // ── confirming, and the locked Continue ──────────────────────────────────────
480
- test('one "Looks right, continue" confirms every row that applies, each through its own check (task 1004550)', async () => {
468
+ // ── nothing to confirm (task 1004648: the summary wins) ─────────────────────
469
+ test('no row needs confirming: the prefilled page is fileable as it stands, and the extras stay the server\'s suggestion', async () => {
481
470
  const w = await onReview({ builders_at_start: 3, builders_expected: 8 });
482
- assert.equal(w.go().disabled, false, 'Continue is never locked: it is the confirmation');
483
- assert.equal(w.go().textContent, 'Looks right, continue');
484
- assert.equal(w.row('code').querySelector('[data-row-ok]'), null, 'no row carries a Looks right of its own');
485
- assert.equal(w.run('unconfirmedRows()').length, 8, 'nothing is confirmed before the founder says so');
486
- assert.equal(w.run('validateStep(4)'), true, 'one Continue confirms them all');
487
- for (const key of ['code', 'address', 'app', 'extras', 'team', 'rewards', 'run', 'ship']) assert.equal(w.state.confirmed[key], true, key);
488
- assert.equal(w.state.modules && plain(w.state.modules).join(), 'discord', 'confirming the extras is the answer the create sends');
489
- w.change('team');
490
- assert.equal(w.state.confirmed.team, undefined, 'Edit takes a row back');
491
- assert.equal(w.isOpen('team'), true);
492
- assert.deepEqual(plain(w.run('unconfirmedRows()')), ['team']);
493
- w.done('team');
494
- assert.equal(w.isOpen('team'), false, 'Done closes it again');
495
- assert.equal(w.state.confirmed.team, true, 'and Done is the answer');
471
+ assert.equal(w.run('firstInvalidRow()'), null, 'every row that applies can be filed as it stands');
472
+ for (const key of ['code', 'address', 'team', 'extras', 'rewards', 'app', 'run', 'ship', 'look']) {
473
+ assert.equal(w.row(key).querySelector('[data-row-chg]').textContent, 'Change', `${key}: one Change`);
474
+ assert.equal(w.isOpen(key), false, `${key}: closed`);
475
+ }
476
+ assert.equal(w.state.modules, null, 'the extras are never turned into an answer nobody gave: the server applies the same bundle');
477
+ assert.equal(w.q('#reviewGo').length, 0, 'there is no Continue to press');
478
+ assert.deepEqual(w.rowSays('look'), ['Chrome world', 'Add a logo later from the project page.'], 'the Look row says the look');
479
+ assert.equal(w.$('lookDot').style.background, '#fb9569', 'and wears its accent');
496
480
  });
497
481
 
498
- test('Continue stops on the first row that fails its check, opened with its reason, and confirms nothing past it', async () => {
482
+ test('Create\'s check finds the first row that cannot be filed, in the page\'s order', async () => {
499
483
  const w = await onReview({ builders_at_start: 3, builders_expected: 8 });
500
484
  w.state.physics.answers.builders_at_start = 4; w.state.physics.answers.builders_expected = 2; // fewer later than now
501
- assert.equal(w.run('validateStep(4)'), false, 'Continue refuses');
502
- assert.equal(w.isOpen('team'), true, 'the failing row opens');
503
- assert.equal(w.state.confirmed.team, undefined);
504
- assert.equal(w.state.confirmed.rewards, undefined, 'nothing after it is confirmed on the founder\'s behalf');
505
- assert.equal(w.state.confirmed.code, true, 'the rows before it held, and stay confirmed');
506
- assert.match(w.$('h-review').textContent, /Fix the row that’s open/);
507
- assert.equal(w.$('h-review').className, 'hint err');
485
+ assert.equal(w.run('firstInvalidRow()'), 'team');
486
+ w.run('validateCode = function () { return false; }');
487
+ assert.equal(w.run('firstInvalidRow()'), 'code', 'the earliest row first');
508
488
  });
509
489
 
510
490
  test('opening a row closes another open one whose answer holds, and leaves one that fails open', async () => {
@@ -512,7 +492,7 @@ test('opening a row closes another open one whose answer holds, and leaves one t
512
492
  w.change('code');
513
493
  w.change('team');
514
494
  assert.equal(w.isOpen('code'), false, 'one row open at a time');
515
- assert.equal(w.state.confirmed.code, true, 'closing it that way is the same as Done');
495
+ assert.equal(w.row('code').querySelector('[data-row-chg]').textContent, 'Change', 'closed that way, it reads Change again');
516
496
  w.state.physics.answers.builders_expected = 1; // fewer later than now: the team row fails
517
497
  w.change('run');
518
498
  assert.equal(w.isOpen('team'), true, 'a row that fails its check stays open');
@@ -528,9 +508,9 @@ test('the prefills never overwrite a founder\'s own answer', () => {
528
508
  assert.deepEqual({ s: w.a().project_state, f: w.a().framing, n: [w.a().builders_at_start, w.a().builders_expected], t: w.state.teamShape },
529
509
  { s: 'existing', f: 'hybrid', n: [3, 9], t: 'community' });
530
510
  assert.equal(w.state.mode, 'adopt', 'a project that already has code adopts its repo');
531
- w.state.mode = 'greenfield'; w.state.confirmed = { code: true };
511
+ w.state.mode = 'greenfield'; w.state.modeChosen = true;
532
512
  w.run('physDefaults()');
533
- assert.equal(w.state.mode, 'greenfield', 'but never over a Code row the founder confirmed');
513
+ assert.equal(w.state.mode, 'greenfield', 'but never over a repo choice the founder made');
534
514
  for (const gone of ['physPrefill', 'typePrefill', 'homePrefill', 'physSummary', 'data-phys-goto', 'physStepSkipped', 'physPastSkips']) {
535
515
  assert.ok(!HUB.includes(gone), `${gone} is gone: the review shows every prefill as a row instead`);
536
516
  }
@@ -548,7 +528,7 @@ test('one plain voice for everyone: professional is stored for an organization a
548
528
  assert.doesNotMatch(HUB, /id="(physCoords|physCallout|physCapLbl)"/, 'the profile caption left with the forming planet');
549
529
  });
550
530
 
551
- test('the one voice carries no planet words on the checks and the review', () => {
531
+ test('the one voice carries no planet words on parts 01 and 02', () => {
552
532
  const words = PANEL.replace(/<!--[\s\S]*?-->/g, '').replace(/<[^>]*>/g, ' ');
553
533
  assert.doesNotMatch(words, /\bplanet|\bmoons?\b|\borbit/i, 'the plain copy names no planet, moon or orbit');
554
534
  });
@@ -582,27 +562,22 @@ test('the forming planet is gone, and the room behind the wizard moves on CSS al
582
562
  });
583
563
 
584
564
  // ── the done-when, end to end ────────────────────────────────────────────────
585
- test('a new owner walks from the quick checks through a fully confirmed review, the setup prefilled', async () => {
586
- const w = boot({ draft: { step: 3, description: 'An inventory tool our team started last year.' } });
587
- w.run('paintChecks()');
565
+ test('a new owner walks the page from the quick checks through the rows they change, the rest prefilled', async () => {
566
+ const w = boot({ draft: { description: 'An inventory tool our team started last year.' } });
567
+ w.run('physDefaults(); paintChecks(); paintSetup();');
588
568
  w.$('physCountry').value = 'US'; w.$('physCountry').listeners.change();
589
569
  w.$('physRegion').value = 'US-NY'; w.$('physRegion').listeners.change();
590
570
  w.pick('builder_geography', 'global');
591
571
  w.pick('minors_on_board', 'false');
592
572
  w.$('physHaveEntity').click();
593
573
  w.pick('entity', 'for-profit'); // a company already formed, so equity is on the table
594
- w.run('checksAdvance()');
595
574
  await w.settle();
596
- assert.deepEqual(w.shown, [4], 'the review');
597
575
  w.run('paintSetup()');
598
576
  assert.equal(w.row('rewards').hidden, true, 'one person: no rewards row');
599
- w.ok('code'); w.ok('address'); w.ok('app'); w.ok('extras');
600
577
  w.change('team');
601
578
  w.q('[data-phys-solo="many"]')[0].click();
602
579
  w.done('team');
603
580
  await w.settle();
604
- assert.equal(w.state.confirmed.extras, undefined, 'the team moved the extras\' preselect, so that row is asked again');
605
- w.ok('extras');
606
581
  assert.equal(w.row('rewards').hidden, false);
607
582
  w.change('rewards');
608
583
  w.pick('reward_intents', 'cash');
@@ -612,34 +587,24 @@ test('a new owner walks from the quick checks through a fully confirmed review,
612
587
  w.pick('framing', 'open-source');
613
588
  await w.settle();
614
589
  w.done('run');
615
- assert.equal(w.state.confirmed.app, undefined, 'the framing moved the App hosting row');
616
- assert.equal(w.rowSays('app')[0], 'Runs on your own machines');
617
- assert.equal(w.run('validateStep(4)'), true, 'one Continue confirms the rest');
618
- assert.equal(w.state.confirmed.app, true);
590
+ assert.equal(w.rowSays('app')[0], 'Runs on your own machines', 'the framing moved the Hosting row');
591
+ assert.equal(w.run('firstInvalidRow()'), null, 'and the page can be filed as it stands');
619
592
  const d = plain(w.run('createDetail()'));
620
593
  assert.deepEqual({ voice: d.voice, framing: d.framing, services: d.services, team: [d.builders_at_start, d.builders_expected], shape: d.team_shape, rewards: d.reward_intents },
621
594
  { voice: 'professional', framing: 'open-source', services: undefined, team: [2, 5], shape: 'small-team', rewards: ['cash', 'equity'] });
622
595
  });
623
596
 
624
597
  // ── the address is the founder's own choice, on every path (task 1004507, BV2.PS19) ──
625
- test('no path reaches Look without the founder confirming the address: a prefill suggests it, never confirms it', async () => {
598
+ test('the address is on every path, the demo\'s too: never a fixed row, always changeable, and an address that cannot be filed holds Create', async () => {
626
599
  for (const draft of [{}, { demo: { people: 3, hours_each: 4, idea: '', invites: [] } }]) {
627
600
  const label = draft.demo ? 'demo' : 'real project';
628
601
  const w = await onReview({}, { draft });
629
- assert.equal(w.state.confirmed.address, undefined, `${label}: the prefills leave the address unconfirmed`);
602
+ assert.equal(w.row('address').hidden, false, `${label}: the address row shows`);
630
603
  assert.equal(w.run("rowFixed('address')"), false, `${label}: the address is never a fixed row`);
631
604
  assert.equal(w.row('address').querySelector('[data-row-chg]').hidden, false, `${label}: it can always be changed`);
632
- for (const key of ['code', 'app', 'extras', 'team', 'rewards', 'run', 'ship']) if (w.run(`rowApplies('${key}')`)) w.ok(key);
633
- assert.deepEqual(plain(w.run('unconfirmedRows()')), ['address'], `${label}: every other row confirmed, the address still asked`);
634
- // the address is on the page the founder confirms (task 1004550): only their own
635
- // "Looks right, continue" — or Done on the row — confirms it, and its check still runs
636
605
  w.run('validateAddress = function () { return false; }');
637
- assert.equal(w.run('validateStep(4)'), false, `${label}: an address that fails its check refuses Continue`);
638
- assert.equal(w.isOpen('address'), true, `${label}: and opens the address row`);
639
- assert.equal(w.state.confirmed.address, undefined, `${label}: still unconfirmed`);
606
+ assert.equal(w.run('firstInvalidRow()'), 'address', `${label}: an address that fails its check is what Create opens`);
640
607
  w.run('validateAddress = function () { return true; }');
641
- w.done('address');
642
- assert.equal(w.state.confirmed.address, true, `${label}: choosing it is the answer`);
643
- assert.equal(w.run('validateStep(4)'), true, `${label}: then Continue goes on`);
608
+ assert.equal(w.run('firstInvalidRow()'), null, `${label}: then Create goes on`);
644
609
  }
645
610
  });
@@ -40,7 +40,7 @@ function slice(start, end, what) {
40
40
  // ── 1. the wizard ──────────────────────────────────────────────────────────
41
41
 
42
42
  test('the review asks how finished work ships: Auto (recommended) first and checked, Manual second', () => {
43
- const row = slice('<div class="setRow" data-row="ship">', '<div class="hint" id="h-review">', 'the ship row');
43
+ const row = slice('<div class="setRow" data-row="ship">', '<div class="setRow" data-row="look">', 'the ship row');
44
44
  const auto = row.indexOf('value="auto"'), manual = row.indexOf('value="manual"');
45
45
  assert.ok(auto > 0 && manual > auto, 'Auto comes first');
46
46
  assert.match(row, /<input type="radio" name="shipMode" value="auto" checked>/, 'and is the pick until the founder says otherwise');
@@ -1,129 +0,0 @@
1
- // tests/wizard_six_screens.mjs — the blended create wizard (task 1004504, BV2.PS16; goal 1000121).
2
- //
3
- // The design of record is docs/design/mocks/combined-onboarding/index.html (the owner approved it
4
- // as drawn, 2026-10-01): six screens and the done panel, in this order — sign in, demo or real
5
- // (the terms as its one tick), what are you making, a few quick checks, check what we set up,
6
- // give it a look → Create, done — with the mockup's own words, no progress rail, and a review
7
- // that cannot be skipped: every prefilled answer is a row the founder confirms or changes, and
8
- // Continue stays locked until all of them are confirmed.
9
- //
10
- // This file pins the shape and the words statically. The rows' behaviour is executed by
11
- // tests/wizard_physics_more.mjs (the controller under a DOM stub) and walked end to end by the
12
- // kit probes in projects.probes.json; the night room's tokens and motion by tests/projects_hub.mjs.
13
- //
14
- // Run: node --test tests/wizard_six_screens.mjs
15
-
16
- import assert from 'node:assert/strict';
17
- import { test } from 'node:test';
18
- import fs from 'node:fs';
19
- import path from 'node:path';
20
- import { fileURLToPath } from 'node:url';
21
-
22
- const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
23
- const HUB = fs.readFileSync(path.join(ROOT, 'modules', 'public-landing', 'public', 'projects.html'), 'utf8').replace(/\r\n/g, '\n');
24
- const SCRIPT = HUB.slice(HUB.lastIndexOf('<script>'), HUB.lastIndexOf('</script>'));
25
- const VIEW = (HUB.match(/<section id="view-new" hidden>[\s\S]*?\n<\/section>/) || [''])[0];
26
- const MOCK = fs.readFileSync(path.join(ROOT, 'docs', 'design', 'mocks', 'combined-onboarding', 'index.html'), 'utf8').replace(/\r\n/g, '\n');
27
- const panel = (id) => (VIEW.match(new RegExp(`<section class="panel[^"]*" id="${id}"[\\s\\S]*?\\n {10}</section>`)) || [''])[0];
28
-
29
- const ORDER = ['panel-gate', 'panel-fork', 'panel-what', 'panel-checks', 'panel-review', 'panel-look', 'panel-done'];
30
-
31
- test('six screens and the done panel, in the design of record\'s order, in the markup and in the controller\'s map', () => {
32
- assert.ok(VIEW, 'the ?view=new section is found');
33
- const inMarkup = [...VIEW.matchAll(/<section class="panel[^"]*" id="(panel-[a-z]+)"/g)].map((m) => m[1]);
34
- assert.deepEqual(inMarkup, ORDER, 'the panels appear in this order and there are no others');
35
- const map = (SCRIPT.match(/var panels = \{([^}]*)\}/) || [, ''])[1];
36
- const fromMap = [...map.matchAll(/(\d+):\s*'(panel-[a-z]+)'/g)].map((m) => [Number(m[1]), m[2]]);
37
- assert.deepEqual(fromMap, ORDER.map((id, i) => [i, id]), 'the controller numbers them 0–6 in the same order');
38
- assert.match(SCRIPT, /var DRAFT_KEY = 'cb-create-draft-v9';/, 'a renumbering bumps the draft key, so an older draft is ignored rather than replayed onto the wrong screen');
39
- });
40
-
41
- test('the old screens and the progress rails are gone', () => {
42
- for (const id of ['panel-terms', 'panel-physics', 'panel-name', 'panel-type', 'panel-detail', 'panel-modules', 'panel-home', 'panel-address', 'panel-app']) {
43
- assert.doesNotMatch(HUB, new RegExp(`id="${id}"`), `${id} folded into the six screens`);
44
- }
45
- assert.doesNotMatch(VIEW, /class="steps"|id="physRail"|data-step=/, 'no progress rail, outer or inner');
46
- assert.doesNotMatch(SCRIPT, /function paintRail\(|function physNextQ\(|function physPaintStage\(/, 'nor the code that painted them');
47
- assert.doesNotMatch(VIEW, /id="physStage"|id="forkStage"|class="bongoCell"|class="bongos"/, 'the forming planet, the plate and the drums give way to the night room');
48
- assert.match(VIEW, /<div class="wizTop">\s*<span class="wizEyebrow" id="wizEyebrow">New project<\/span>\s*<button type="button" class="wizBack" id="wizBack" hidden>← Back<\/button>/,
49
- 'one eyebrow names the screen and one Back walks backwards');
50
- });
51
-
52
- test('each screen opens with the mockup\'s own words', () => {
53
- const pairs = [
54
- ['panel-gate', 'Make something.', 'Sign in with GitHub to start your project.'],
55
- ['panel-fork', 'You’re about to start a digital planet.', 'Try a small demo first, or start your real project.'],
56
- ['panel-what', 'What are you making?', null],
57
- ['panel-checks', 'A few quick checks.', 'These decide which legal and payout options we can offer.'],
58
- ['panel-review', 'Check what we set up.', 'We filled these in from your answers. Change anything that’s wrong.'],
59
- ['panel-look', 'Give it a look.', 'So builders always know which project they’re in.'],
60
- ];
61
- for (const [id, h, lede] of pairs) {
62
- const p = panel(id);
63
- assert.ok(p, `${id} is found`);
64
- const mockLine = h.replace(/’/g, '(’|\'|&rsquo;)');
65
- assert.match(MOCK, new RegExp(mockLine.replace(/[.?]/g, '\\$&')), `"${h}" is the mockup's own headline`);
66
- assert.match(p, new RegExp(`<h1 id="[a-z-]+h[0-9]?"?[^>]*>${h.replace(/[.?]/g, '\\$&')}`), `${id} opens "${h}"`);
67
- if (lede) assert.match(p, new RegExp(`<p class="lede">${lede.replace(/[.?]/g, '\\$&')}</p>`), `${id}'s one line`);
68
- }
69
- assert.match(panel('panel-gate'), /Continue with GitHub/, 'the gate\'s door');
70
- assert.match(panel('panel-fork'), /<button type="submit" class="btn big" id="wizStart" disabled>Start my project<\/button>/);
71
- assert.match(panel('panel-fork'), /id="wizDemo"[^>]*disabled>Try the demo<\/button>/, 'both doors wait for the terms tick');
72
- assert.match(panel('panel-fork'), /I can sign for this project, and I accept the <a class="termsRead" href="\/terms"/, 'the terms are one tick on the fork, with the terms one click away');
73
- assert.match(panel('panel-what'), /<span>Name<\/span>[\s\S]*<span id="typeLbl">Kind<\/span>[\s\S]*In a sentence <small>\(optional — puts it on the public map\)<\/small>/, 'name, kind, one optional sentence');
74
- });
75
-
76
- test('planet words survive only as the light touch: the fork and the alive moment', () => {
77
- const words = VIEW.replace(/<!--[\s\S]*?-->/g, '').replace(/<[^>]+>/g, ' ');
78
- const planets = words.match(/planet/gi) || [];
79
- assert.equal(planets.length, 1, 'one planet in the whole view: the fork\'s headline');
80
- assert.match(SCRIPT, /\(state\.name \|\| state\.slug \|\| 'Your project'\) \+ ' is alive\.'/, 'the alive moment');
81
- assert.match(SCRIPT, /if \(inst\.last_verified_up\) \{\n\s*\/\*[^*]*\*\/\n\s*\$\('done-h'\)\.textContent =/, 'said only once the project has answered (Live is a verified claim)');
82
- });
83
-
84
- test('the review cannot be skipped: eight rows, one Edit each, and one Continue that confirms them all (task 1004550)', () => {
85
- const p = panel('panel-review');
86
- const rows = [...p.matchAll(/<div class="setRow" data-row="([a-z]+)">/g)].map((m) => m[1]);
87
- assert.deepEqual(rows, ['code', 'address', 'app', 'extras', 'team', 'rewards', 'run', 'ship']);
88
- for (const key of rows) {
89
- const r = (p.match(new RegExp(`<div class="setRow" data-row="${key}">[\\s\\S]*?<span class="acts">[^\\n]*\\n`)) || [''])[0];
90
- assert.match(r, /<span class="acts"><button type="button" class="rowChg" data-row-chg aria-expanded="false">Edit<\/button><\/span>/, `${key}: one Edit, nothing else`);
91
- assert.match(r, /<div class="opts" hidden>/, `${key}: Edit opens the row's own controls`);
92
- }
93
- assert.doesNotMatch(p, /data-row-ok|data-row-use|Use this/, 'no per-row Looks right and no Use this are left');
94
- assert.match(p, /<button type="submit" class="btn" id="reviewGo">Looks right, continue<\/button>/, 'Continue is the one confirmation, never locked');
95
- assert.match(SCRIPT, /if \(step === 4\) return confirmAllRows\(\);/, 'Continue confirms every row through its own check');
96
- assert.match(SCRIPT, /if \(!confirmRow\(rows\[i\]\)\) \{/, 'and stops on the first that fails');
97
- assert.match(SCRIPT, /if \(unconfirmedRows\(\)\.length\) \{ reopenReview\(\); return; \}/, 'and Create refuses until every row is confirmed');
98
- assert.match(SCRIPT, /chg\.textContent = 'Done';/, 'an open row\'s control reads Done');
99
- });
100
-
101
- test('nothing the old steps could do is lost: their controls are moved whole into the rows', () => {
102
- const p = panel('panel-review');
103
- const has = (row, ids) => {
104
- const r = (p.match(new RegExp(`<div class="setRow" data-row="${row}">[\\s\\S]*?\\n {16}</div>\\n`)) || [''])[0];
105
- for (const id of ids) assert.match(r, new RegExp(`id="${id}"`), `${row} row hosts #${id}`);
106
- };
107
- has('code', ['physExisting', 'c-green', 'c-adopt', 'adoptNote', 'ghNoAccess', 'ghAdopt', 'ghFresh', 'manualHome', 'h-home']);
108
- has('address', ['f-slug', 'addrConfirm', 'addrCustom', 'f-domain', 'f-noaddr']);
109
- has('app', ['appOpts', 'renderLater', 'renderSetup', 'f-renderKey', 'renderWs', 'renderSvc', 'renderNew']);
110
- has('extras', ['modQ', 'modCards', 'modWhy', 'modCoreNote']);
111
- has('team', ['physTeam', 'physStartN', 'physExpected', 'teamCards']);
112
- has('run', ['physSuite']);
113
- has('ship', ['shipOpts']);
114
- for (const id of ['physCountry', 'physRegion', 'physAges', 'physIntent', 'physForms', 'physHaveEntity', 'checksStop', 'physStopMsg', 'physFromAccount']) {
115
- assert.match(panel('panel-checks'), new RegExp(`id="${id}"`), `the quick checks host #${id}`);
116
- }
117
- for (const id of ['demoSetup', 'demoIdea', 'demoInvites', 'demoStart', 'wizResume']) {
118
- assert.match(panel('panel-fork'), new RegExp(`id="${id}"`), `the fork hosts #${id}`);
119
- }
120
- for (const id of ['wizLook', 'wizRecap', 'createAlert', 'doCreate']) {
121
- assert.match(panel('panel-look'), new RegExp(`id="${id}"`), `the look screen hosts #${id}`);
122
- }
123
- /* two answers moved across screens, so two old guarantees are re-pinned where they live now */
124
- assert.match(SCRIPT, /function renderNow\(\) \{ return !appSkipped\(\) && /, 'a framing on the founder\'s own machines rules Render out at Create, whatever was picked before');
125
- assert.match(SCRIPT, /function renderLater\(\) \{ return !appSkipped\(\) && /);
126
- assert.match(SCRIPT, /if \(key === 'extras' && starterBundles\) state\.modules = pickedModules\(\);/, 'confirming the extras before the catalog lands files no answer, never an empty list');
127
- const ids = [...VIEW.matchAll(/\sid="([^"]+)"/g)].map((m) => m[1]);
128
- assert.deepEqual(ids.filter((id, i) => ids.indexOf(id) !== i), [], 'moved, never copied: every id in the view is unique');
129
- });