@bongos/core 1.21.72 → 1.21.74

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 (68) hide show
  1. package/.bongos-core.json +87 -72
  2. package/bin/bongos.js +1 -0
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +4 -0
  5. package/clients/bongos-client/index.cjs +4 -0
  6. package/clients/bongos-client/index.d.ts +7 -0
  7. package/clients/bongos-client/index.mjs +4 -0
  8. package/docs/adr/0369-ship-mode-decides-who-starts-a-ship-not-how-it-lands.md +44 -0
  9. package/docs/adr/README.md +1 -0
  10. package/docs/api/openapi.json +139 -3
  11. package/docs/api-reference.md +4 -2
  12. package/docs/copy-inventory.md +40 -36
  13. package/docs/copy-registry.json +97 -51
  14. package/docs/module-api-changelog.md +4 -0
  15. package/docs/page-readings.json +329 -330
  16. package/modules/hall-ui/public/atlas.html +3 -3
  17. package/modules/hall-ui/public/blockers.html +3 -3
  18. package/modules/hall-ui/public/board-room.html +3 -3
  19. package/modules/hall-ui/public/city.html +3 -3
  20. package/modules/hall-ui/public/collab.html +3 -3
  21. package/modules/hall-ui/public/commands.html +3 -3
  22. package/modules/hall-ui/public/copy-desk.html +3 -3
  23. package/modules/hall-ui/public/craft-mode.js +69 -1
  24. package/modules/hall-ui/public/deploy.html +3 -3
  25. package/modules/hall-ui/public/diagrams.html +3 -3
  26. package/modules/hall-ui/public/drachmae.html +3 -3
  27. package/modules/hall-ui/public/fleet.html +3 -3
  28. package/modules/hall-ui/public/gate.html +3 -3
  29. package/modules/hall-ui/public/goal-map.html +3 -3
  30. package/modules/hall-ui/public/goals.html +3 -3
  31. package/modules/hall-ui/public/government.html +3 -3
  32. package/modules/hall-ui/public/idea.html +3 -3
  33. package/modules/hall-ui/public/ideas.html +3 -3
  34. package/modules/hall-ui/public/index.html +3 -3
  35. package/modules/hall-ui/public/modules.html +3 -3
  36. package/modules/hall-ui/public/primer.html +3 -3
  37. package/modules/hall-ui/public/profile.html +3 -3
  38. package/modules/hall-ui/public/project-settings.css +15 -0
  39. package/modules/hall-ui/public/project-settings.html +7 -7
  40. package/modules/hall-ui/public/project-settings.js +42 -0
  41. package/modules/hall-ui/public/ranks.html +3 -3
  42. package/modules/hall-ui/public/roadmap.html +3 -3
  43. package/modules/hall-ui/public/roster.html +3 -3
  44. package/modules/hall-ui/public/sessions.html +3 -3
  45. package/modules/hall-ui/public/settings.html +3 -3
  46. package/modules/hall-ui/public/shell.js +9 -0
  47. package/modules/hall-ui/public/street.html +3 -3
  48. package/modules/hall-ui/public/studio.html +3 -3
  49. package/modules/hall-ui/public/task.html +3 -3
  50. package/modules/hall-ui/public/thinking.html +3 -3
  51. package/modules/hall-ui/public/tweak-editor.html +3 -3
  52. package/modules/hall-ui/public/watch.html +3 -3
  53. package/modules/hall-ui/public/work.html +3 -3
  54. package/modules/lifecycle/routes/project-settings.js +65 -0
  55. package/package-lock.json +2 -2
  56. package/package.json +1 -1
  57. package/release-notes.json +12 -0
  58. package/scripts/gds/shipping-mode.js +71 -0
  59. package/src/bongos/project-settings.js +67 -0
  60. package/src/module-api.js +7 -1
  61. package/tests/craft_mode_nav.mjs +117 -1
  62. package/tests/hall_city.mjs +5 -3
  63. package/tests/hall_nav.mjs +3 -1
  64. package/tests/hall_street.mjs +2 -1
  65. package/tests/hall_tweak_editor.mjs +1 -1
  66. package/tests/nav_permission_atoms.mjs +1 -1
  67. package/tests/project_settings_page.mjs +8 -5
  68. package/tests/ship_mode.mjs +171 -0
@@ -87,7 +87,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
87
87
  // the file and left the query alone, so a returning browser kept the cached copy —
88
88
  // a fix that reaches nobody looks identical to a fix that does not work.
89
89
 
90
- const SHELL_SHA = 'af1c61ca87767b44';
90
+ const SHELL_SHA = '02e600a176c32f6a';
91
91
 
92
92
  test('shell.js has not changed without its cache-bust being bumped', () => {
93
93
  const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
@@ -63,8 +63,10 @@ function buildRouterWithStubs({ timer, categories, reward = WORKING_REWARD }) {
63
63
  resolveOptional: api.resolveOptional,
64
64
  list: gc.listCategories,
65
65
  set: api.projectSettings.set,
66
+ resolveShipMode: api.projectSettings.resolveShipMode,
66
67
  };
67
68
  api.resolveRotDays = async () => timer;
69
+ api.projectSettings.resolveShipMode = async () => ({ mode: 'auto', source: 'default', chosen: false, invalid: null });
68
70
  // reward: WORKING_REWARD (default) | null (port absent) | 'throws' (rewardConfig raises).
69
71
  const rewardPort = reward === 'throws'
70
72
  ? { rewardConfig: () => { throw new Error('reward boom'); } }
@@ -80,6 +82,7 @@ function buildRouterWithStubs({ timer, categories, reward = WORKING_REWARD }) {
80
82
  api.resolveOptional = saved.resolveOptional;
81
83
  gc.listCategories = saved.list;
82
84
  api.projectSettings.set = saved.set;
85
+ api.projectSettings.resolveShipMode = saved.resolveShipMode;
83
86
  };
84
87
  // The handler is the LAST layer on the matching route (requireBuilder and the
85
88
  // permission gate sit in front); calling it directly is what keeps this a unit test
@@ -99,22 +102,22 @@ const CATS = [
99
102
  { id: 2, slug: 'retired-thing', label: 'Retired thing', sort_order: 20, active: false },
100
103
  ];
101
104
 
102
- test('one read returns all five fundamentals, each naming where its value came from', async () => {
105
+ test('one read returns all six fundamentals, each naming where its value came from', async () => {
103
106
  const { handlerFor, restore } = buildRouterWithStubs({ timer: TIMER_OK, categories: CATS });
104
107
  try {
105
108
  const res = fakeRes();
106
109
  await handlerFor('get', '/project-settings')({ query: {} }, res);
107
110
  const rows = res.payload.rows;
108
- assert.equal(rows.length, 5, 'the page draws five rows, so the read returns five');
111
+ assert.equal(rows.length, 6, 'the page draws six rows, so the read returns six');
109
112
  assert.deepEqual(rows.map((r) => r.key),
110
- ['rot_timer', 'work_categories', 'credit_calculation', 'equity_calculation', 'upmarket_percent']);
113
+ ['ship_mode', 'rot_timer', 'work_categories', 'credit_calculation', 'equity_calculation', 'upmarket_percent']);
111
114
  // Provenance is the point of the page: every row says where its value came from.
112
115
  for (const r of rows) {
113
116
  assert.ok(r.reads && r.reads.file, `${r.key} names no file`);
114
117
  assert.ok(r.sentence && r.sentence.length > 20, `${r.key} has no readable sentence`);
115
118
  }
116
- // Exactly two knobs — the criterion's "change the two that are knobs".
117
- assert.deepEqual(rows.filter((r) => r.editable).map((r) => r.key), ['rot_timer', 'work_categories']);
119
+ // Three knobs: the criterion's two, plus ship mode (task 1004628).
120
+ assert.deepEqual(rows.filter((r) => r.editable).map((r) => r.key), ['ship_mode', 'rot_timer', 'work_categories']);
118
121
  // A no-store receipt: a cached settings read is wrong the moment a knob turns.
119
122
  assert.equal(res.headers['Cache-Control'], 'no-store');
120
123
  } finally { restore(); }
@@ -0,0 +1,171 @@
1
+ // tests/ship_mode.mjs — the project's ship mode: who starts a ship (task 1004628, ADR 0369).
2
+ //
3
+ // Pinned here:
4
+ // 1. the parser accepts exactly auto|manual (trimmed, any case) and explains a refusal;
5
+ // 2. the resolver defaults to AUTO, tells "never chosen" apart from "chose auto",
6
+ // reports an unusable stored value instead of throwing, and survives a dead DB;
7
+ // 3. the routes: any builder can READ it (the AI must know), the write validates
8
+ // through the same parser and echoes what the system will now read;
9
+ // 4. the CLI verb is registered and its text names how to change the mode.
10
+ //
11
+ // Run: node --test tests/ship_mode.mjs
12
+ import { strict as assert } from 'node:assert';
13
+ import { test } from 'node:test';
14
+ import { createRequire } from 'node:module';
15
+ import path from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ const require = createRequire(import.meta.url);
19
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
20
+
21
+ const poolMod = require(path.join(ROOT, 'src', 'bongos', 'pool.js'));
22
+ // Destructured from a literal relative path, so the dead-code scan (knip) can see
23
+ // these exports are used — a computed path.join() is invisible to it.
24
+ const {
25
+ SHIP_MODES, DEFAULT_SHIP_MODE, parseShipMode, resolveShipMode, describeShipMode,
26
+ } = require('../src/bongos/project-settings.js');
27
+ const ps = { SHIP_MODES, DEFAULT_SHIP_MODE, parseShipMode, resolveShipMode, describeShipMode };
28
+ const api = require(path.join(ROOT, 'src', 'module-api.js'));
29
+
30
+ // Swap the pool's query for the duration of one call; project-settings reads
31
+ // `pool.query` at call time, so this reaches the real SQL path.
32
+ async function withPool(query, fn) {
33
+ const pool = poolMod.pool;
34
+ const saved = pool.query;
35
+ pool.query = query;
36
+ try { return await fn(); } finally { pool.query = saved; }
37
+ }
38
+ const rowWith = (value) => async () => ({ rows: value == null ? [] : [{ key: 'ship.mode', value, updated_at: null, updated_by: 1 }] });
39
+
40
+ test('parseShipMode accepts exactly auto|manual, trimmed and in any case', () => {
41
+ assert.deepEqual([...ps.SHIP_MODES], ['auto', 'manual']);
42
+ assert.deepEqual(ps.parseShipMode('auto'), { mode: 'auto' });
43
+ assert.deepEqual(ps.parseShipMode(' Manual '), { mode: 'manual' });
44
+ assert.equal(ps.parseShipMode('').reason, 'empty');
45
+ assert.equal(ps.parseShipMode(null).reason, 'empty');
46
+ assert.match(ps.parseShipMode('yolo').reason, /auto \| manual/);
47
+ });
48
+
49
+ test('the default is auto, and an unset project reads as NOT chosen', async () => {
50
+ assert.equal(ps.DEFAULT_SHIP_MODE, 'auto');
51
+ const r = await withPool(rowWith(null), () => ps.resolveShipMode());
52
+ assert.deepEqual(r, { mode: 'auto', source: 'default', chosen: false, invalid: null });
53
+ });
54
+
55
+ test('a stored choice is read back as chosen — even when it equals the default', async () => {
56
+ const auto = await withPool(rowWith('auto'), () => ps.resolveShipMode());
57
+ assert.equal(auto.mode, 'auto');
58
+ assert.equal(auto.chosen, true, 'choosing auto is different from never choosing');
59
+ assert.equal(auto.source, 'project_settings');
60
+ const manual = await withPool(rowWith('manual'), () => ps.resolveShipMode());
61
+ assert.equal(manual.mode, 'manual');
62
+ });
63
+
64
+ test('an unusable stored value falls back to auto and is reported, not thrown', async () => {
65
+ const r = await withPool(rowWith('sometimes'), () => ps.resolveShipMode());
66
+ assert.equal(r.mode, 'auto');
67
+ assert.equal(r.chosen, false);
68
+ assert.equal(r.invalid.raw, 'sometimes');
69
+ assert.match(r.invalid.where, /ship\.mode/);
70
+ });
71
+
72
+ test('a database that cannot be read answers the default instead of failing a ship', async () => {
73
+ const r = await withPool(async () => { throw new Error('db down'); }, () => ps.resolveShipMode());
74
+ assert.deepEqual(r, { mode: 'auto', source: 'default', chosen: false, invalid: null });
75
+ });
76
+
77
+ test('describeShipMode says what each mode means in plain words', () => {
78
+ assert.match(ps.describeShipMode({ mode: 'auto', chosen: true }), /ships and lands it/);
79
+ assert.match(ps.describeShipMode({ mode: 'manual', chosen: true }), /waits for a person/);
80
+ assert.match(ps.describeShipMode({ mode: 'auto', chosen: false }), /nobody has chosen yet/);
81
+ });
82
+
83
+ // ---- the routes -------------------------------------------------------------
84
+
85
+ const fakeRes = () => ({
86
+ code: null, statusCode: 200, payload: null, headers: {},
87
+ fail(code, opts) { this.code = code; this.statusCode = opts?.status ?? 400; this.payload = { error: code, ...(opts || {}) }; return this; },
88
+ status(c) { this.statusCode = c; return this; },
89
+ set(k, v) { this.headers[k] = v; return this; },
90
+ json(p) { this.payload = p; return this; },
91
+ });
92
+
93
+ function buildRouter({ resolved }) {
94
+ const saved = { resolve: api.projectSettings.resolveShipMode, set: api.projectSettings.set };
95
+ const writes = [];
96
+ let current = resolved;
97
+ api.projectSettings.resolveShipMode = async () => current;
98
+ api.projectSettings.set = async (key, value, by) => {
99
+ writes.push({ key, value, by });
100
+ current = { mode: value, source: 'project_settings', chosen: true, invalid: null };
101
+ return { key, value };
102
+ };
103
+ const file = path.join(ROOT, 'modules', 'lifecycle', 'routes', 'project-settings.js');
104
+ delete require.cache[require.resolve(file)];
105
+ const router = require(file)();
106
+ const route = (method, p) => router.stack.find((l) => l.route && l.route.path === p && l.route.methods[method]).route;
107
+ const restore = () => { api.projectSettings.resolveShipMode = saved.resolve; api.projectSettings.set = saved.set; };
108
+ return { route, writes, restore };
109
+ }
110
+
111
+ test('GET /project-settings/ship-mode is open to any signed-in builder, not gated Metic+', () => {
112
+ const { route, restore } = buildRouter({ resolved: { mode: 'auto', source: 'default', chosen: false, invalid: null } });
113
+ try {
114
+ const names = route('get', '/project-settings/ship-mode').stack.map((l) => l.name);
115
+ assert.equal(names.length, 2, 'requireBuilder + the handler, and no permission gate');
116
+ const write = route('patch', '/project-settings/ship-mode').stack;
117
+ assert.equal(write.length, 3, 'the write carries requireBuilder + the settings permission + the handler');
118
+ } finally { restore(); }
119
+ });
120
+
121
+ test('the read returns the mode, whether it was chosen, and the sentence to show', async () => {
122
+ const { route, restore } = buildRouter({ resolved: { mode: 'auto', source: 'default', chosen: false, invalid: null } });
123
+ try {
124
+ const res = fakeRes();
125
+ const stack = route('get', '/project-settings/ship-mode').stack;
126
+ await stack[stack.length - 1].handle({}, res);
127
+ assert.equal(res.payload.mode, 'auto');
128
+ assert.equal(res.payload.chosen, false);
129
+ assert.match(res.payload.sentence, /nobody has chosen yet/);
130
+ assert.deepEqual(res.payload.choices.map((c) => c.mode), ['auto', 'manual']);
131
+ assert.match(res.payload.choices[0].label, /recommended/i);
132
+ assert.equal(res.headers['Cache-Control'], 'no-store');
133
+ } finally { restore(); }
134
+ });
135
+
136
+ test('the write refuses an unknown mode and saves a good one, echoing what is now read', async () => {
137
+ const { route, writes, restore } = buildRouter({ resolved: { mode: 'auto', source: 'default', chosen: false, invalid: null } });
138
+ try {
139
+ const stack = route('patch', '/project-settings/ship-mode').stack;
140
+ const handle = stack[stack.length - 1].handle;
141
+
142
+ const bad = fakeRes();
143
+ await handle({ body: { mode: 'sometimes' }, builder: { id: 7 } }, bad);
144
+ assert.equal(bad.code, 'bad_ship_mode');
145
+ assert.equal(writes.length, 0, 'a refused mode writes nothing');
146
+
147
+ const good = fakeRes();
148
+ await handle({ body: { mode: 'Manual' }, builder: { id: 7 } }, good);
149
+ assert.equal(good.code, null);
150
+ assert.deepEqual(writes, [{ key: 'ship.mode', value: 'manual', by: 7 }]);
151
+ assert.equal(good.payload.mode, 'manual');
152
+ assert.equal(good.payload.chosen, true);
153
+ } finally { restore(); }
154
+ });
155
+
156
+ // ---- the CLI ----------------------------------------------------------------
157
+
158
+ test('bongos ship-mode is a registered verb wired to shipping-mode.js', () => {
159
+ const cli = require(path.join(ROOT, 'bin', 'bongos.js'));
160
+ const def = cli.VERBS['ship-mode'];
161
+ assert.ok(def, 'bongos ship-mode must be a registered verb');
162
+ assert.equal(def.script, 'shipping-mode.js');
163
+ });
164
+
165
+ test('the CLI text names the mode and how to switch it', () => {
166
+ const { describe } = require(path.join(ROOT, 'scripts', 'gds', 'shipping-mode.js'));
167
+ const auto = describe({ mode: 'auto', chosen: false, sentence: 'Auto: …' });
168
+ assert.match(auto, /Ship mode: auto \(default — not chosen yet\)/);
169
+ assert.match(auto, /shipping-mode\.js manual/);
170
+ assert.match(describe({ mode: 'manual', chosen: true }), /shipping-mode\.js auto/);
171
+ });