@bongos/core 1.25.17 → 1.25.19
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.
- package/.bongos-core.json +135 -65
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +12 -0
- package/clients/bongos-client/index.cjs +12 -0
- package/clients/bongos-client/index.d.ts +17 -1
- package/clients/bongos-client/index.mjs +12 -0
- package/docs/adr/0408-a-genesis-stage-is-three-doors-onto-one-record.md +2 -1
- package/docs/api/openapi.json +288 -6
- package/docs/api-reference.md +14 -4
- package/docs/copy-inventory.md +132 -121
- package/docs/copy-registry.json +347 -195
- package/docs/file-map.md +2 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +3 -0
- package/docs/page-readings.json +63 -63
- package/modules/app-host/CLAUDE.md +15 -0
- package/modules/app-host/module.json +20 -0
- package/modules/app-host/public/settings-widget.js +143 -0
- package/modules/app-host/routes/app-host.js +88 -0
- package/modules/government/charter-advice.js +103 -0
- package/modules/government/presets.js +42 -0
- package/modules/government/recommendations.js +102 -7
- package/modules/government/routes/government.js +1 -0
- package/modules/hall-ui/public/genesis-home.js +28 -3
- package/modules/lifecycle/first-read-catalog.js +62 -0
- package/modules/lifecycle/first-read-db.js +47 -9
- package/modules/lifecycle/first-read.js +88 -23
- package/modules/lifecycle/migrations/lifecycle_023_repo_map_dismissed.sql +19 -0
- package/modules/lifecycle/routes/first-read.js +27 -5
- package/modules/onboarding/genesis-home.js +68 -7
- package/modules/onboarding/genesis-work.js +169 -4
- package/modules/onboarding/routes/genesis-work.js +5 -2
- package/modules/platform-identity/module.json +1 -1
- package/modules/platform-identity/routes/sso-app-status.js +65 -0
- package/modules/provisioning/module.json +1 -0
- package/modules/provisioning/seams.js +32 -1
- package/modules/provisioning/starter-bundles.js +1 -0
- package/modules/public-landing/public/assets/guide-core.js +475 -0
- package/modules/public-landing/public/assets/guide-hall.css +121 -0
- package/modules/public-landing/public/assets/guide-hall.js +164 -0
- package/modules/public-landing/public/assets/guide-scene.css +9 -0
- package/modules/public-landing/public/assets/guide-scene.js +58 -381
- package/modules/public-landing/public/index.html +46 -1
- package/modules/public-landing/public/projects.html +11 -2
- package/modules/ui-design/kit/serve.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +24 -0
- package/scripts/bongos/first-read.js +45 -11
- package/scripts/bongos/genesis.js +29 -2
- package/scripts/bongos/publish-manifest.js +1 -0
- package/scripts/bongos/seam-wiring-guard.js +3 -2
- package/scripts/hall-preview/server.js +2 -0
- package/src/bongos/auth-admission.js +64 -0
- package/src/bongos/auth.js +2 -1
- package/src/bongos/module-scope-map.js +4 -0
- package/src/bongos/routes/core-update.js +3 -16
- package/src/bongos/routes/modules.js +11 -0
- package/src/bongos/serve-internal.js +20 -6
- package/src/module-api.js +6 -1
- package/src/module-loader/loader.js +2 -2
- package/tests/app_host.mjs +413 -0
- package/tests/first_read.mjs +184 -8
- package/tests/genesis_home.mjs +81 -1
- package/tests/genesis_work.mjs +103 -3
- package/tests/genesis_work_routes.mjs +9 -0
- package/tests/government_charter_advice.mjs +150 -0
- package/tests/landing_guide_hall.mjs +364 -0
- package/tests/landing_guide_scene.mjs +8 -5
- package/tests/module_api.mjs +1 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/npm_release_where.mjs +13 -3
|
@@ -19,8 +19,12 @@
|
|
|
19
19
|
// position), and every filed ref is remembered (lifecycle_repo_map_filed), so a second
|
|
20
20
|
// map that names the same goal or task again plans to file nothing for it.
|
|
21
21
|
//
|
|
22
|
-
// ROOM TO GROW. Unknown top-level keys are kept, not refused
|
|
23
|
-
//
|
|
22
|
+
// ROOM TO GROW. Unknown top-level keys are kept, not refused, so a later section needs
|
|
23
|
+
// no change to this version's shape. `recommended_modules` (task 1005281) is the first:
|
|
24
|
+
// modules from THIS project's live list that would do a job better than what the code
|
|
25
|
+
// has (first-read-catalog.js supplies the list; a key not on it is refused on submit).
|
|
26
|
+
// A recommendation is a suggestion: accepting files a draft task to switch it on, and
|
|
27
|
+
// dismissing records the no, so a re-run does not raise it again.
|
|
24
28
|
//
|
|
25
29
|
// Pure — no I/O. The DB half is first-read-db.js; the HTTP surface routes/first-read.js;
|
|
26
30
|
// the CLI scripts/bongos/first-read.js; the Genesis step that hands out the prompt is
|
|
@@ -41,12 +45,13 @@ const LIMITS = Object.freeze({
|
|
|
41
45
|
GOALS: 20,
|
|
42
46
|
TASKS_PER_GOAL: 30,
|
|
43
47
|
NEXT: 50,
|
|
48
|
+
MODULES: 10,
|
|
44
49
|
BYTES: 60000,
|
|
45
50
|
});
|
|
46
51
|
const TASK_STATES = ['done', 'in_progress', 'not_started'];
|
|
47
52
|
const NEXT_KINDS = ['error', 'mishap', 'gap'];
|
|
48
53
|
// Top-level keys this version reads. Anything else is kept as-is for a later reader.
|
|
49
|
-
const KNOWN_KEYS = ['schema', 'summary', 'goals', 'next'];
|
|
54
|
+
const KNOWN_KEYS = ['schema', 'summary', 'goals', 'next', 'recommended_modules'];
|
|
50
55
|
const UNSAFE_KEYS = ['__proto__', 'constructor', 'prototype'];
|
|
51
56
|
// The goal a `next` item lands under when it names no goal of the map.
|
|
52
57
|
const NEXT_GOAL = Object.freeze({
|
|
@@ -55,6 +60,13 @@ const NEXT_GOAL = Object.freeze({
|
|
|
55
60
|
why: 'Errors, mishaps and gaps the first read-through of the repository found, not tied to one goal.',
|
|
56
61
|
});
|
|
57
62
|
|
|
63
|
+
// The goal an accepted module recommendation lands under.
|
|
64
|
+
const MODULES_GOAL = Object.freeze({
|
|
65
|
+
ref: 'goal:modules-the-first-read-recommends',
|
|
66
|
+
title: 'Modules the first read-through recommends',
|
|
67
|
+
why: 'Modules from this project\'s own list that the first read-through of the repository found would do a job better than what the code has now.',
|
|
68
|
+
});
|
|
69
|
+
|
|
58
70
|
// slug(text) → a stable lower-case key: the same title always gives the same ref.
|
|
59
71
|
function slug(text) {
|
|
60
72
|
return String(text || '').toLowerCase().normalize('NFKD').replace(/[̀-ͯ]/g, '')
|
|
@@ -65,6 +77,7 @@ function slug(text) {
|
|
|
65
77
|
const goalRef = (title) => `goal:${slug(title)}`;
|
|
66
78
|
const taskRef = (goalTitle, title) => `task:${slug(goalTitle)}:${slug(title)}`;
|
|
67
79
|
const nextRef = (title) => `next:${slug(title)}`;
|
|
80
|
+
const moduleRef = (key) => `module:${key}`;
|
|
68
81
|
|
|
69
82
|
// ── the prompt ───────────────────────────────────────────────────────────────
|
|
70
83
|
|
|
@@ -88,9 +101,24 @@ function exampleMap() {
|
|
|
88
101
|
};
|
|
89
102
|
}
|
|
90
103
|
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
104
|
+
// The recommended_modules entry shape, shown in the prompt beside the example map.
|
|
105
|
+
const MODULE_EXAMPLE = { module: '<a key from the module list>', found: 'What in the code is weak, in one or two sentences.', why: 'Why this module does that job better.', seen_at: ['src/billing/'] };
|
|
106
|
+
|
|
107
|
+
// catalogText(catalog) → the module list as prompt lines, or a line saying there is none.
|
|
108
|
+
function catalogText(catalog) {
|
|
109
|
+
if (!Array.isArray(catalog)) return ' (The module list could not be read. Leave "recommended_modules" out.)';
|
|
110
|
+
if (!catalog.length) return ' (This project has no modules left to switch on. Leave "recommended_modules" out.)';
|
|
111
|
+
return catalog.map((m) => ` - ${m.key}: ${m.title}. ${m.summary}`.trimEnd()).join('\n');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// firstReadPrompt({ cli, catalog }) → the full text the owner hands their own AI session.
|
|
115
|
+
// Without a catalog it is the Genesis step's description (fixed text, so the hall's
|
|
116
|
+
// copy-prompt carries all of it) and tells the session to fetch the live module list;
|
|
117
|
+
// with one (`bongos first-read prompt`) the list is printed in.
|
|
118
|
+
function firstReadPrompt({ cli = 'npx bongos', catalog } = {}) {
|
|
119
|
+
const modules = catalog === undefined
|
|
120
|
+
? [` Get the modules this project can switch on: ${cli} first-read catalog`]
|
|
121
|
+
: [' The modules this project can switch on (key: name. what it does):', catalogText(catalog)];
|
|
94
122
|
return [
|
|
95
123
|
'First read-through of a project that already exists. You are the owner\'s own AI session, working on their computer, on their subscription. Bongos itself never reads this repository.',
|
|
96
124
|
'',
|
|
@@ -101,10 +129,13 @@ function firstReadPrompt({ cli = 'npx bongos' } = {}) {
|
|
|
101
129
|
' - Every goal, task and next item has a short "why" (one or two sentences, in plain words) and "seen_at" (where you saw it: file paths, with a line number when it helps). Reasons and places only, never copied code.',
|
|
102
130
|
' Use exactly this shape:',
|
|
103
131
|
JSON.stringify(exampleMap(), null, 2).split('\n').map((l) => ' ' + l).join('\n'),
|
|
132
|
+
' - Optional "recommended_modules": compare what the code does with the modules below. Where a module would clearly do a job better than what is there (a billing system that keeps failing, say), add one entry: "module" (its key, exactly as listed), "found" (what in the code is weak), "why" (why the module is better) and "seen_at". Recommend only from the list, at most ' + LIMITS.MODULES + '. When nothing on it beats what the code already has, leave the section out: no recommendation is a good answer.',
|
|
133
|
+
' Entry shape: ' + JSON.stringify(MODULE_EXAMPLE),
|
|
134
|
+
...modules,
|
|
104
135
|
`3. Check the file: ${cli} first-read check ${MAP_FILE} — fix anything it lists and check again.`,
|
|
105
136
|
`4. Send it to Bongos: ${cli} first-read submit ${MAP_FILE} — this files nothing yet; it prints the map's number and every item's ref.`,
|
|
106
|
-
'5. Walk the owner through the map in plain words, goal by goal. Ask which items to accept: all of them, some of them, or none.',
|
|
107
|
-
`6. Only after the owner says yes, accept exactly what they chose: ${cli} first-read accept <map number> --all, or --items <ref>,<ref>. If they want none of it: ${cli} first-read reject <map number>. Never accept on your own. Accepted items become draft goals and tasks; nothing can be worked on until someone promotes it.`,
|
|
137
|
+
'5. Walk the owner through the map in plain words, goal by goal, then any recommended modules. Ask which items to accept: all of them, some of them, or none. Recommendations are suggestions; anything they say no to can be dismissed.',
|
|
138
|
+
`6. Only after the owner says yes, accept exactly what they chose: ${cli} first-read accept <map number> --all, or --items <ref>,<ref>. Items they turn down: ${cli} first-read dismiss <map number> --items <ref>,<ref> (a later read-through will not raise them again). If they want none of it: ${cli} first-read reject <map number>. Never accept or dismiss on your own. Accepted items become draft goals and tasks; nothing can be worked on until someone promotes it, and an accepted module is a to-do to switch it on, never switched on by itself.`,
|
|
108
139
|
'Running the read-through again later is safe: anything already accepted is not filed twice.',
|
|
109
140
|
].join('\n');
|
|
110
141
|
}
|
|
@@ -130,11 +161,14 @@ function seenAt(errors, where, v) {
|
|
|
130
161
|
return out;
|
|
131
162
|
}
|
|
132
163
|
|
|
133
|
-
// validateRepoMap(raw) → { ok: true, map, items } | { ok: false, errors
|
|
164
|
+
// validateRepoMap(raw, { catalog }) → { ok: true, map, items } | { ok: false, errors }
|
|
134
165
|
// `map` is the normalised map (unknown top-level keys kept verbatim); `items` is the flat
|
|
135
|
-
// review list: [{ ref, type: 'goal'|'task'|'next', title, why, seen_at, status?,
|
|
136
|
-
// goal_ref }]. Every error names the field it is about, so a
|
|
137
|
-
|
|
166
|
+
// review list: [{ ref, type: 'goal'|'task'|'next'|'module', title, why, seen_at, status?,
|
|
167
|
+
// kind?, found?, module?, goal_ref }]. Every error names the field it is about, so a
|
|
168
|
+
// session can fix and retry. `catalog` ([{ key, title }], or null when unreadable) is
|
|
169
|
+
// given on SUBMIT: every recommended key must be on it, and the title is the catalog's.
|
|
170
|
+
// It is left out when re-reading a stored map, which was checked when it came in.
|
|
171
|
+
function validateRepoMap(raw, { catalog } = {}) {
|
|
138
172
|
const errors = [];
|
|
139
173
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { ok: false, errors: ['the map must be a JSON object'] };
|
|
140
174
|
if (raw.schema !== SCHEMA) errors.push(`schema must be "${SCHEMA}"`);
|
|
@@ -143,11 +177,14 @@ function validateRepoMap(raw) {
|
|
|
143
177
|
const summary = text(errors, 'summary', raw.summary, LIMITS.SUMMARY);
|
|
144
178
|
const goalsIn = raw.goals == null ? [] : raw.goals;
|
|
145
179
|
const nextIn = raw.next == null ? [] : raw.next;
|
|
180
|
+
const modsIn = raw.recommended_modules == null ? [] : raw.recommended_modules;
|
|
146
181
|
if (!Array.isArray(goalsIn)) errors.push('goals must be a list');
|
|
147
182
|
if (!Array.isArray(nextIn)) errors.push('next must be a list');
|
|
183
|
+
if (!Array.isArray(modsIn)) errors.push('recommended_modules must be a list');
|
|
148
184
|
if (errors.length) return { ok: false, errors };
|
|
149
185
|
if (goalsIn.length > LIMITS.GOALS) errors.push(`goals lists more than ${LIMITS.GOALS}`);
|
|
150
186
|
if (nextIn.length > LIMITS.NEXT) errors.push(`next lists more than ${LIMITS.NEXT}`);
|
|
187
|
+
if (modsIn.length > LIMITS.MODULES) errors.push(`recommended_modules lists more than ${LIMITS.MODULES}`);
|
|
151
188
|
if (!goalsIn.length && !nextIn.length) errors.push('the map has no goals and nothing next — list at least one');
|
|
152
189
|
|
|
153
190
|
const items = [];
|
|
@@ -201,9 +238,33 @@ function validateRepoMap(raw) {
|
|
|
201
238
|
if (ref && claimRef(ref, w)) items.push({ ref, type: 'next', title, why, seen_at: where, kind, goal_ref: gref });
|
|
202
239
|
next.push({ title, kind, goal, why, seen_at: where });
|
|
203
240
|
});
|
|
241
|
+
const known = Array.isArray(catalog) ? new Map(catalog.map((m) => [m.key, m])) : null;
|
|
242
|
+
const recommended = [];
|
|
243
|
+
modsIn.slice(0, LIMITS.MODULES).forEach((m, mi) => {
|
|
244
|
+
const w = `recommended_modules[${mi}]`;
|
|
245
|
+
if (!m || typeof m !== 'object' || Array.isArray(m)) { errors.push(`${w} must be an object`); return; }
|
|
246
|
+
const key = text(errors, `${w}.module`, m.module, LIMITS.TITLE);
|
|
247
|
+
const found = text(errors, `${w}.found`, m.found, LIMITS.WHY);
|
|
248
|
+
const why = text(errors, `${w}.why`, m.why, LIMITS.WHY);
|
|
249
|
+
const where = seenAt(errors, w, m.seen_at);
|
|
250
|
+
if (!key) return;
|
|
251
|
+
if (catalog !== undefined && !(known && known.has(key))) {
|
|
252
|
+
errors.push(known
|
|
253
|
+
? `${w}.module "${key}" is not a module this project can switch on — use a key from the module list (first-read catalog)`
|
|
254
|
+
: `${w}.module: the module list could not be read here, so no module can be recommended — leave recommended_modules out`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const title = known ? known.get(key).title : (text([], '', m.title, LIMITS.TITLE) || key);
|
|
258
|
+
const ref = moduleRef(key);
|
|
259
|
+
if (seen.has(ref)) { errors.push(`${w}.module repeats a module already recommended`); return; }
|
|
260
|
+
seen.add(ref);
|
|
261
|
+
items.push({ ref, type: 'module', module: key, title, found, why, seen_at: where, goal_ref: MODULES_GOAL.ref });
|
|
262
|
+
recommended.push({ module: key, title, found, why, seen_at: where });
|
|
263
|
+
});
|
|
204
264
|
if (errors.length) return { ok: false, errors };
|
|
205
265
|
|
|
206
266
|
const map = { schema: SCHEMA, summary, goals, next };
|
|
267
|
+
if (recommended.length) map.recommended_modules = recommended;
|
|
207
268
|
// Forward-compatible: a later section (task 1005281's recommended_modules) rides along.
|
|
208
269
|
// Prototype-shaped keys are never copied (CWE-1321): a later section is plain data.
|
|
209
270
|
for (const [k, v] of Object.entries(raw)) if (!KNOWN_KEYS.includes(k) && !UNSAFE_KEYS.includes(k)) map[k] = v;
|
|
@@ -213,19 +274,24 @@ function validateRepoMap(raw) {
|
|
|
213
274
|
// ── acceptance ───────────────────────────────────────────────────────────────
|
|
214
275
|
|
|
215
276
|
function describe(item, mapId) {
|
|
216
|
-
const lines =
|
|
277
|
+
const lines = item.type === 'module'
|
|
278
|
+
? [`Found: ${item.found}`, '', `Why the ${item.title} module: ${item.why}`, '', `Seen at: ${item.seen_at.join(', ')}`,
|
|
279
|
+
'Switching it on is done on the Modules page by whoever may turn modules on; this task is the reminder.']
|
|
280
|
+
: [item.why, '', `Seen at: ${item.seen_at.join(', ')}`];
|
|
217
281
|
if (item.status && item.status !== 'not_started') lines.push(`Status when read: ${item.status.replace('_', ' ')}`);
|
|
218
282
|
lines.push('', `From the first read-through of the repository (map ${mapId}, ${item.ref}). A draft: promote it when it is right.`);
|
|
219
283
|
return lines.join('\n');
|
|
220
284
|
}
|
|
221
285
|
|
|
222
|
-
// planAcceptance({ items, mapId, select, filedRefs }) → { goals, tasks, skipped, unknown }
|
|
223
|
-
// select
|
|
224
|
-
// filedRefs
|
|
286
|
+
// planAcceptance({ items, mapId, select, filedRefs, dismissedRefs }) → { goals, tasks, skipped, unknown }
|
|
287
|
+
// select 'all' | [ref] — what the owner said yes to. Nothing else is planned.
|
|
288
|
+
// filedRefs Set of refs already filed by an earlier accept (any map) — never filed again.
|
|
289
|
+
// dismissedRefs Set of refs the owner said no to before — 'all' skips them; naming one
|
|
290
|
+
// explicitly files it (the owner changed their mind).
|
|
225
291
|
// A chosen task or next item brings its goal along (a task needs a home); a goal that is
|
|
226
292
|
// already filed is reused by ref. A task whose status is `done` is history, not work:
|
|
227
293
|
// it is shown in review but never filed.
|
|
228
|
-
function planAcceptance({ items, mapId, select, filedRefs = new Set() }) {
|
|
294
|
+
function planAcceptance({ items, mapId, select, filedRefs = new Set(), dismissedRefs = new Set() }) {
|
|
229
295
|
const byRef = new Map(items.map((i) => [i.ref, i]));
|
|
230
296
|
const wanted = select === 'all' ? items.map((i) => i.ref) : [...new Set(Array.isArray(select) ? select : [])];
|
|
231
297
|
const unknown = wanted.filter((r) => !byRef.has(r));
|
|
@@ -234,10 +300,8 @@ function planAcceptance({ items, mapId, select, filedRefs = new Set() }) {
|
|
|
234
300
|
const skipped = [];
|
|
235
301
|
const needGoal = (ref) => {
|
|
236
302
|
if (!ref || filedRefs.has(ref) || goals.has(ref)) return;
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
303
|
+
const fixed = ref === NEXT_GOAL.ref ? NEXT_GOAL : ref === MODULES_GOAL.ref ? MODULES_GOAL : null;
|
|
304
|
+
if (fixed) { goals.set(ref, { ref, title: fixed.title, description: fixed.why }); return; }
|
|
241
305
|
const g = byRef.get(ref);
|
|
242
306
|
if (g) goals.set(ref, { ref, title: g.title, description: describe(g, mapId) });
|
|
243
307
|
};
|
|
@@ -245,13 +309,14 @@ function planAcceptance({ items, mapId, select, filedRefs = new Set() }) {
|
|
|
245
309
|
const it = byRef.get(ref);
|
|
246
310
|
if (!it) continue;
|
|
247
311
|
if (filedRefs.has(ref)) { skipped.push({ ref, reason: 'already_filed' }); continue; }
|
|
312
|
+
if (select === 'all' && dismissedRefs.has(ref)) { skipped.push({ ref, reason: 'dismissed' }); continue; }
|
|
248
313
|
if (it.type === 'goal') { needGoal(ref); continue; }
|
|
249
314
|
if (it.type === 'task' && it.status === 'done') { skipped.push({ ref, reason: 'already_done' }); continue; }
|
|
250
315
|
needGoal(it.goal_ref);
|
|
251
316
|
tasks.push({
|
|
252
317
|
ref,
|
|
253
318
|
goalRef: it.goal_ref,
|
|
254
|
-
title: it.title,
|
|
319
|
+
title: it.type === 'module' ? `Switch on the ${it.title} module` : it.title,
|
|
255
320
|
description: describe(it, mapId),
|
|
256
321
|
kind: it.type === 'next' && it.kind !== 'gap' ? 'bug' : 'feature',
|
|
257
322
|
});
|
|
@@ -260,6 +325,6 @@ function planAcceptance({ items, mapId, select, filedRefs = new Set() }) {
|
|
|
260
325
|
}
|
|
261
326
|
|
|
262
327
|
module.exports = {
|
|
263
|
-
SCHEMA, MAP_FILE, LIMITS, NEXT_GOAL,
|
|
328
|
+
SCHEMA, MAP_FILE, LIMITS, NEXT_GOAL, MODULES_GOAL,
|
|
264
329
|
exampleMap, firstReadPrompt, validateRepoMap, planAcceptance,
|
|
265
330
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- lifecycle_023_repo_map_dismissed.sql — the owner's "no" to an item of a repository
|
|
2
|
+
-- map is remembered too (task 1005281, goal 1000106).
|
|
3
|
+
--
|
|
4
|
+
-- A module recommendation is a suggestion the owner accepts OR dismisses. A dismissed
|
|
5
|
+
-- item is a row in lifecycle_repo_map_filed with kind 'dismissed' and no entity, so a
|
|
6
|
+
-- later read-through's "accept all" does not raise it again. Naming it explicitly later
|
|
7
|
+
-- still files it: the accept upserts over a 'dismissed' row only, never over a filed one.
|
|
8
|
+
-- Works for every item type, not only modules.
|
|
9
|
+
|
|
10
|
+
ALTER TABLE lifecycle_repo_map_filed ALTER COLUMN entity_id DROP NOT NULL;
|
|
11
|
+
|
|
12
|
+
ALTER TABLE lifecycle_repo_map_filed DROP CONSTRAINT IF EXISTS lifecycle_repo_map_filed_kind_chk;
|
|
13
|
+
ALTER TABLE lifecycle_repo_map_filed ADD CONSTRAINT lifecycle_repo_map_filed_kind_chk
|
|
14
|
+
CHECK (kind IN ('goal', 'task', 'dismissed'));
|
|
15
|
+
|
|
16
|
+
-- A filed item points at what it became; a dismissed one points at nothing.
|
|
17
|
+
ALTER TABLE lifecycle_repo_map_filed DROP CONSTRAINT IF EXISTS lifecycle_repo_map_filed_entity_chk;
|
|
18
|
+
ALTER TABLE lifecycle_repo_map_filed ADD CONSTRAINT lifecycle_repo_map_filed_entity_chk
|
|
19
|
+
CHECK ((kind = 'dismissed') = (entity_id IS NULL));
|
|
@@ -13,23 +13,34 @@ const express = require('express');
|
|
|
13
13
|
const api = require('../../../src/module-api');
|
|
14
14
|
const fr = require('../first-read');
|
|
15
15
|
const store = require('../first-read-db');
|
|
16
|
+
const modulesCatalog = require('../first-read-catalog');
|
|
16
17
|
|
|
17
18
|
const { asyncHandler, parseId, validateOrRespond } = api;
|
|
18
19
|
|
|
19
20
|
module.exports = function buildFirstReadRouter() {
|
|
20
21
|
const router = express.Router();
|
|
21
22
|
|
|
22
|
-
// rank: any-builder — the prompt and the shape are instructions, not project data.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
// rank: any-builder — the prompt and the shape are instructions, not project data. The
|
|
24
|
+
// prompt carries the live module list (task 1005281), so a session recommends from it.
|
|
25
|
+
router.get('/first-read/prompt', api.requireBuilder, asyncHandler('GET /first-read/prompt', async (req, res) => {
|
|
26
|
+
const catalog = await modulesCatalog.switchOnCatalog();
|
|
27
|
+
res.json({ schema: fr.SCHEMA, file: fr.MAP_FILE, prompt: fr.firstReadPrompt({ catalog }), example: fr.exampleMap(), limits: fr.LIMITS, catalog });
|
|
28
|
+
}, { errorCode: 'first_read_prompt_failed' }));
|
|
29
|
+
|
|
30
|
+
// rank: any-builder — the modules this project can switch on, the only ones a map may
|
|
31
|
+
// recommend. `available: false` when the list cannot be read here.
|
|
32
|
+
router.get('/first-read/catalog', api.requireBuilder, asyncHandler('GET /first-read/catalog', async (req, res) => {
|
|
33
|
+
const catalog = await modulesCatalog.switchOnCatalog();
|
|
34
|
+
res.json({ available: Array.isArray(catalog), modules: catalog || [] });
|
|
35
|
+
}, { errorCode: 'first_read_catalog_failed' }));
|
|
26
36
|
|
|
27
37
|
// rank: goal.create — a map is a planning proposal for this project. Stores it
|
|
28
38
|
// pending; files nothing. A malformed map is refused with every error named.
|
|
29
39
|
router.post('/first-read/maps', api.requireBuilder, api.requirePermission('goal.create'), asyncHandler('POST /first-read/maps', async (req, res) => {
|
|
30
40
|
// The envelope is checked here; the map inside it by first-read.js, which names every error.
|
|
31
41
|
if (validateOrRespond(req, res, { map: { required: true, type: 'object' } })) return undefined;
|
|
32
|
-
const
|
|
42
|
+
const catalog = await modulesCatalog.switchOnCatalog();
|
|
43
|
+
const out = await store.submitMap({ raw: req.body.map, builderId: req.builder.id, catalog });
|
|
33
44
|
if (!out.ok) return res.fail('invalid_map', 400, { errors: out.errors });
|
|
34
45
|
return res.status(201).json({
|
|
35
46
|
map_id: out.map_id, status: out.status,
|
|
@@ -66,6 +77,17 @@ module.exports = function buildFirstReadRouter() {
|
|
|
66
77
|
return res.json(out);
|
|
67
78
|
}, { errorCode: 'first_read_accept_failed' }));
|
|
68
79
|
|
|
80
|
+
// rank: goal.create — the owner's no to named items (a module recommendation, usually).
|
|
81
|
+
// Files nothing; a later "accept all" passes them by.
|
|
82
|
+
router.post('/first-read/maps/:id/dismiss', api.requireBuilder, api.requirePermission('goal.create'), asyncHandler('POST /first-read/maps/:id/dismiss', async (req, res) => {
|
|
83
|
+
const mapId = parseId(req, res, { positiveInt: true });
|
|
84
|
+
if (mapId == null) return undefined;
|
|
85
|
+
if (validateOrRespond(req, res, { items: { required: true, type: 'array', itemsType: 'string', maxItems: 500 } })) return undefined;
|
|
86
|
+
const out = await store.dismissItems({ mapId, refs: req.body.items, builderId: req.builder.id });
|
|
87
|
+
if (!out.ok) { const { code, status, ...rest } = out; return res.fail(code, status, rest); }
|
|
88
|
+
return res.json(out);
|
|
89
|
+
}, { errorCode: 'first_read_dismiss_failed' }));
|
|
90
|
+
|
|
69
91
|
// rank: goal.create — the owner's no. Files nothing.
|
|
70
92
|
router.post('/first-read/maps/:id/reject', api.requireBuilder, api.requirePermission('goal.create'), asyncHandler('POST /first-read/maps/:id/reject', async (req, res) => {
|
|
71
93
|
const mapId = parseId(req, res, { positiveInt: true });
|
|
@@ -27,6 +27,15 @@
|
|
|
27
27
|
// as `board.genesis_stage.passed` (the route factory listens), and every read
|
|
28
28
|
// reconciles again, so a missed nudge cannot strand a stage.
|
|
29
29
|
//
|
|
30
|
+
// WITH NO BOARD ROOM (task 1005115; owner, 2026-11-07: "I would automatically proceed").
|
|
31
|
+
// A project without the government module has nobody to put a stage to, so a stage
|
|
32
|
+
// whose tasks are all finished moves on BY ITSELF — reconcile does it, on every read and
|
|
33
|
+
// on every ship — and the view says so (`moved`: from, to, what woke). There is no
|
|
34
|
+
// close button to press on that path. Where there IS a Board Room the sitting stays:
|
|
35
|
+
// a team deciding together is not the same act as a solo founder acknowledging their
|
|
36
|
+
// own work. Either way, each stage leaves a DOCUMENT behind (genesis-work.js
|
|
37
|
+
// documentOf) that the founder can edit at any time without reopening the stage.
|
|
38
|
+
//
|
|
30
39
|
// THE BOUNDARY. Core through ../../src/module-api.js; the lifecycle and government
|
|
31
40
|
// modules through their ports only (resolveOptional), never a require.
|
|
32
41
|
|
|
@@ -158,7 +167,11 @@ function taskState(t) {
|
|
|
158
167
|
// task, named in `waits_on`) or 'open' — decided here, never re-derived in a browser.
|
|
159
168
|
// board { stage: genesisStageStatus row } or {}
|
|
160
169
|
// stageAt { stage: ISO } — when each stage began, for its due date
|
|
161
|
-
|
|
170
|
+
// boardRoom false on a project with no government module: stages advance by
|
|
171
|
+
// themselves, so nothing is ever "closable" and the alive door opens on
|
|
172
|
+
// finished legislation tasks (task 1005115)
|
|
173
|
+
// documents { stage: { title, filled, updated_at, editable } } from genesis-work.js
|
|
174
|
+
function shapeGenesis({ mode, plan, tasks = {}, board = {}, stageAt = {}, boardRoom = true, documents = {} }) {
|
|
162
175
|
const at = fm.STAGES.indexOf(mode.stage);
|
|
163
176
|
const stages = PROPER.map((key) => {
|
|
164
177
|
const info = STAGE_INFO[key];
|
|
@@ -190,7 +203,10 @@ function shapeGenesis({ mode, plan, tasks = {}, board = {}, stageAt = {} }) {
|
|
|
190
203
|
total: list.length,
|
|
191
204
|
// task 1005199: this stage (the current one or any later one) can be put to
|
|
192
205
|
// the Board Room now — its tasks are all finished and it is not already there.
|
|
193
|
-
can_close: at >= 1 && idx >= at && allFinished && !onBoard,
|
|
206
|
+
can_close: boardRoom && at >= 1 && idx >= at && allFinished && !onBoard,
|
|
207
|
+
// What this stage left behind (task 1005115): null where the store is not read.
|
|
208
|
+
document: documents[key] || null,
|
|
209
|
+
all_finished: allFinished,
|
|
194
210
|
board: b ? {
|
|
195
211
|
state: b.state, item_id: b.item_id || null, needs: b.needs || null,
|
|
196
212
|
one_vote_decides: !!b.one_vote_decides, waiting_on: b.waiting_on || [],
|
|
@@ -200,6 +216,14 @@ function shapeGenesis({ mode, plan, tasks = {}, board = {}, stageAt = {} }) {
|
|
|
200
216
|
const now = stages.find((s) => s.state === 'now') || null;
|
|
201
217
|
const sitting = now && now.board && now.board.state === 'sitting';
|
|
202
218
|
const passed = now && now.board && now.board.state === 'passed';
|
|
219
|
+
// The last move, so a presentation can say plainly that it happened and what opened
|
|
220
|
+
// (task 1005115). Derived from the stage stamps, never stored twice.
|
|
221
|
+
const prev = at >= 2 ? fm.STAGES[at - 1] : null;
|
|
222
|
+
const moved = prev && stageAt[mode.stage] ? {
|
|
223
|
+
from: prev, to: mode.stage, at: stageAt[mode.stage],
|
|
224
|
+
how: boardRoom ? 'board_passed' : 'tasks_finished',
|
|
225
|
+
opened: fm.WAKES[prev] || [],
|
|
226
|
+
} : null;
|
|
203
227
|
return {
|
|
204
228
|
stage: mode.stage,
|
|
205
229
|
stage_number: mode.stage_number,
|
|
@@ -211,9 +235,12 @@ function shapeGenesis({ mode, plan, tasks = {}, board = {}, stageAt = {} }) {
|
|
|
211
235
|
demo: plan && plan.demo ? plan.demo : null,
|
|
212
236
|
stages,
|
|
213
237
|
// The close door: every task of the current stage finished, and not already sitting.
|
|
214
|
-
can_close: !!(now && now.total > 0 && now.finished === now.total && !sitting && !passed),
|
|
215
|
-
// Alive: the last stage has passed its sitting
|
|
216
|
-
|
|
238
|
+
can_close: !!(boardRoom && now && now.total > 0 && now.finished === now.total && !sitting && !passed),
|
|
239
|
+
// Alive: the last stage has passed its sitting — or, with no Board Room, its tasks
|
|
240
|
+
// are all finished.
|
|
241
|
+
can_come_alive: !!(now && now.key === 'legislation' && (boardRoom ? passed : now.total > 0 && now.finished === now.total)),
|
|
242
|
+
board_room: boardRoom,
|
|
243
|
+
moved,
|
|
217
244
|
};
|
|
218
245
|
}
|
|
219
246
|
|
|
@@ -226,9 +253,18 @@ function seams(deps = {}) {
|
|
|
226
253
|
set: deps.setSetting || api.projectSettings.set,
|
|
227
254
|
lifecycle: deps.lifecycle !== undefined ? deps.lifecycle : api.resolveOptional('lifecycle.kickoffSeed'),
|
|
228
255
|
government: deps.government !== undefined ? deps.government : api.resolveOptional('government'),
|
|
256
|
+
// Each stage's document summary (task 1005115): the founder's store unless injected.
|
|
257
|
+
// Fails soft in readGenesis, so a hall without the tables still renders.
|
|
258
|
+
documents: deps.documents !== undefined ? deps.documents : () => require('./genesis-work').readDocuments(),
|
|
229
259
|
now: deps.now,
|
|
230
260
|
};
|
|
231
261
|
}
|
|
262
|
+
|
|
263
|
+
// hasBoardRoom(gov) — can a stage be put to a sitting at all? Without one a finished
|
|
264
|
+
// stage advances by itself (task 1005115).
|
|
265
|
+
const hasBoardRoom = (gov) => !!(gov && (typeof gov.closeGenesisStage === 'function'
|
|
266
|
+
|| typeof gov.openGenesisStageClose === 'function'));
|
|
267
|
+
const allDone = (list) => !!(list && list.length && list.every((t) => FINISHED.includes(t.status)));
|
|
232
268
|
const modeDeps = (s) => ({ getSetting: s.get, setSetting: s.set, government: s.government, now: s.now });
|
|
233
269
|
|
|
234
270
|
async function boardByStage(gov) {
|
|
@@ -245,6 +281,15 @@ async function boardByStage(gov) {
|
|
|
245
281
|
async function reconcile(s) {
|
|
246
282
|
let mode = await fm.foundingModeFor(modeDeps(s));
|
|
247
283
|
if (!mode || !PROPER.includes(mode.stage) || mode.stage === 'legislation') return mode;
|
|
284
|
+
if (!hasBoardRoom(s.government)) {
|
|
285
|
+
// No Board Room: finished work IS the decision (task 1005115). Move past every
|
|
286
|
+
// stage whose tasks are all finished, in order; legislation waits for the alive door.
|
|
287
|
+
const all = s.lifecycle && typeof s.lifecycle.genesisTasks === 'function' ? await s.lifecycle.genesisTasks() : {};
|
|
288
|
+
while (mode && PROPER.includes(mode.stage) && mode.stage !== 'legislation' && allDone(all[mode.stage])) {
|
|
289
|
+
mode = await fm.setFoundingMode({ stage: fm.STAGES[fm.STAGES.indexOf(mode.stage) + 1] }, null, modeDeps(s));
|
|
290
|
+
}
|
|
291
|
+
return mode;
|
|
292
|
+
}
|
|
248
293
|
const board = await boardByStage(s.government);
|
|
249
294
|
while (mode && PROPER.includes(mode.stage) && mode.stage !== 'legislation'
|
|
250
295
|
&& board[mode.stage] && board[mode.stage].state === 'passed') {
|
|
@@ -268,7 +313,11 @@ async function readGenesis(deps = {}) {
|
|
|
268
313
|
const row = await s.get(fm.stageAtKey(key));
|
|
269
314
|
if (row && row.value) stageAt[key] = row.value;
|
|
270
315
|
}
|
|
271
|
-
|
|
316
|
+
let documents = {};
|
|
317
|
+
if (typeof s.documents === 'function') {
|
|
318
|
+
try { documents = (await s.documents()) || {}; } catch (_) { documents = {}; } // the home still renders without them
|
|
319
|
+
}
|
|
320
|
+
return shapeGenesis({ mode, plan, tasks, board, stageAt, boardRoom: hasBoardRoom(s.government), documents });
|
|
272
321
|
}
|
|
273
322
|
|
|
274
323
|
// startFounding — a brand-new project enters genesis (founding-birth.js calls this once
|
|
@@ -403,6 +452,13 @@ async function closeStage(stageKey, builderId, deps = {}) {
|
|
|
403
452
|
// stage's pass waits for the earlier ones. A stage already behind, or the decide
|
|
404
453
|
// pre-stage, is still refused.
|
|
405
454
|
if (!PROPER.includes(stageKey)) return { ok: false, reason: 'unknown_stage' };
|
|
455
|
+
// No Board Room (task 1005115): the stage already moved on by itself when its tasks
|
|
456
|
+
// finished, so asking to close it again is answered with what happened, not refused.
|
|
457
|
+
if (!hasBoardRoom(s.government) && PROPER.includes(mode.stage)
|
|
458
|
+
&& fm.STAGES.indexOf(stageKey) < fm.STAGES.indexOf(mode.stage)) {
|
|
459
|
+
// next_stage is where the project IS now, which may be several stages on.
|
|
460
|
+
return { ok: true, closed: true, stage: stageKey, next_stage: mode.stage, item_id: null, confirm: null, already: true };
|
|
461
|
+
}
|
|
406
462
|
if (!PROPER.includes(mode.stage) || fm.STAGES.indexOf(stageKey) < fm.STAGES.indexOf(mode.stage)) {
|
|
407
463
|
return { ok: false, reason: 'not_current_stage', stage: mode.stage };
|
|
408
464
|
}
|
|
@@ -546,7 +602,12 @@ async function putFinishedStages(deps = {}) {
|
|
|
546
602
|
// stage; on a project that is not founding this costs the one genesis.enabled read.
|
|
547
603
|
// Best-effort: the founder's own close button is still there if this misses.
|
|
548
604
|
async function onTaskShipped(deps = {}) {
|
|
549
|
-
try {
|
|
605
|
+
try {
|
|
606
|
+
const s = seams(deps);
|
|
607
|
+
// No Board Room: the ship that finishes a stage moves the project on (task 1005115).
|
|
608
|
+
if (!hasBoardRoom(s.government)) { await reconcile(s); return []; }
|
|
609
|
+
return await putFinishedStages(deps);
|
|
610
|
+
} catch (_) { return []; }
|
|
550
611
|
}
|
|
551
612
|
|
|
552
613
|
module.exports = {
|