@bongos/core 1.25.14 → 1.25.16

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 (46) hide show
  1. package/.bongos-core.json +101 -41
  2. package/.claude/skills/goal-create/SKILL.md +1 -0
  3. package/.claude/skills/planning-session/SKILL.md +5 -4
  4. package/bin/bongos.js +1 -0
  5. package/clients/bongos-client/README.md +1 -1
  6. package/clients/bongos-client/bongos-client.global.js +20 -0
  7. package/clients/bongos-client/index.cjs +20 -0
  8. package/clients/bongos-client/index.d.ts +24 -0
  9. package/clients/bongos-client/index.mjs +20 -0
  10. package/docs/adr/0408-a-genesis-stage-is-three-doors-onto-one-record.md +61 -0
  11. package/docs/adr/README.md +1 -0
  12. package/docs/api/openapi.json +544 -2
  13. package/docs/api-reference.md +12 -2
  14. package/docs/copy-inventory.md +120 -109
  15. package/docs/copy-registry.json +275 -165
  16. package/docs/file-map.md +1 -0
  17. package/docs/module-api-changelog.md +2 -0
  18. package/docs/page-inventory.json +2 -0
  19. package/docs/page-readings.json +107 -107
  20. package/docs/project-context.template.md +1 -0
  21. package/modules/hall-ui/public/dom-utils.js +5 -0
  22. package/modules/hall-ui/public/genesis-home.js +23 -8
  23. package/modules/hall-ui/public/setup-gate.js +1 -1
  24. package/modules/hall-ui/public/style.css +21 -1
  25. package/modules/lifecycle/kickoff-checklist.js +18 -4
  26. package/modules/lifecycle/kickoff-seed.js +27 -4
  27. package/modules/onboarding/genesis-home.js +11 -2
  28. package/modules/onboarding/genesis-work.js +387 -0
  29. package/modules/onboarding/migrations/onboarding_002_genesis_work.sql +76 -0
  30. package/modules/onboarding/routes/genesis-work.js +85 -0
  31. package/modules/onboarding/routes/onboarding.js +3 -0
  32. package/modules/public-landing/public/assets/guide-scene.css +245 -0
  33. package/modules/public-landing/public/assets/guide-scene.js +566 -0
  34. package/modules/public-landing/public/index.html +62 -2
  35. package/package-lock.json +2 -2
  36. package/package.json +1 -1
  37. package/release-notes.json +18 -0
  38. package/scripts/bongos/genesis.js +180 -0
  39. package/scripts/bongos/run-unit-tests.js +3 -0
  40. package/scripts/gds/genesis.js +7 -0
  41. package/src/module-api.js +1 -1
  42. package/tests/genesis_home.mjs +70 -2
  43. package/tests/genesis_work.mjs +304 -0
  44. package/tests/genesis_work_db.mjs +73 -0
  45. package/tests/genesis_work_routes.mjs +130 -0
  46. package/tests/landing_guide_scene.mjs +504 -0
@@ -43,6 +43,9 @@
43
43
  then styles new markup with an old sheet). -->
44
44
  <link rel="stylesheet" href="/modules/ui-design/public/tokens.css">
45
45
  <link rel="stylesheet" href="/assets/world.css">
46
+ <!-- the start-a-project guide, as a scene in the game (task 1005275): drawn only when the
47
+ front-door-guide module is on and its server can talk -->
48
+ <link rel="stylesheet" href="/assets/guide-scene.css">
46
49
  <style>
47
50
  /* ── PAGE-LOCAL SHEET ────────────────────────────────────────────────────
48
51
  The composition: how this page arranges the world's sculpture, and nothing
@@ -574,6 +577,12 @@ body{
574
577
  --orb-x:4vw;
575
578
  --orb-y:-6vh;
576
579
  --doors-cx:0;--doors-cy:0;
580
+ /* the start-a-project guide (assets/guide-scene.css, task 1005275): one column, the
581
+ frame the whole screen, the plan over the talk from its Plan button */
582
+ --gd-pad:0;--gd-frame-w:100%;--gd-frame-h:100%;--gd-cols:minmax(0,1fr);--gd-side:0px;
583
+ --gd-gutter:var(--ds-space-gutter-narrow);--gd-face:56px;--gd-face-img:48px;
584
+ --gd-planbtn:inline-flex;--gd-planx:inline-grid;--gd-go-w:100%;
585
+ --gd-plan-show:none;--gd-plan-pos:absolute;--gd-plan-edge:0px;--gd-talk-under-plan:hidden;
577
586
  }
578
587
  :root[data-mode="dark"] body{
579
588
  --layer-pos:relative;
@@ -779,7 +788,7 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
779
788
  <div class="doors">
780
789
  <div class="doorbar">
781
790
  <a class="door" href="/projects">Find a project to join</a>
782
- <a class="door door--go" href="/projects?view=new">Start a project</a>
791
+ <a class="door door--go" href="/projects?view=new" data-guide-door>Start a project</a>
783
792
  </div>
784
793
  </div>
785
794
  </div>
@@ -945,7 +954,7 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
945
954
  <div class="wrap">
946
955
  <div class="closemark" aria-hidden="true"></div>
947
956
  <div class="doorbar">
948
- <a class="door door--go" href="/projects?view=new">Start a project</a>
957
+ <a class="door door--go" href="/projects?view=new" data-guide-door>Start a project</a>
949
958
  <a class="door" href="/projects">Find a project</a>
950
959
  </div>
951
960
  <div class="cli" data-cli hidden>
@@ -957,6 +966,53 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
957
966
  </section>
958
967
  </main>
959
968
 
969
+ <!-- THE GUIDE (task 1005275): the start-a-project conversation as a scene in the game.
970
+ Hidden, and never opened, unless the front-door-guide module is on and its status read
971
+ says it can talk (assets/guide-scene.js); with it off the doors above are plain links. -->
972
+ <div class="gd" id="guide" role="dialog" aria-modal="true" aria-labelledby="gdTitle" hidden>
973
+ <div class="gd__frame">
974
+ <section class="gd__talk" aria-labelledby="gdTitle">
975
+ <div class="gd__head">
976
+ <h2 class="gd__title" id="gdTitle">Start a project</h2>
977
+ <button type="button" class="gd__x" data-gd="close" aria-label="Close"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M5 5l10 10M15 5L5 15"/></svg></button>
978
+ </div>
979
+ <div class="gd__who-row">
980
+ <span class="gd__face"><img src="/modules/hall-ui/public/characters/patron.png" alt="" width="64" height="64"></span>
981
+ <div class="gd__meta">
982
+ <p class="gd__name">Your guide</p>
983
+ <p class="gd__left" data-gd="left" aria-live="polite" hidden></p>
984
+ <span class="gd__segs" data-gd="segs" aria-hidden="true" hidden></span>
985
+ </div>
986
+ <button type="button" class="gd__planbtn" data-gd="plan-toggle" aria-expanded="false" aria-controls="gdPlan">Plan</button>
987
+ </div>
988
+ <ol class="gd__log" data-gd="log" aria-live="polite" aria-label="Conversation"></ol>
989
+ <p class="gd__err" data-gd="err" role="alert" hidden></p>
990
+ <form class="gd__say" data-gd="form" hidden>
991
+ <label class="vh" for="gdText">Your message</label>
992
+ <textarea class="gd__text" id="gdText" data-gd="text" rows="2" maxlength="1000" placeholder="What do you want to build?"></textarea>
993
+ <input class="gd__hp" type="text" name="website" data-gd="website" tabindex="-1" autocomplete="off" aria-hidden="true">
994
+ <button type="submit" class="gd__send" data-gd="send">Send</button>
995
+ <div class="gd__sayfoot">
996
+ <button type="button" class="gd__more" data-gd="make-plan" hidden>Make my plan</button>
997
+ <a class="gd__skip" data-gd="skip" href="/projects?view=new">Skip to sign up</a>
998
+ </div>
999
+ </form>
1000
+ <div class="gd__end" data-gd="end" hidden>
1001
+ <button type="button" class="gd__more" data-gd="plan-again" hidden>Make my plan</button>
1002
+ <a class="gd__go" data-gd="go" href="/projects?view=new">Sign up and get started</a>
1003
+ </div>
1004
+ </section>
1005
+ <aside class="gd__plan" id="gdPlan" aria-labelledby="gdPlanH">
1006
+ <div class="gd__planhead">
1007
+ <h3 class="gd__planh" id="gdPlanH">Your plan</h3>
1008
+ <button type="button" class="gd__x" data-gd="plan-close" aria-label="Back to the chat"><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M5 5l10 10M15 5L5 15"/></svg></button>
1009
+ </div>
1010
+ <div class="gd__planbody" data-gd="plan-body"><p class="gd__planwait">When the chat ends, your guide writes your plan here.</p></div>
1011
+ <p class="gd__saved" data-gd="saved" hidden></p>
1012
+ </aside>
1013
+ </div>
1014
+ </div>
1015
+
960
1016
  <footer>
961
1017
  <div class="footrow">
962
1018
  <p><!--apex-only:start-->A non-profit, capture-resistant commons. <!--apex-only:end-->Built by agents, on Cloud&nbsp;Bongos. <a href="https://www.npmjs.com/package/@bongos/core" target="_blank" rel="noopener">Open source</a> (<a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">AGPL-3.0</a>).</p>
@@ -1035,6 +1091,8 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
1035
1091
  json(API + '/me').then(function (data) {
1036
1092
  var me = data && (data.builder || data.me);
1037
1093
  if (!me || !me.github_login) return;
1094
+ /* the start-a-project guide is for a visitor who has not signed up (task 1005275) */
1095
+ document.querySelectorAll('[data-guide-door]').forEach(function (d) { d.removeAttribute('data-guide-door'); });
1038
1096
  var slot = document.getElementById('who');
1039
1097
  if (!slot) return;
1040
1098
  slot.textContent = '';
@@ -1302,6 +1360,8 @@ composition A; landing composition v3, picked by the owner 2026-08-27. No roll,
1302
1360
  });
1303
1361
  })();
1304
1362
  </script>
1363
+ <!-- the start-a-project guide (task 1005275) -->
1364
+ <script src="/assets/guide-scene.js"></script>
1305
1365
  <!-- the Artist review button: signed-in builders only (task 1004499) -->
1306
1366
  <script src="/assets/artist-review.js" data-hall="{{buildersOrigin}}"></script>
1307
1367
  </body>
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.25.14",
3
+ "version": "1.25.16",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.25.14",
9
+ "version": "1.25.16",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.25.14",
3
+ "version": "1.25.16",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -11806,5 +11806,23 @@
11806
11806
  "id": "1005279",
11807
11807
  "text": "The project guide now asks whether you are starting fresh or already have something, and for an existing project plans from your own description, without ever reading your files or code."
11808
11808
  }
11809
+ ],
11810
+ "1.25.16": [
11811
+ {
11812
+ "id": "1005110",
11813
+ "text": "Genesis setup now shows each stage's work as a checklist that fills in as tasks ship, and a step that must wait for the one before it is clearly locked until then."
11814
+ },
11815
+ {
11816
+ "id": "1005114",
11817
+ "text": "The project's first setup step now asks the founder real questions — what they are making and why, who it is for, what success looks like in two years, and what others in the area get wrong. Answers are saved as answers, 'not"
11818
+ },
11819
+ {
11820
+ "id": "1005275",
11821
+ "text": "Visitors pressing Start a project can now talk with a game-style guide about their idea, see a plan form and their messages left, then sign up; when the guide is off it works exactly as before."
11822
+ },
11823
+ {
11824
+ "id": "1005116",
11825
+ "text": "Founders now write their own strategy. Each setup stage can be done three ways: answer guided questions, write it freely, or get help from your own AI — and you can switch at any time without losing anything. Your answers are"
11826
+ }
11809
11827
  ]
11810
11828
  }
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env node
2
+ // scripts/bongos/genesis.js — the founder's genesis answers from a terminal (task 1005116)
3
+ //
4
+ // WHY. Each genesis stage is the founder's to write, through one of three doors onto one
5
+ // stored record (modules/onboarding/genesis-work.js): guided questions, an open page, or
6
+ // their own AI session helping. This is how an AI session works a stage WITH the founder
7
+ // — it reads what they have said, asks the stage's own questions, and records only the
8
+ // answers the founder accepts — and how any session planning a goal or task reads what
9
+ // the founder said (`context`). Every write goes to the server as the person signed in;
10
+ // only the project owner may write, so a session can never author a stage on its own.
11
+ //
12
+ // Usage:
13
+ // npx bongos genesis show [stage] every stage, or one, with its answers
14
+ // npx bongos genesis context what the founder said, as markdown (for planning sessions)
15
+ // npx bongos genesis assist <stage> the brief for the founder's AI session (the assisted door)
16
+ // npx bongos genesis answer <stage> <question> "<words>" [--origin founder|assisted]
17
+ // npx bongos genesis answer <stage> <question> --not-sure ["why"]
18
+ // npx bongos genesis answer <stage> <question> --skip ["why"]
19
+ // npx bongos genesis clear <stage> <question> un-answer: back to empty (not skipped)
20
+ // npx bongos genesis history <stage> <question> what an answer used to say
21
+ // npx bongos genesis door <stage> guided|open|assisted which way the founder is working now
22
+ // npx bongos genesis page <stage> "<words>" | --file <path> | --remove
23
+ // npx bongos genesis link <stage> <url> | --remove the document the founder already has
24
+ //
25
+ // Also `node scripts/bongos/genesis.js …`.
26
+ 'use strict';
27
+
28
+ const fs = require('node:fs');
29
+ const { apiCall, requireSession, errCode, cliExit } = require('./cli-lib');
30
+
31
+ const USAGE = 'usage: npx bongos genesis show [stage] | context | assist <stage> | answer <stage> <question> "<words>" [--origin founder|assisted] | answer <stage> <question> --not-sure|--skip ["why"] | clear <stage> <question> | history <stage> <question> | door <stage> guided|open|assisted | page <stage> "<words>"|--file <path>|--remove | link <stage> <url>|--remove';
32
+
33
+ const STATE_LABEL = { answered: '', not_sure: '[not sure yet] ', skipped: '[skipped] ' };
34
+ const ORIGIN_LABEL = { assisted: ' (with AI help)', carried_over: ' (carried over — confirm or edit)' };
35
+
36
+ // Pure: one stage as the founder reads it.
37
+ function formatStage(s) {
38
+ const out = [`${s.stage[0].toUpperCase()}${s.stage.slice(1)} — working ${s.door}` +
39
+ (s.adopted ? ` · ${s.counts.answered} answered, ${s.counts.not_sure} not sure, ${s.counts.skipped} skipped, ${s.counts.empty} empty` : '')];
40
+ let group = null;
41
+ for (const q of s.questions) {
42
+ if (q.group && q.group !== group) { group = q.group; out.push(` — ${group} —`); }
43
+ out.push(` ${q.id}: ${q.ask}`);
44
+ if (!q.answer) { out.push(' (empty)'); continue; }
45
+ const a = q.answer;
46
+ const words = a.text ? a.text.replace(/\n/g, '\n ') : '';
47
+ out.push(` ${STATE_LABEL[a.state] || ''}${words}${ORIGIN_LABEL[a.origin] || ''}`.trimEnd());
48
+ }
49
+ if (s.page) out.push(' Page:', ` ${s.page.replace(/\n/g, '\n ')}`);
50
+ if (s.link) out.push(` Link: ${s.link}`);
51
+ return out.join('\n');
52
+ }
53
+
54
+ // Pure: parse `answer` arguments into the PUT body, or { error }.
55
+ function parseAnswer(rest) {
56
+ const args = [...rest];
57
+ const body = {};
58
+ const take = (flag) => {
59
+ const i = args.indexOf(flag);
60
+ if (i < 0) return undefined;
61
+ const next = args[i + 1];
62
+ const val = next != null && !String(next).startsWith('--') ? next : '';
63
+ args.splice(i, val === '' && (next == null || String(next).startsWith('--')) ? 1 : 2);
64
+ return val;
65
+ };
66
+ const origin = take('--origin');
67
+ const notSure = take('--not-sure');
68
+ const skip = take('--skip');
69
+ if (notSure !== undefined && skip !== undefined) return { error: 'choose one of --not-sure or --skip' };
70
+ if (origin !== undefined) body.origin = origin;
71
+ if (notSure !== undefined) { body.state = 'not_sure'; if (notSure) body.text = notSure; }
72
+ else if (skip !== undefined) { body.state = 'skipped'; if (skip) body.text = skip; }
73
+ else {
74
+ const words = args.join(' ').trim();
75
+ if (!words) return { error: 'give the answer in quotes, or --not-sure / --skip' };
76
+ body.state = 'answered';
77
+ body.text = words;
78
+ }
79
+ return { body };
80
+ }
81
+
82
+ async function call(method, path, body, what) {
83
+ const r = await apiCall(method, `/api/bongos${path}`, body);
84
+ if (!r.ok) console.error(`Could not ${what} (${r.status}${errCode(r.data) ? `: ${errCode(r.data)}` : ''}).`);
85
+ return r.ok ? r.data : null;
86
+ }
87
+
88
+ const enc = encodeURIComponent;
89
+
90
+ async function main(argv) {
91
+ const [cmd, stage, ...rest] = argv;
92
+ if (!cmd || cmd === '--help' || cmd === '-h') { console.log(USAGE); return cmd ? 0 : 2; }
93
+ await requireSession();
94
+ if (cmd === 'show') {
95
+ if (stage) {
96
+ const d = await call('GET', `/founding/work/${enc(stage)}`, null, 'read the stage');
97
+ if (!d) return 1;
98
+ console.log(formatStage(d.work));
99
+ return 0;
100
+ }
101
+ const d = await call('GET', '/founding/work', null, 'read genesis');
102
+ if (!d) return 1;
103
+ console.log(d.stages.map(formatStage).join('\n\n'));
104
+ return 0;
105
+ }
106
+ if (cmd === 'context') {
107
+ const d = await call('GET', '/founding/context', null, 'read what the founder said');
108
+ if (!d) return 1;
109
+ console.log(d.markdown || 'The founder has not written anything in genesis yet.');
110
+ return 0;
111
+ }
112
+ if (!stage) { console.error(USAGE); return 2; }
113
+ if (cmd === 'assist') {
114
+ const d = await call('GET', `/founding/work/${enc(stage)}/assist`, null, 'build the brief');
115
+ if (!d) return 1;
116
+ console.log(d.prompt);
117
+ return 0;
118
+ }
119
+ if (cmd === 'answer') {
120
+ const [question, ...words] = rest;
121
+ if (!question) { console.error(USAGE); return 2; }
122
+ const p = parseAnswer(words);
123
+ if (p.error) { console.error(p.error); return 2; }
124
+ const d = await call('PUT', `/founding/work/${enc(stage)}/answers/${enc(question)}`, p.body, 'save the answer');
125
+ if (!d) return 1;
126
+ console.log(d.changed === false ? 'Unchanged — that is already the answer.' : 'Saved.');
127
+ console.log(formatStage(d.work));
128
+ return 0;
129
+ }
130
+ if (cmd === 'clear' || cmd === 'history') {
131
+ const [question] = rest;
132
+ if (!question) { console.error(USAGE); return 2; }
133
+ if (cmd === 'clear') {
134
+ const d = await call('DELETE', `/founding/work/${enc(stage)}/answers/${enc(question)}`, null, 'clear the answer');
135
+ if (!d) return 1;
136
+ console.log(d.cleared ? 'Cleared — the question is empty again.' : 'It was already empty.');
137
+ return 0;
138
+ }
139
+ const d = await call('GET', `/founding/work/${enc(stage)}/answers/${enc(question)}/history`, null, 'read the history');
140
+ if (!d) return 1;
141
+ if (!d.history.length) console.log('No answer has ever been saved here.');
142
+ for (const h of d.history) console.log(`${h.changed_at} ${h.state}${h.text ? `: ${h.text}` : ''}`);
143
+ return 0;
144
+ }
145
+ if (cmd === 'door') {
146
+ const d = await call('PUT', `/founding/work/${enc(stage)}/door`, { door: String(rest[0] || '') }, 'switch the door');
147
+ if (!d) return 1;
148
+ console.log(`Now working ${d.work.door}. Nothing already written was lost.`);
149
+ return 0;
150
+ }
151
+ if (cmd === 'page') {
152
+ let page;
153
+ if (rest[0] === '--remove') page = '';
154
+ else if (rest[0] === '--file') {
155
+ if (!rest[1]) { console.error('give the file to read: --file <path>'); return 2; }
156
+ page = fs.readFileSync(rest[1], 'utf8');
157
+ } else page = rest.join(' ');
158
+ const d = await call('PUT', `/founding/work/${enc(stage)}/page`, { page }, 'save the page');
159
+ if (!d) return 1;
160
+ console.log(d.work.page ? 'Page saved.' : 'Page removed.');
161
+ return 0;
162
+ }
163
+ if (cmd === 'link') {
164
+ const url = rest[0] === '--remove' ? '' : String(rest[0] || '');
165
+ const d = await call('PUT', `/founding/work/${enc(stage)}/link`, { url }, 'save the link');
166
+ if (!d) return 1;
167
+ console.log(d.work.link ? `Linked: ${d.work.link}` : 'Link removed.');
168
+ return 0;
169
+ }
170
+ console.error(USAGE);
171
+ return 2;
172
+ }
173
+
174
+ if (require.main === module) {
175
+ main(process.argv.slice(2))
176
+ .then((code) => cliExit(code))
177
+ .catch((err) => { console.error('fatal:', err && err.message ? err.message : err); cliExit(1); });
178
+ }
179
+
180
+ module.exports = { main, formatStage, parseAnswer };
@@ -375,6 +375,9 @@ const INTEGRATION = new Set([
375
375
  // the second proves the search index never widens a caller past their rank or into
376
376
  // another builder memory. Each skipped in CI before, so none had ever been gated.
377
377
  'page_notes_db', 'howto_state_db', 'search_isolation_db',
378
+ // task 1005116: the genesis stage-work store on the real schema (the DB-free half,
379
+ // genesis_work, stays in the unit lane).
380
+ 'genesis_work_db',
378
381
  ]);
379
382
 
380
383
  // Excluded so this gate is trustworthy in a CLEAN checkout (the CI condition):
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/genesis.js — @bongos-forwarder (task 1004848). Generated by
3
+ // scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
4
+ // The real script is scripts/bongos/genesis.js. This forwarder keeps callers that still name
5
+ // the old path working, and warns once so they get repointed. Task 1003706 removes it.
6
+ 'use strict';
7
+ require('../bongos/forwarder.js').forward(module, 'genesis.js');
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.25.14'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.25.16'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -46,17 +46,54 @@ test('genesis files a task per stage, keyed by a ref that names the step, not it
46
46
 
47
47
  test('the seeder files every genesis step once, under the catch-all, and never twice', async () => {
48
48
  const filed = [];
49
+ const edges = [];
49
50
  const deps = {
50
51
  pool: { query: async () => ({ rows: [{ id: 'V1' }] }) },
51
- createTask: async (row) => { filed.push(row); },
52
+ createTask: async (row) => { filed.push({ ...row, id: 100 + filed.length }); },
52
53
  findTaskBySourceRef: async ({ source, sourceRef }) => filed.find((r) => r.source === source && r.sourceRef === sourceRef) || null,
54
+ addTaskDependency: async (id, dep) => { edges.push([id, dep]); },
53
55
  };
54
56
  const first = await seed.seedGenesisStages({ builderId: 1 }, deps);
55
57
  assert.equal(first.seeded, 5);
56
58
  assert.ok(filed.every((r) => r.status === 'ready' && r.source === 'genesis' && r.allowCatchAll === true && r.versionId === 'V1'));
59
+ assert.deepEqual(edges, [], 'a new project: every stage has one step, nothing waits on anything');
57
60
  const again = await seed.seedGenesisStages({ builderId: 1, existing: true }, deps);
58
61
  assert.equal(again.seeded, 1, 'a second run files only the step that was not there (the brownfield one)');
59
62
  assert.equal(filed.length, 6);
63
+ const scope = filed.find((r) => r.sourceRef === 'genesis:scope');
64
+ const gather = filed.find((r) => r.sourceRef === 'genesis:scope:existing');
65
+ assert.deepEqual(edges, [[scope.id, gather.id]], 'writing the scope waits on gathering what exists (task 1005110)');
66
+ });
67
+
68
+ test('the checklist declares order only where it is real: brownfield scope (task 1005110)', () => {
69
+ const fresh = genesisChecklist({});
70
+ assert.ok(Object.values(fresh).flat().every((t) => t.after === null), 'a new project: all open');
71
+ const old = genesisChecklist({ existing: true });
72
+ assert.deepEqual(old.scope.map((t) => [t.ref, t.after]), [
73
+ ['genesis:scope:existing', null],
74
+ ['genesis:scope', 'genesis:scope:existing'],
75
+ ]);
76
+ for (const k of ['strategize', 'charter', 'legalize', 'legislation']) assert.equal(old[k][0].after, null);
77
+ });
78
+
79
+ test('the board read says what each task waits on, from the dependency edges (task 1005110)', async () => {
80
+ let seen = '';
81
+ const pool = { query: async (sql) => {
82
+ seen = sql;
83
+ return { rows: [
84
+ { id: 1, title: 'Gather', status: 'ready', description: '', source_ref: 'genesis:scope:existing', waits_on: [] },
85
+ { id: 2, title: 'Write', status: 'ready', description: '', source_ref: 'genesis:scope', waits_on: [{ id: 1, title: 'Gather' }] },
86
+ ] };
87
+ } };
88
+ const out = await seed.genesisStageTasks({ pool });
89
+ assert.match(seen, /FROM dependencies d/);
90
+ assert.match(seen, /x\.status <> 'shipped'/, 'the claim gate\'s own rule: only a shipped task stops blocking');
91
+ assert.deepEqual(out.scope[1].waits_on, [{ id: '1', title: 'Gather' }]);
92
+ const g = gh.shapeGenesis({ mode: { stage: 'scope', stage_number: 1, stage_count: 5, day: 1 }, plan: gh.parsePlan({}).plan, tasks: out });
93
+ assert.deepEqual(g.stages[0].tasks.map((t) => t.state), ['open', 'locked']);
94
+ const done = gh.shapeGenesis({ mode: { stage: 'scope', stage_number: 1, stage_count: 5, day: 1 }, plan: gh.parsePlan({}).plan,
95
+ tasks: { scope: [{ id: '1', title: 'Gather', status: 'shipped' }, { id: '2', title: 'Write', status: 'ready', waits_on: [] }] } });
96
+ assert.deepEqual(done.stages[0].tasks.map((t) => t.state), ['done', 'open'], 'once the first ships, the second opens');
60
97
  });
61
98
 
62
99
  test('the board read groups genesis tasks by the stage their ref names', async () => {
@@ -389,7 +426,7 @@ function bootCard({ founding, data = { genesis: null } }) {
389
426
  const attrs = founding ? { 'data-founding': 'on' } : {};
390
427
  const sandbox = {
391
428
  window: {
392
- OTB: { escapeHtml: (s) => String(s), statusPill: (s) => `<span>${s}</span>`, contributeHallWidget: (w) => { widget = w; }, toast: (m) => toasts.push(m) },
429
+ OTB: { escapeHtml: (s) => String(s), statusPill: (s) => `<span>${s}</span>`, contributeHallWidget: (w) => { widget = w; }, toast: (m) => toasts.push(m), CHECK_SVG: '<svg/>' },
393
430
  BongosClient: { createClient: () => ({ request: async (m, p) => { calls.push(`${m} ${p}`); return { ok: true, status: 200, data }; } }) },
394
431
  },
395
432
  navigator: { clipboard: { writeText: async (t) => { copied.push(t); } } },
@@ -537,6 +574,37 @@ test('each open stage task has Copy prompt, and it copies the task\'s own words
537
574
  assert.match(b.toasts.join(' '), /copied/i);
538
575
  });
539
576
 
577
+ test('the stage reads as a checklist: done, open and locked, and a locked row offers no prompt (task 1005110)', async () => {
578
+ const genesis = gh.shapeGenesis({
579
+ mode: { stage: 'scope', stage_number: 1, stage_count: 5, day: 1 },
580
+ plan: gh.parsePlan({}).plan,
581
+ tasks: { scope: [
582
+ { id: '301', title: 'Gather what already exists', status: 'ready', description: 'Link it.' },
583
+ { id: '302', title: 'Write the scope', status: 'ready', description: 'Write it.', waits_on: [{ id: '301', title: 'Gather what already exists' }] },
584
+ ], strategize: [{ id: '303', title: 'Strategy', status: 'shipped' }] },
585
+ });
586
+ const b = bootCard({ founding: true, data: { genesis, viewer_id: '1' } });
587
+ await mountClickable(b);
588
+ const html = b.host.innerHTML;
589
+ const row = (id) => html.split('<li ').find((x) => x.includes(`/task/${id}"`)) || '';
590
+ assert.match(row('301'), /class="genesis__task is-open"/);
591
+ assert.match(row('301'), /data-genesis-copy="301"/, 'the open row has Copy prompt');
592
+ assert.match(row('302'), /class="genesis__task is-locked"/);
593
+ assert.match(row('302'), /after Gather what already exists/, 'a locked row says what it waits on');
594
+ assert.doesNotMatch(row('302'), /data-genesis-copy/, 'a locked row offers no prompt for work it cannot claim');
595
+ assert.doesNotMatch(html, /is-finished|line-through/, 'nothing is drawn struck through');
596
+ // The "done" row: the earlier stage is not the one on screen, so check the shape directly.
597
+ const done = gh.shapeGenesis({ mode: { stage: 'scope', stage_number: 1, stage_count: 5, day: 1 }, plan: gh.parsePlan({}).plan,
598
+ tasks: { scope: [{ id: '304', title: 'Gathered', status: 'shipped' }, { id: '305', title: 'Next', status: 'ready' }] } });
599
+ const b2 = bootCard({ founding: true, data: { genesis: done, viewer_id: '1' } });
600
+ await mountClickable(b2);
601
+ const r304 = b2.host.innerHTML.split('<li ').find((x) => x.includes('/task/304"')) || '';
602
+ assert.match(r304, /class="genesis__task is-done"/);
603
+ assert.match(r304, /<span class="genesis__mark" aria-hidden="true"><svg/, 'done fills its circle with the hall\'s tick');
604
+ assert.match(r304, /class="sr-only">Done: /, 'and says so to a screen reader');
605
+ assert.doesNotMatch(r304, /data-genesis-copy/);
606
+ });
607
+
540
608
  test('the hall carries the five life plates byte-identical to the world\'s', () => {
541
609
  const world = path.join(ROOT, 'modules', 'public-landing', 'public', 'assets', 'world');
542
610
  const hall = path.join(ROOT, 'modules', 'hall-ui', 'public', 'genesis');