@bongos/core 1.20.82 → 1.21.1

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 (76) hide show
  1. package/.bongos-core.json +154 -64
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +4 -0
  4. package/clients/bongos-client/index.cjs +4 -0
  5. package/clients/bongos-client/index.d.ts +7 -0
  6. package/clients/bongos-client/index.mjs +4 -0
  7. package/docs/adr/0051-full-session-transcript-corpus.md +3 -1
  8. package/docs/adr/0361-merges-publish-as-candidates-release-decides-what-others-are-offered.md +3 -3
  9. package/docs/adr/README.md +1 -1
  10. package/docs/api/openapi.json +153 -4
  11. package/docs/api-reference.md +5 -3
  12. package/docs/architecture.md +3 -0
  13. package/docs/copy-inventory.md +10 -10
  14. package/docs/copy-registry.json +12 -12
  15. package/docs/design/project-startup-direction.md +63 -0
  16. package/docs/file-map.md +2 -0
  17. package/docs/module-api-changelog.md +8 -0
  18. package/docs/page-inventory.json +8 -3
  19. package/docs/page-readings.json +382 -373
  20. package/docs/recipes/private-npm-distribution.md +1 -1
  21. package/migrations/core_269_session_transcripts.sql +35 -0
  22. package/modules/hall-ui/public/genesis-home.js +17 -3
  23. package/modules/hall-ui/public/index-genesis-demo.states.json +13 -0
  24. package/modules/lifecycle/done-when.js +82 -42
  25. package/modules/lifecycle/workflow-dispatch.js +21 -11
  26. package/modules/npm-release/release.js +10 -1
  27. package/modules/onboarding/founding-birth.js +116 -0
  28. package/modules/onboarding/genesis-home.js +72 -5
  29. package/modules/onboarding/port.js +9 -2
  30. package/modules/onboarding/routes/onboarding.js +8 -1
  31. package/modules/provisioning/birth.js +64 -0
  32. package/modules/provisioning/dns-resolves.js +54 -0
  33. package/modules/provisioning/migrations/provisioning_036_dns_resolved.sql +30 -0
  34. package/modules/provisioning/pollers/liveness-sweep.js +5 -0
  35. package/modules/provisioning/provisioning.js +7 -6
  36. package/modules/provisioning/routes/provisioning.js +6 -2
  37. package/modules/provisioning/tests/liveness.mjs +48 -0
  38. package/modules/public-landing/public/projects-dns-pending.states.json +85 -0
  39. package/modules/public-landing/public/projects.html +40 -15
  40. package/modules/sessions/db.js +58 -0
  41. package/modules/sessions/routes/sessions.js +97 -0
  42. package/modules/ui-design/kit/fixtures/founding-genesis-demo.json +176 -0
  43. package/modules/ui-design/kit/fixtures/me-founding-demo.json +27 -0
  44. package/modules/ui-design/kit/fixtures/provisioning-instances-demo.json +3 -0
  45. package/modules/ui-design/kit/fixtures/provisioning-instances-dns-pending.json +49 -0
  46. package/modules/ui-design/kit/fixtures/provisioning-instances.json +3 -0
  47. package/package-lock.json +2 -2
  48. package/package.json +1 -1
  49. package/release-notes.json +41 -0
  50. package/scripts/gds/provision.js +77 -53
  51. package/scripts/gds/session-digest.js +11 -3
  52. package/scripts/gds/session-transcript-build.js +155 -0
  53. package/scripts/gds/session-transcript-upload.js +29 -0
  54. package/scripts/gds/ship-session-upload.js +6 -1
  55. package/scripts/gds/upgrade-commit-identity.js +32 -0
  56. package/scripts/gds/upgrade.js +1 -1
  57. package/src/bongos/routes.js +3 -0
  58. package/src/branding.js +9 -0
  59. package/src/module-api.js +1 -1
  60. package/tests/auto_satisfy_criteria.mjs +20 -0
  61. package/tests/founding_birth.mjs +200 -0
  62. package/tests/founding_mode.mjs +3 -1
  63. package/tests/genesis_home.mjs +71 -2
  64. package/tests/goal_closure_invariants.mjs +68 -0
  65. package/tests/hub_map_orb_states.mjs +5 -1
  66. package/tests/npm_release_release.mjs +37 -2
  67. package/tests/projects_hub.mjs +5 -3
  68. package/tests/projects_hub_dns_ready.mjs +153 -0
  69. package/tests/projects_hub_pre_uat.mjs +9 -5
  70. package/tests/provision_dns_first.mjs +171 -0
  71. package/tests/provision_settings_apply.mjs +6 -1
  72. package/tests/provisioning_settings_env.mjs +49 -0
  73. package/tests/session_transcript.mjs +273 -0
  74. package/tests/ui_design_kit.mjs +1 -1
  75. package/tests/upgrade_commit_identity.mjs +89 -0
  76. package/tests/wizard_demo.mjs +29 -3
@@ -0,0 +1,200 @@
1
+ // tests/founding_birth.mjs — A NEW PROJECT IS BORN INTO GENESIS, and a demo's genesis is
2
+ // expedited (task 1004506 / BV2.PS18, goal 1000121; spec step 5, D4, D8, D9).
3
+ //
4
+ // Done-when: a new real project and a new demo each open their hall already in genesis;
5
+ // the demo's decide stage needs no choices and its stages fit its time box; a
6
+ // pre-existing hall is unchanged. These tests RUN the instance half
7
+ // (modules/onboarding/founding-birth.js) over an in-memory project_settings store and
8
+ // fake lifecycle port: the birth hook runs once, is idempotent, never runs without the
9
+ // marker, and never after the project came alive. The hub half (the marker on the row
10
+ // and in web.env) is pinned in tests/provisioning_settings_env.mjs and
11
+ // tests/wizard_demo.mjs.
12
+ //
13
+ // Run: node --test tests/founding_birth.mjs
14
+
15
+ import { strict as assert } from 'node:assert';
16
+ import { test } from 'node:test';
17
+ import { createRequire } from 'node:module';
18
+
19
+ const require = createRequire(import.meta.url);
20
+ const birth = require('../modules/onboarding/founding-birth.js');
21
+ const gh = require('../modules/onboarding/genesis-home.js');
22
+ const fm = require('../modules/onboarding/founding-mode.js');
23
+
24
+ const BORN = { born: true, demo: null };
25
+ const BORN_DEMO = { born: true, demo: { people: 3, hours_each: 4 } };
26
+
27
+ function world({ marker = BORN, settings = {}, withLifecycle = true } = {}) {
28
+ const store = new Map(Object.entries(settings));
29
+ const writes = [];
30
+ let seeds = 0;
31
+ const lifecycle = withLifecycle ? {
32
+ seedGenesis: async () => { seeds += 1; return { seeded: 5 }; },
33
+ genesisTasks: async () => ({}),
34
+ } : null;
35
+ const deps = () => ({
36
+ marker,
37
+ getSetting: async (k) => (store.has(k) ? { key: k, value: store.get(k) } : null),
38
+ setSetting: async (k, v) => { writes.push(k); store.set(k, String(v)); return { key: k, value: String(v) }; },
39
+ lifecycle, government: null, now: new Date('2026-10-01T09:00:00Z'),
40
+ });
41
+ return { store, writes, deps, seeds: () => seeds };
42
+ }
43
+
44
+ // ── the marker ───────────────────────────────────────────────────────────────
45
+
46
+ test('the marker: born only with a readable birth stamp; a demo box only inside its limits', () => {
47
+ assert.deepEqual(birth.birthMarker(undefined), { born: false, demo: null });
48
+ assert.deepEqual(birth.birthMarker({}), { born: false, demo: null });
49
+ assert.deepEqual(birth.birthMarker({ birth: 'whenever' }), { born: false, demo: null });
50
+ assert.deepEqual(birth.birthMarker({ birth: '2026-10-01T00:00:00.000Z' }), { born: true, demo: null });
51
+ assert.deepEqual(birth.birthMarker({ birth: '2026-10-01T00:00:00.000Z', demoPeople: '3', demoHours: '4' }),
52
+ { born: true, demo: { people: 3, hours_each: 4 } });
53
+ assert.deepEqual(birth.birthMarker({ birth: '2026-10-01T00:00:00.000Z', demoPeople: '99', demoHours: '4' }),
54
+ { born: true, demo: null }, 'a malformed box still founds, with the ordinary plan');
55
+ assert.deepEqual(birth.birthMarker({ demoPeople: '3', demoHours: '4' }), { born: false, demo: null },
56
+ 'a demo box without a birth is not a birth');
57
+ });
58
+
59
+ // ── the birth hook ───────────────────────────────────────────────────────────
60
+
61
+ test('no marker: nothing runs, and the database is never read (a pre-existing hall is unchanged)', async () => {
62
+ const r = await birth.bornFounding({
63
+ marker: { born: false, demo: null },
64
+ getSetting: async () => { throw new Error('read without a marker'); },
65
+ setSetting: async () => { throw new Error('write without a marker'); },
66
+ });
67
+ assert.deepEqual(r, { ran: false, reason: 'no_marker' });
68
+ });
69
+
70
+ test('a new real project is born deciding: founding on, stage decide, no plan, nothing filed', async () => {
71
+ const w = world();
72
+ const r = await birth.bornFounding(w.deps());
73
+ assert.deepEqual(r, { ran: true, started: true, plan: false, demo: false });
74
+ const mode = await fm.foundingModeFor({ getSetting: w.deps().getSetting, government: null });
75
+ assert.equal(mode.active, true);
76
+ assert.equal(mode.stage, 'decide');
77
+ assert.equal(w.store.get(gh.PLAN_KEY), undefined, 'a real founder settles their own plan');
78
+ assert.equal(w.seeds(), 0);
79
+ });
80
+
81
+ test('idempotent: a second run (a restart, a settings push) changes nothing', async () => {
82
+ const w = world();
83
+ await birth.bornFounding(w.deps());
84
+ const before = new Map(w.store);
85
+ w.writes.length = 0;
86
+ assert.deepEqual(await birth.bornFounding(w.deps()), { ran: false, reason: 'already_started' });
87
+ assert.deepEqual(w.writes, [], 'no write at all');
88
+ assert.deepEqual([...w.store], [...before]);
89
+ });
90
+
91
+ test('never after the project came alive: genesis happens once', async () => {
92
+ const w = world({ settings: {
93
+ [fm.KEYS.ACTIVE]: 'false', [fm.KEYS.STARTED_AT]: '2026-09-01T00:00:00.000Z', [fm.KEYS.ENDED_AT]: '2026-09-10T00:00:00.000Z',
94
+ } });
95
+ assert.deepEqual(await birth.bornFounding(w.deps()), { ran: false, reason: 'already_alive' });
96
+ assert.deepEqual(w.writes, []);
97
+ assert.equal(await fm.foundingModeFor({ getSetting: w.deps().getSetting, government: null }), null, 'the hall stays alive');
98
+ });
99
+
100
+ test('a founding already under way (started by hand, mid-stage) is left exactly where it is', async () => {
101
+ const w = world({ marker: BORN_DEMO, settings: {
102
+ [fm.KEYS.ACTIVE]: 'true', [fm.KEYS.STAGE]: 'strategize', [fm.KEYS.STARTED_AT]: '2026-09-28T00:00:00.000Z',
103
+ } });
104
+ assert.deepEqual(await birth.bornFounding(w.deps()), { ran: false, reason: 'already_started' });
105
+ assert.deepEqual(w.writes, []);
106
+ assert.equal(w.seeds(), 0);
107
+ });
108
+
109
+ test('a new demo is born past decide: the plan is settled from the demo, and its stages are filed', async () => {
110
+ const w = world({ marker: BORN_DEMO });
111
+ const r = await birth.bornFounding(w.deps());
112
+ assert.deepEqual(r, { ran: true, started: true, plan: true, demo: true });
113
+ assert.equal(w.seeds(), 1, 'the five stages filed as tasks, once');
114
+ const plan = JSON.parse(w.store.get(gh.PLAN_KEY));
115
+ assert.equal(plan.government, 'monarchy', 'Monarchy / BDFL: the founder\'s one yes closes each stage (D4)');
116
+ assert.equal(plan.passes, 1);
117
+ assert.equal(plan.advisors, 'none');
118
+ assert.equal(plan.expected_builders, 3, 'the demo\'s people');
119
+ assert.deepEqual(plan.demo, { people: 3, hours_each: 4 });
120
+ const mode = await fm.foundingModeFor({ getSetting: w.deps().getSetting, government: null });
121
+ assert.equal(mode.stage, 'scope', 'the decide stage needs no choices: it is already behind');
122
+ // Again: nothing more.
123
+ w.writes.length = 0;
124
+ assert.deepEqual(await birth.bornFounding(w.deps()), { ran: false, reason: 'already_started' });
125
+ assert.deepEqual(w.writes, []);
126
+ assert.equal(w.seeds(), 1, 'never filed twice');
127
+ });
128
+
129
+ test('a demo whose plan could not be saved yet (no board) asks for a retry, and the retry settles it', async () => {
130
+ birth.resetBirthForTests();
131
+ const store = new Map();
132
+ let seeds = 0;
133
+ const base = {
134
+ marker: BORN_DEMO,
135
+ getSetting: async (k) => (store.has(k) ? { key: k, value: store.get(k) } : null),
136
+ setSetting: async (k, v) => { store.set(k, String(v)); },
137
+ government: null,
138
+ };
139
+ const first = await birth.ensureBirth({ ...base, lifecycle: null });
140
+ assert.equal(first.retry, true);
141
+ assert.equal(first.reason, 'no_board');
142
+ assert.equal(store.get(fm.KEYS.STAGE) || 'decide', 'decide');
143
+ const second = await birth.ensureBirth({ ...base, lifecycle: { seedGenesis: async () => { seeds += 1; return { seeded: 5 }; } } });
144
+ assert.deepEqual(second, { ran: true, started: false, plan: true, demo: true }, 'the retry saves the plan without restarting founding');
145
+ assert.equal(seeds, 1);
146
+ assert.equal(store.get(fm.KEYS.STAGE), 'scope');
147
+ birth.resetBirthForTests();
148
+ });
149
+
150
+ test('once per process: concurrent first reads share ONE run', async () => {
151
+ birth.resetBirthForTests();
152
+ const w = world({ marker: BORN_DEMO });
153
+ const results = await Promise.all([birth.ensureBirth(w.deps()), birth.ensureBirth(w.deps()), birth.ensureBirth(w.deps())]);
154
+ assert.equal(results[0], results[1]);
155
+ assert.equal(results[1], results[2]);
156
+ assert.equal(w.seeds(), 1);
157
+ assert.equal(w.writes.filter((k) => k === fm.KEYS.STARTED_AT).length, 1, 'started once');
158
+ assert.equal(w.writes.filter((k) => k === gh.PLAN_KEY).length, 1, 'planned once');
159
+ // A later read reuses the settled run; it does not touch the store again.
160
+ w.writes.length = 0;
161
+ await birth.ensureBirth(w.deps());
162
+ assert.deepEqual(w.writes, []);
163
+ birth.resetBirthForTests();
164
+ });
165
+
166
+ test('a run that throws is forgotten, so a later read tries again; the read itself never fails', async () => {
167
+ birth.resetBirthForTests();
168
+ let fail = true;
169
+ const store = new Map();
170
+ const deps = {
171
+ marker: BORN,
172
+ getSetting: async (k) => { if (fail) throw new Error('db down'); return store.has(k) ? { value: store.get(k) } : null; },
173
+ setSetting: async (k, v) => { store.set(k, String(v)); },
174
+ };
175
+ assert.deepEqual(await birth.ensureBirth(deps), { ran: false, reason: 'error' });
176
+ fail = false;
177
+ assert.equal((await birth.ensureBirth(deps)).ran, true);
178
+ assert.equal(store.get(fm.KEYS.ACTIVE), 'true');
179
+ birth.resetBirthForTests();
180
+ });
181
+
182
+ // ── the wiring ───────────────────────────────────────────────────────────────
183
+
184
+ test('the /me read (the port, no arguments) runs the birth first; a caller with its own deps does not', async () => {
185
+ const port = require('../modules/onboarding/port.js');
186
+ const orig = { ensure: birth.ensureBirth, read: fm.foundingModeFor };
187
+ const calls = [];
188
+ birth.ensureBirth = async () => { calls.push('birth'); };
189
+ fm.foundingModeFor = async (d) => { calls.push(d ? 'read:deps' : 'read'); return null; };
190
+ try {
191
+ await port.foundingModeFor();
192
+ assert.deepEqual(calls, ['birth', 'read'], 'the birth lands before the read, so the first hall paint is already founding');
193
+ calls.length = 0;
194
+ await port.foundingModeFor({ getSetting: async () => null });
195
+ assert.deepEqual(calls, ['read:deps']);
196
+ } finally {
197
+ birth.ensureBirth = orig.ensure;
198
+ fm.foundingModeFor = orig.read;
199
+ }
200
+ });
@@ -278,7 +278,9 @@ test('/me carries founding_mode from the onboarding port, fail-open to null', ()
278
278
  assert.match(ME, /onboarding\.foundingModeFor\(\)/);
279
279
  const at = ME.indexOf('onboarding.foundingModeFor()');
280
280
  assert.match(ME.slice(at, at + 200), /catch \(_\) \{ return null; \}/);
281
- assert.match(PORT, /foundingModeFor: \(\.\.\.a\) => foundingMode\.foundingModeFor\(\.\.\.a\)/);
281
+ // The port delegates at call time; a no-argument read (me.js) first runs the guarded
282
+ // birth (task 1004506, tests/founding_birth.mjs).
283
+ assert.match(PORT, /foundingModeFor: async \(\.\.\.a\) => \{\s*if \(!a\.length\) await foundingBirth\.ensureBirth\(\);\s*return foundingMode\.foundingModeFor\(\.\.\.a\);/);
282
284
  });
283
285
 
284
286
  test('BOTH applyAccess callers pass it, or the index would never show the band', () => {
@@ -91,6 +91,58 @@ test('a plan out of range is refused with the reason', () => {
91
91
  assert.equal(gh.parsePlan({ expected_builders: 0 }).reason, 'bad_expected_builders');
92
92
  });
93
93
 
94
+ // ── the demo's expedited plan (task 1004506) ────────────────────────────────
95
+
96
+ test('a demo\'s stages are minutes, fitted to a fifth of its time box (10 min to 2 h)', () => {
97
+ const sum = (m) => Object.values(m).reduce((a, b) => a + b, 0);
98
+ assert.deepEqual(gh.demoMinutes(4), { scope: 14, strategize: 10, charter: 10, legalize: 5, legislation: 10 },
99
+ '3 people × 4 hours: about 48 minutes of genesis, then building');
100
+ assert.deepEqual(Object.keys(gh.demoMinutes(1)), STAGES5);
101
+ for (const h of [1, 2, 4, 8, 20, 40]) {
102
+ const total = sum(gh.demoMinutes(h));
103
+ assert.ok(total <= h * 60 * 0.25, `${h}h: genesis (${total} min) leaves most of the time box for building`);
104
+ assert.ok(total >= 8 && total <= 125, `${h}h: ${total} min is inside the bounds`);
105
+ assert.ok(Object.values(gh.demoMinutes(h)).every((m) => Number.isInteger(m) && m >= 1));
106
+ }
107
+ });
108
+
109
+ test('the demo plan is the settled one: Monarchy / BDFL, one pass, no advisors, the demo\'s people', () => {
110
+ const r = gh.parsePlan(gh.demoPlan({ people: 3, hours_each: 4 }));
111
+ assert.equal(r.ok, true, JSON.stringify(r));
112
+ assert.equal(r.plan.government, 'monarchy');
113
+ assert.equal(r.plan.passes, 1);
114
+ assert.equal(r.plan.advisors, 'none');
115
+ assert.equal(r.plan.expected_builders, 3);
116
+ assert.equal(r.plan.existing, false);
117
+ assert.deepEqual(r.plan.demo, { people: 3, hours_each: 4 });
118
+ assert.deepEqual(r.plan.minutes, gh.demoMinutes(4));
119
+ assert.equal(gh.parsePlan({ demo: { people: 0, hours_each: 4 } }).reason, 'bad_demo');
120
+ assert.equal(gh.parsePlan({ minutes: { scope: 5 } }).reason, 'bad_minutes', 'every stage needs its minutes');
121
+ assert.equal(gh.parsePlan({}).plan.minutes, undefined, 'a real project\'s plan stays in days');
122
+ });
123
+
124
+ test('the plan route does not name the demo fields, so only the birth can write a demo plan', () => {
125
+ const src = fs.readFileSync(path.join(ROOT, 'modules', 'onboarding', 'routes', 'onboarding.js'), 'utf8');
126
+ const schema = src.slice(src.indexOf("router.post('/founding/plan'"), src.indexOf("router.post('/founding/stages"));
127
+ assert.ok(schema.includes('validateOrRespond'));
128
+ assert.doesNotMatch(schema, /\bdemo\b|\bminutes\b/);
129
+ });
130
+
131
+ test('a demo genesis reads in minutes: each stage due minutes after it began', () => {
132
+ const plan = gh.parsePlan(gh.demoPlan({ people: 2, hours_each: 4 })).plan;
133
+ const g = gh.shapeGenesis({
134
+ mode: { stage: 'scope', stage_number: 1, stage_count: 5, day: 1 },
135
+ plan, stageAt: { scope: '2026-10-01T09:00:00.000Z' },
136
+ });
137
+ assert.deepEqual(g.demo, { people: 2, hours_each: 4 });
138
+ assert.equal(g.stages[0].minutes, 14);
139
+ assert.equal(g.stages[0].due, '2026-10-01T09:14:00.000Z');
140
+ const real = gh.shapeGenesis({ mode: { stage: 'scope', stage_number: 1, stage_count: 5, day: 1 }, plan: gh.parsePlan({}).plan, stageAt: { scope: '2026-10-01T09:00:00.000Z' } });
141
+ assert.equal(real.demo, null);
142
+ assert.equal(real.stages[0].minutes, null);
143
+ assert.equal(real.stages[0].due, '2026-10-08T09:00:00.000Z', 'a real project is unchanged: days');
144
+ });
145
+
94
146
  // ── the flow, end to end over fakes ──────────────────────────────────────────
95
147
 
96
148
  function world({ withGovernment = true } = {}) {
@@ -232,7 +284,7 @@ test('every genesis write is the project owner\'s; the read is any signed-in bui
232
284
 
233
285
  const CARD = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'genesis-home.js'), 'utf8');
234
286
 
235
- function bootCard({ founding }) {
287
+ function bootCard({ founding, data = { genesis: null } }) {
236
288
  const calls = [];
237
289
  let widget = null;
238
290
  let observer = null;
@@ -240,7 +292,7 @@ function bootCard({ founding }) {
240
292
  const sandbox = {
241
293
  window: {
242
294
  OTB: { escapeHtml: (s) => String(s), contributeHallWidget: (w) => { widget = w; } },
243
- BongosClient: { createClient: () => ({ request: async (m, p) => { calls.push(`${m} ${p}`); return { ok: true, status: 200, data: { genesis: null } }; } }) },
295
+ BongosClient: { createClient: () => ({ request: async (m, p) => { calls.push(`${m} ${p}`); return { ok: true, status: 200, data }; } }) },
244
296
  },
245
297
  document: { documentElement: { getAttribute: (k) => attrs[k] || null } },
246
298
  MutationObserver: class { constructor(fn) { observer = fn; } observe() {} disconnect() {} },
@@ -277,6 +329,23 @@ test('founding mode stamped after mount still starts the card, once', async () =
277
329
  assert.deepEqual(b.calls, ['GET /api/bongos/founding/genesis']);
278
330
  });
279
331
 
332
+ test('a demo\'s genesis card: no decide form, the settled plan in one line, each stage in minutes (task 1004506)', async () => {
333
+ const genesis = gh.shapeGenesis({
334
+ mode: { stage: 'scope', stage_number: 1, stage_count: 5, day: 1 },
335
+ plan: gh.parsePlan(gh.demoPlan({ people: 3, hours_each: 4 })).plan,
336
+ stageAt: { scope: '2026-10-01T09:00:00.000Z' },
337
+ });
338
+ const { host } = bootCard({ founding: true, data: { genesis, viewer_id: '1' } });
339
+ await new Promise((r) => setImmediate(r));
340
+ await new Promise((r) => setImmediate(r));
341
+ assert.equal(host.hidden, false);
342
+ assert.match(host.innerHTML, /Monarchy \/ BDFL, 3 people, 4 hours each/);
343
+ assert.match(host.innerHTML, /about 49 minutes/);
344
+ assert.match(host.innerHTML, /14 min, due /, 'the stage in progress is due at a time, not a date');
345
+ assert.match(host.innerHTML, /Stage 1 of 5 · scope · demo/);
346
+ assert.doesNotMatch(host.innerHTML, /data-genesis-plan/, 'a demo never shows the decide form');
347
+ });
348
+
280
349
  test('the hall carries the five life plates byte-identical to the world\'s', () => {
281
350
  const world = path.join(ROOT, 'modules', 'public-landing', 'public', 'assets', 'world');
282
351
  const hall = path.join(ROOT, 'modules', 'hall-ui', 'public', 'genesis');
@@ -95,6 +95,14 @@ function makeExec(w) {
95
95
  return { rows: closed.map((c) => ({ ...c, satisfied_by_task_id: scoped })) };
96
96
  }
97
97
 
98
+ // (1b) task 1004513: the shipping task's OWN goal, asked only when the ship
99
+ // flipped no criterion — the case where every criterion was already
100
+ // satisfied and the goal has nothing left but this task.
101
+ if (/SELECT goal_id FROM tasks WHERE id = \$1/.test(s)) {
102
+ const t = taskById(Number(params[0]));
103
+ return { rows: t && t.goal_id != null ? [{ goal_id: t.goal_id }] : [] };
104
+ }
105
+
98
106
  // (2) the cascade. NOT hand-coded: the model reads the statement it was
99
107
  // actually handed and honours only the clauses PRESENT in it — including
100
108
  // the terminal set, parsed out of the SQL rather than imported. That is
@@ -265,6 +273,66 @@ test('every statement goes through the caller exec — none reaches the module p
265
273
  assert.match(exec.calls[3].sql, /UPDATE versions v/);
266
274
  });
267
275
 
276
+ // ---------------------------------------------------------------------------
277
+ // 2b. The ship that flips NO criterion still finishes its goal (task 1004513)
278
+ //
279
+ // The hole this pins: satisfy every criterion of a goal while one of its tasks is
280
+ // still open. The manual door (achieveGoalIfComplete, off POST /done-when/:id/
281
+ // satisfy) runs then and correctly declines — a task is unfinished. When that
282
+ // task later ships, the cascade flips no criterion (they are already satisfied),
283
+ // and before this fix the no-criteria early return meant the goal was never
284
+ // re-asked: it sat `open` with everything done. Live on goal 1000119, 2026-10-01.
285
+ // ---------------------------------------------------------------------------
286
+
287
+ test('a goal whose criteria were ALL satisfied before its last task shipped still closes', async () => {
288
+ const w = world({ criteria: [{ satisfied: true, task_ids: [200] }], tasks: [SHIPPED()] });
289
+ const exec = makeExec(w);
290
+ const out = await autoSatisfyShippedCriteria(exec, { taskId: 200 });
291
+ assert.deepEqual(out.criteria, [], 'the ship flips no criterion — they were satisfied already');
292
+ assert.equal(out.goals.length, 1, 'and the goal still reaches achieved');
293
+ assert.equal(w.goals[0].status, 'achieved');
294
+ // The wiring, not just the verdict: it asks for the shipping task's own goal
295
+ // and then runs the SAME cascade statement, on the same exec.
296
+ assert.match(exec.calls[0].sql, /UPDATE done_when_criteria/);
297
+ assert.match(exec.calls[1].sql, /SELECT goal_id FROM tasks WHERE id = \$1/);
298
+ assert.match(exec.calls[2].sql, /UPDATE goals g/);
299
+ assert.deepEqual(exec.calls[2].params, [[77]], 'scoped to the shipping task\'s goal, nothing wider');
300
+ assert.match(exec.calls[3].sql, /UPDATE goal_membership_requests/, 'an achieved goal closes its pending asks on this path too (task 1003660)');
301
+ assert.match(exec.calls[4].sql, /UPDATE versions v/, 'and the version close still rides along');
302
+ });
303
+
304
+ for (const status of HOLDS_OPEN) {
305
+ test(`the no-criterion re-check honours the same rule: a '${status}' sibling still holds the goal open`, async () => {
306
+ const w = world({ criteria: [{ satisfied: true, task_ids: [200] }], tasks: [SHIPPED(), { status }] });
307
+ const out = await autoSatisfyShippedCriteria(makeExec(w), { taskId: 200 });
308
+ assert.deepEqual(out.goals, [], 'the re-check is the rule, not a bypass of it');
309
+ assert.equal(w.goals[0].status, 'open');
310
+ });
311
+ }
312
+
313
+ test('the no-criterion re-check reads through the exec it was handed', async () => {
314
+ // Same instant, two views. Inside the ship transaction task 200 is already
315
+ // `shipped`; on the module pool it is still `confirmed` — non-terminal — and
316
+ // the goal must not close. The pair is what proves the read goes through exec.
317
+ const inTxn = world({ criteria: [{ satisfied: true, task_ids: [200] }], tasks: [SHIPPED()] });
318
+ const outsideTxn = world({ criteria: [{ satisfied: true, task_ids: [200] }], tasks: [{ status: 'confirmed' }] });
319
+ assert.equal((await autoSatisfyShippedCriteria(makeExec(inTxn), { taskId: 200 })).goals.length, 1);
320
+ assert.deepEqual((await autoSatisfyShippedCriteria(makeExec(outsideTxn), { taskId: 200 })).goals, []);
321
+ assert.equal(outsideTxn.goals[0].status, 'open');
322
+ });
323
+
324
+ test('a goal-less task closes nothing, and the UNSCOPED sweep keeps its cheap early return', async () => {
325
+ const orphan = world({ criteria: [{ satisfied: true, task_ids: [200] }], tasks: [SHIPPED({ goal_id: null })] });
326
+ const orphanExec = makeExec(orphan);
327
+ assert.deepEqual((await autoSatisfyShippedCriteria(orphanExec, { taskId: 200 })).goals, []);
328
+ assert.equal(orphanExec.calls.length, 2, 'it asks for the goal, finds none, and stops');
329
+
330
+ const sweep = makeExec(world({ criteria: [{ satisfied: true, task_ids: [200] }], tasks: [SHIPPED()] }));
331
+ const out = await autoSatisfyShippedCriteria(sweep, {});
332
+ assert.deepEqual(out, { criteria: [], goals: [] }, 'the sweep holds no task, so it has no goal to name');
333
+ assert.equal(sweep.calls.length, 1, 'and issues no second statement');
334
+ });
335
+
268
336
  // ---------------------------------------------------------------------------
269
337
  // 3. A ship still LANDS while its goal is held open
270
338
  // ---------------------------------------------------------------------------
@@ -66,6 +66,10 @@ const PA_START = SCRIPT.indexOf('function planetAttrs(');
66
66
  const PA_END = SCRIPT.indexOf('var PLANET_SHAPE_WORDS');
67
67
  assert.ok(PA_START > 0 && PA_END > PA_START, 'planetAttrs must still be sliceable');
68
68
  const PLANET_ATTRS = SCRIPT.slice(PA_START, PA_END);
69
+ // The row's site door (task 1004505) and the rule it asks, sliced the same way.
70
+ const ADDRESS_READY = (SCRIPT.match(/function addressReady\(inst\) \{[^\n]*\}/) || [''])[0];
71
+ const SITE_HREF = (SCRIPT.match(/function siteHref\(p\) \{[^\n]*\}/) || [''])[0];
72
+ assert.ok(ADDRESS_READY && SITE_HREF, 'addressReady and siteHref must still be sliceable');
69
73
 
70
74
  // ── the DOM stub ─────────────────────────────────────────────────────────────
71
75
  // Narrow on purpose: it supports exactly what the region touches, so it stays
@@ -111,7 +115,7 @@ function boot() {
111
115
  listedDate: (iso) => String(iso),
112
116
  };
113
117
  vm.createContext(sandbox);
114
- vm.runInContext(PLANET_ATTRS + '\n' + REGION, sandbox);
118
+ vm.runInContext(PLANET_ATTRS + '\n' + ADDRESS_READY + '\n' + SITE_HREF + '\n' + REGION, sandbox);
115
119
  return {
116
120
  sandbox,
117
121
  sky,
@@ -99,6 +99,41 @@ test('an unreadable registry and a missing dispatcher both release nothing, and
99
99
  assert.equal((await requestRelease({ version: '1.20.73', registry: REG, dispatch: null })).body.error.code, 'dispatch_unavailable');
100
100
  });
101
101
 
102
+ // ── the refusal names the credential the server actually uses (task 1004518) ─────────
103
+
104
+ test('a server on the access token is told to edit the ACCESS TOKEN', async () => {
105
+ const saved = process.env.GITHUB_PUSH_TOKEN;
106
+ process.env.GITHUB_PUSH_TOKEN = 'PAT';
107
+ try {
108
+ const d = await gp.dispatchWorkflow({ workflow: 'publish.yml' }, { appToken: async () => null, repoInfo: REPO, fetchImpl: async () => ({ ok: false, status: 403 }) });
109
+ assert.equal(d.credential, 'token');
110
+ const r = await requestRelease({ version: '1.20.73', registry: REG, dispatch: async () => d });
111
+ assert.match(r.body.error.message, /access token \(GITHUB_PUSH_TOKEN/);
112
+ assert.doesNotMatch(r.body.error.message, /GitHub App/);
113
+ } finally {
114
+ if (saved === undefined) delete process.env.GITHUB_PUSH_TOKEN; else process.env.GITHUB_PUSH_TOKEN = saved;
115
+ }
116
+ });
117
+
118
+ test('a server on a GitHub App is told to edit the APP — the App wins over a token, as resolveToken orders them', async () => {
119
+ const saved = process.env.GITHUB_PUSH_TOKEN;
120
+ process.env.GITHUB_PUSH_TOKEN = 'PAT';
121
+ try {
122
+ let auth = null;
123
+ const d = await gp.dispatchWorkflow({ workflow: 'publish.yml' }, {
124
+ appToken: async () => 'APPTOK', repoInfo: REPO,
125
+ fetchImpl: async (_u, opts) => { auth = opts.headers.Authorization || opts.headers.authorization; return { ok: false, status: 403 }; },
126
+ });
127
+ assert.equal(d.credential, 'app');
128
+ assert.match(String(auth), /APPTOK/, 'the App token is the one sent');
129
+ const r = await requestRelease({ version: '1.20.73', registry: REG, dispatch: async () => d });
130
+ assert.match(r.body.error.message, /the server's GitHub App needs/);
131
+ assert.doesNotMatch(r.body.error.message, /access token/);
132
+ } finally {
133
+ if (saved === undefined) delete process.env.GITHUB_PUSH_TOKEN; else process.env.GITHUB_PUSH_TOKEN = saved;
134
+ }
135
+ });
136
+
102
137
  test('GitHub refusing for want of Actions: write is named as exactly that', async () => {
103
138
  const r = await requestRelease({ version: '1.20.73', registry: REG, dispatch: async () => ({ ok: false, code: 'REFUSED', status: 403 }) });
104
139
  assert.equal(r.status, 502);
@@ -113,7 +148,7 @@ test('dispatchWorkflow POSTs ref + inputs to the workflow\'s dispatches endpoint
113
148
  let captured = null;
114
149
  const fetchImpl = async (url, opts) => { captured = { url, opts, body: JSON.parse(opts.body) }; return { ok: false, status: 204 }; };
115
150
  const r = await gp.dispatchWorkflow({ workflow: 'publish.yml', ref: 'main', inputs: { release_version: '1.20.73' } }, { token: 'T', repoInfo: REPO, fetchImpl });
116
- assert.deepEqual(r, { ok: true });
151
+ assert.deepEqual(r, { ok: true, credential: 'token' });
117
152
  assert.equal(captured.url, 'https://api.github.com/repos/someowner/somerepo/actions/workflows/publish.yml/dispatches');
118
153
  assert.equal(captured.opts.method, 'POST');
119
154
  assert.deepEqual(captured.body, { ref: 'main', inputs: { release_version: '1.20.73' } });
@@ -135,7 +170,7 @@ test('dispatchWorkflow: no credential is UNCONFIGURED; a 403 is REFUSED with its
135
170
  } finally {
136
171
  if (saved !== undefined) process.env.GITHUB_PUSH_TOKEN = saved;
137
172
  }
138
- assert.deepEqual(await gp.dispatchWorkflow({ workflow: 'publish.yml' }, { token: 'T', repoInfo: REPO, fetchImpl: async () => ({ ok: false, status: 403 }) }), { ok: false, code: 'REFUSED', status: 403 });
173
+ assert.deepEqual(await gp.dispatchWorkflow({ workflow: 'publish.yml' }, { token: 'T', repoInfo: REPO, fetchImpl: async () => ({ ok: false, status: 403 }) }), { ok: false, code: 'REFUSED', status: 403, credential: 'token' });
139
174
  assert.equal((await gp.dispatchWorkflow({ workflow: 'publish.yml' }, { token: 'T', repoInfo: REPO, fetchImpl: async () => { throw new Error('down'); } })).code, 'UNREACHABLE');
140
175
  });
141
176
 
@@ -765,7 +765,9 @@ test('the table is in the world: a masthead, then ledger rows — no panel, no s
765
765
  assert.match(COSMOS, /\.tbl \.tblRow:hover\{border-top-color:var\(--ink-faint\);\}/, 'hover is the hairline taking ink, not an accent tint');
766
766
  assert.match(COSMOS, /\.rowGo\{[^}]*border:1px solid var\(--rule\);border-radius:var\(--radius-pill\);/, 'the row\'s door is the mine row\'s hairline pill');
767
767
  assert.match(HUB, /<a class="rowGo" href="\/">view<\/a>/, 'the self row\'s door');
768
- assert.match(SCRIPT, /'<a class="rowGo" href="' \+ esc\(safeHttpUrl\(p\.origin\)\)/, 'the generated rows\' door');
768
+ assert.match(SCRIPT, /'<a class="rowGo" href="' \+ esc\(siteHref\(p\)\)/, 'the generated rows\' door');
769
+ assert.match(SCRIPT, /function siteHref\(p\) \{ return p\._inst && !addressReady\(p\._inst\) \? '' : safeHttpUrl\(p\.origin\); \}/,
770
+ 'scheme-checked, and none for your own project until its address answers (task 1004505)');
769
771
  assert.match(COSMOS, /\.mastObj\{display:block;--dia:clamp\(96px,10vw,132px\);[^}]*background-image:var\(--plate-set-cut\);/, 'the object is the set\'s small bead by token, in both modes');
770
772
  assert.doesNotMatch(COSMOS, /mix-blend-mode:var\(--mast/, 'the cutout needs no blend — the view wrap is a stacking context');
771
773
  });
@@ -1426,7 +1428,7 @@ test('the save sends the choice and shows the platform’s receipt verbatim —
1426
1428
  test('what the project RUNS comes only from its own manifest, and every answer is said', () => {
1427
1429
  const runs = fnHub('loadVisRuns');
1428
1430
  const manifest = fnHub('ensureManifest');
1429
- assert.match(runs, /inst\.status !== 'active' \|\| !inst\.domain/, 'only a live project with an address can be asked');
1431
+ assert.match(runs, /inst\.status !== 'active' \|\| !addressReady\(inst\)/, 'only a live project whose address answers can be asked (task 1004505)');
1430
1432
  assert.match(manifest, /'https:\/\/' \+ domain \+ '\/api\/bongos\/instance'/,
1431
1433
  'the same public GET /instance the platform’s runner reads after a restart');
1432
1434
  assert.doesNotMatch(runs, /fetch\(/, 'the card fires no GET of its own — one manifest read serves the pair (task 1003554)');
@@ -1631,7 +1633,7 @@ test('the joinability save sends the one key and shows the platform’s receipt
1631
1633
 
1632
1634
  test('what the project RUNS for joinability comes only from its own manifest, and every answer is said', () => {
1633
1635
  const runs = fnHub('loadJoinRuns');
1634
- assert.match(runs, /inst\.status !== 'active' \|\| !inst\.domain/, 'only a live project with an address can be asked');
1636
+ assert.match(runs, /inst\.status !== 'active' \|\| !addressReady\(inst\)/, 'only a live project whose address answers can be asked (task 1004505)');
1635
1637
  assert.match(runs, /ensureManifest\(inst\.domain\)/, 'the same public GET /instance the runner reads, through the shared reader');
1636
1638
  assert.doesNotMatch(runs, /fetch\(/, 'the second card of the pair adds no round trip of its own (task 1003554)');
1637
1639
  assert.match(runs, /p && p\.joinability/, 'the manifest key is brand.project.joinability');