@bongos/core 1.21.29 → 1.21.30

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 (50) hide show
  1. package/.bongos-core.json +60 -55
  2. package/docs/copy-inventory.md +8 -8
  3. package/docs/copy-registry.json +8 -8
  4. package/docs/module-api-changelog.md +2 -0
  5. package/docs/page-readings.json +229 -229
  6. package/modules/hall-ui/public/atlas.html +3 -3
  7. package/modules/hall-ui/public/blockers.html +3 -3
  8. package/modules/hall-ui/public/board-room.html +3 -3
  9. package/modules/hall-ui/public/city.html +3 -3
  10. package/modules/hall-ui/public/collab.html +3 -3
  11. package/modules/hall-ui/public/commands.html +3 -3
  12. package/modules/hall-ui/public/copy-desk.html +3 -3
  13. package/modules/hall-ui/public/craft-mode.js +57 -3
  14. package/modules/hall-ui/public/deploy.html +3 -3
  15. package/modules/hall-ui/public/diagrams.html +3 -3
  16. package/modules/hall-ui/public/drachmae.html +3 -3
  17. package/modules/hall-ui/public/fleet.html +3 -3
  18. package/modules/hall-ui/public/gate.html +3 -3
  19. package/modules/hall-ui/public/goal-map.html +3 -3
  20. package/modules/hall-ui/public/goals.html +3 -3
  21. package/modules/hall-ui/public/government.html +3 -3
  22. package/modules/hall-ui/public/idea.html +3 -3
  23. package/modules/hall-ui/public/ideas.html +3 -3
  24. package/modules/hall-ui/public/index.html +3 -3
  25. package/modules/hall-ui/public/modules.html +3 -3
  26. package/modules/hall-ui/public/primer.html +3 -3
  27. package/modules/hall-ui/public/profile.html +3 -3
  28. package/modules/hall-ui/public/project-settings.html +3 -3
  29. package/modules/hall-ui/public/ranks.html +3 -3
  30. package/modules/hall-ui/public/roadmap.html +3 -3
  31. package/modules/hall-ui/public/roster.html +3 -3
  32. package/modules/hall-ui/public/sessions.html +3 -3
  33. package/modules/hall-ui/public/settings.html +3 -3
  34. package/modules/hall-ui/public/shell.js +2 -0
  35. package/modules/hall-ui/public/street.html +3 -3
  36. package/modules/hall-ui/public/studio.html +3 -3
  37. package/modules/hall-ui/public/style.css +2 -0
  38. package/modules/hall-ui/public/task.html +3 -3
  39. package/modules/hall-ui/public/thinking.html +3 -3
  40. package/modules/hall-ui/public/tweak-editor.html +3 -3
  41. package/modules/hall-ui/public/watch.html +3 -3
  42. package/modules/hall-ui/public/work.html +3 -3
  43. package/modules/hall-ui/records/craft-mode.md +7 -0
  44. package/package-lock.json +2 -2
  45. package/package.json +1 -1
  46. package/release-notes.json +6 -0
  47. package/src/module-api.js +1 -1
  48. package/tests/craft_mode_nav.mjs +202 -0
  49. package/tests/hall_tweak_editor.mjs +1 -1
  50. package/tests/nav_permission_atoms.mjs +1 -1
@@ -171,9 +171,9 @@
171
171
 
172
172
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
173
173
  time so the chrome exists before the deferred page scripts execute. -->
174
- <script src="/builders/shell-trail.js?v=2026-10-02-craft"></script>
175
- <script src="/builders/craft-mode.js?v=2026-10-02-craft"></script>
176
- <script src="/builders/shell.js?v=2026-10-02-craft"></script>
174
+ <script src="/builders/shell-trail.js?v=2026-10-03-nav"></script>
175
+ <script src="/builders/craft-mode.js?v=2026-10-03-nav"></script>
176
+ <script src="/builders/shell.js?v=2026-10-03-nav"></script>
177
177
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
178
178
  <script src="/builders/palette.js" defer></script>
179
179
  <!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
@@ -20,3 +20,10 @@ Covers: every shelled hall page (`craft-mode.js`, `shell.js`, `style.css`), and
20
20
  ## The carve
21
21
 
22
22
  `shell.js` sat at exactly 1,500 lines, the fitness ratchet, so the Back link + trail section (tasks 1004430/1004476/1004477) moved verbatim into `shell-trail.js` (`window.OTBShellTrail.make({ esc, nav })`). Behaviour is unchanged; `tests/hall_fullscreen.mjs` loads it before the shell and still drives the trail through the real top bar.
23
+
24
+ ## The nav follows the mode (task 1004493)
25
+
26
+ - **Each craft has a named tab list**, `MODE_TABS` in `craft-mode.js`, by NAV id. `ALWAYS_TABS` is the owner's list of tabs no mode may hide (2026-10-01: Builders, NAV id `roster`, first), and every mode's list is `ALWAYS_TABS` plus its own. Legacy and no mode have no entry, so they show every tab, as before.
27
+ - **Government shows only in Governing**, and outside that group Governing is the shortest list (Blockers, Board Room). Building is every tab except Government and the Board Room; Artistry is Studio, Goals, Your thinking; Ideating is Sky, Your thinking, Goals. The prototype names rooms that do not exist yet (Store, Page reviews, Treasury); a list names only real NAV items, and `tests/craft_mode_nav.mjs` requires every NAV item to sit in some mode.
28
+ - **The mode only ever takes away, and never through `hidden`.** The access gate owns `hidden`; `applyNav()` marks what the mode leaves out with `[data-craft-off]`, which `style.css` hides. So a mode can never show a tab the gate hid, and switching mode just moves the attribute. A group head goes too once nothing under it shows.
29
+ - **Two callers.** `apply()` runs it on every mode change (live, no reload), and `shell.js` `applyAccess` runs it right after its gate loop, so a later `/me` answer (a promotion, a permission) brings a head back with its tabs. Hidden tabs still open by their URL; nothing is enforced (ADR 0016).
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.29",
3
+ "version": "1.21.30",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.29",
9
+ "version": "1.21.30",
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.21.29",
3
+ "version": "1.21.30",
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",
@@ -8930,5 +8930,11 @@
8930
8930
  "id": "1004548",
8931
8931
  "text": "After approving GitHub sign-in, founders go straight back to their setup checklist to wait, and can tick a box to have their project open automatically when it's ready."
8932
8932
  }
8933
+ ],
8934
+ "1.21.30": [
8935
+ {
8936
+ "id": "1004493",
8937
+ "text": "The hall's sidebar now follows the mode you pick. Building shows the everyday tabs, Artistry shows Studio, Goals and Your thinking, Ideating shows the Sky, Your thinking and Goals, and Governing is the shortest list: Blockers,"
8938
+ }
8933
8939
  ]
8934
8940
  }
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.21.29'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.30'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -0,0 +1,202 @@
1
+ // tests/craft_mode_nav.mjs — the hall nav follows the craft mode (task 1004493,
2
+ // goal 1000095 WA6).
3
+ //
4
+ // Each craft mode shows only the tabs its craft needs; Legacy mode and no mode
5
+ // show every tab, exactly as before. The owner's rule (2026-10-01): some tabs
6
+ // stay in EVERY mode, Builders first among them, written down as ONE shared list
7
+ // that each mode's list adds to. These cases boot the REAL shell-trail.js +
8
+ // craft-mode.js + shell.js and read the sidebar the shell actually rendered, so a
9
+ // tab id that drifts from NAV, or a gate pass that runs after the mode and
10
+ // re-shows a tab, reds here.
11
+ import assert from 'node:assert/strict';
12
+ import { test } from 'node:test';
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import vm from 'node:vm';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
19
+ const HALL = path.join(ROOT, 'modules/hall-ui/public');
20
+ const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
21
+ const SHELL = read('shell.js');
22
+ const TRAIL = read('shell-trail.js');
23
+ const CRAFT = read('craft-mode.js');
24
+
25
+ // Every NAV id, and the Government group's, read from shell.js itself.
26
+ const NAV_IDS = [...SHELL.matchAll(/\{ id: '([a-z-]+)', label:/g)].map((m) => m[1]);
27
+ const GOV_BLOCK = SHELL.slice(SHELL.indexOf("{ group: 'Government'"));
28
+ const GOV_IDS = [...GOV_BLOCK.slice(0, GOV_BLOCK.indexOf('] }')).matchAll(/\{ id: '([a-z-]+)'/g)].map((m) => m[1]);
29
+
30
+ // A small element. Elements parsed from the sidebar's markup are built ONCE, so
31
+ // what the gate loop writes (hidden) is what the mode pass reads back.
32
+ function makeEl(tag) {
33
+ const attrs = {};
34
+ return {
35
+ tagName: tag, id: '', dataset: {}, style: {}, hidden: false, children: [], _html: '',
36
+ classList: { _s: new Set(), add(c) { this._s.add(c); }, remove(c) { this._s.delete(c); }, toggle(c, on) { if (on === undefined) on = !this._s.has(c); if (on) this._s.add(c); else this._s.delete(c); return on; }, contains(c) { return this._s.has(c); } },
37
+ setAttribute(k, v) { attrs[k] = String(v); }, getAttribute(k) { return k in attrs ? attrs[k] : null; },
38
+ hasAttribute(k) { return k in attrs; }, removeAttribute(k) { delete attrs[k]; },
39
+ addEventListener() {}, appendChild(c) { this.children.push(c); return c; }, removeChild() {}, insertBefore(c) { return c; },
40
+ focus() {}, closest() { return null; }, querySelector() { return null; }, querySelectorAll() { return []; },
41
+ };
42
+ }
43
+ // The rendered sidebar as groups of { head, items }.
44
+ function parseSidebar(html) {
45
+ return html.split('<div class="nav-group').slice(1).map((chunk) => {
46
+ const group = makeEl('DIV');
47
+ const tag = (m, t) => {
48
+ const el = makeEl(t);
49
+ for (const a of m.matchAll(/([a-z-]+)="([^"]*)"/g)) {
50
+ if (a[1] === 'id') el.id = a[2];
51
+ else if (a[1].startsWith('data-')) el.dataset[a[1].slice(5)] = a[2];
52
+ }
53
+ el.hidden = /\shidden[\s>]/.test(m);
54
+ return el;
55
+ };
56
+ const headM = chunk.match(/<button type="button" class="nav-group__head"[^>]*>/);
57
+ const head = headM ? tag(headM[0], 'BUTTON') : null;
58
+ const items = [...chunk.matchAll(/<a class="nav-item"[^>]*>/g)].map((m) => tag(m[0], 'A'));
59
+ group.querySelector = (sel) => (sel === '.nav-group__head' ? head : null);
60
+ group.querySelectorAll = (sel) => (sel === '.nav-item' ? items : []);
61
+ return { el: group, head, items };
62
+ });
63
+ }
64
+
65
+ function boot({ session = {}, perms = { 'board.vote.cast': true } } = {}) {
66
+ const root = makeEl('HTML');
67
+ const topbar = makeEl('DIV');
68
+ const sidebar = makeEl('NAV');
69
+ const body = makeEl('BODY');
70
+ body.dataset.page = 'work';
71
+ let groups = null;
72
+ const parsed = () => (groups ||= parseSidebar(sidebar._html));
73
+ Object.defineProperty(sidebar, 'innerHTML', { set(v) { sidebar._html = String(v); groups = null; }, get() { return sidebar._html; } });
74
+ sidebar.querySelectorAll = (sel) => {
75
+ if (sel === '.nav-item') return parsed().flatMap((g) => g.items);
76
+ if (sel === '.nav-group') return parsed().map((g) => g.el);
77
+ return [];
78
+ };
79
+ const gated = () => parsed().flatMap((g) => (g.head ? [g.head, ...g.items] : g.items));
80
+ const els = { 'app-topbar': topbar, 'app-sidebar': sidebar };
81
+ const documentObj = {
82
+ body, referrer: '', documentElement: root, activeElement: null,
83
+ getElementById: (id) => els[id] || null,
84
+ querySelector: () => null,
85
+ querySelectorAll: (sel) => (sel === '#app-sidebar [data-gate]' ? gated() : []),
86
+ createElement: (t) => makeEl(t.toUpperCase()),
87
+ addEventListener() {}, removeEventListener() {}, dispatchEvent() { return true; },
88
+ get cookie() { return ''; }, set cookie(_v) {},
89
+ };
90
+ const location = { hostname: 'h', origin: 'https://h', href: 'https://h/work', pathname: '/work', hash: '', search: '', assign() {}, replace() {}, reload() {} };
91
+ const mk = (obj) => ({ getItem: (k) => (k in obj ? obj[k] : null), setItem: (k, v) => { obj[k] = String(v); }, removeItem: (k) => { delete obj[k]; } });
92
+ const windowObj = {
93
+ __BRANDING__: {}, __MODULES__: { enabled: { provisioning: true } },
94
+ BongosClient: { createClient: () => ({ request: () => new Promise(() => {}) }) },
95
+ matchMedia: () => ({ matches: false, addEventListener() {} }),
96
+ localStorage: mk({}), sessionStorage: mk(session), location,
97
+ clearTimeout() {}, setTimeout() { return 0; }, requestAnimationFrame() {}, scrollTo() {},
98
+ addEventListener() {}, dispatchEvent() { return true; },
99
+ };
100
+ const sandbox = {
101
+ window: windowObj, document: documentObj, location, console, URL, URLSearchParams, queueMicrotask,
102
+ setTimeout() { return 0; }, clearTimeout() {},
103
+ Event: class { constructor(t) { this.type = t; } },
104
+ CustomEvent: class { constructor(t, i) { this.type = t; Object.assign(this, i); } },
105
+ };
106
+ sandbox.globalThis = sandbox;
107
+ vm.createContext(sandbox);
108
+ vm.runInContext(TRAIL, sandbox, { filename: 'shell-trail.js' });
109
+ vm.runInContext(CRAFT, sandbox, { filename: 'craft-mode.js' });
110
+ vm.runInContext(SHELL, sandbox, { filename: 'shell.js' });
111
+ const shell = windowObj.OTBShell;
112
+ const craft = windowObj.OTBCraftMode;
113
+ const byId = (id) => parsed().flatMap((g) => g.items).find((a) => a.id === 'nav-' + id);
114
+ return {
115
+ craft, root,
116
+ signIn: (rank = 'archon') => shell.applyAccess({ authed: true, rank, permissions: perms, builder: { id: 7, rank } }),
117
+ // The tabs a reader can see: the gate let them through AND the mode kept them.
118
+ showing: () => parsed().flatMap((g) => g.items).filter((a) => !a.hidden && !a.hasAttribute('data-craft-off')).map((a) => a.id.slice(4)),
119
+ headShowing: (group) => {
120
+ const g = parsed().find((x) => x.head && x.head.dataset.group === group);
121
+ return !!g && !g.head.hidden && !g.head.hasAttribute('data-craft-off');
122
+ },
123
+ byId,
124
+ };
125
+ }
126
+
127
+ const CRAFTS = ['building', 'artistry', 'ideating', 'governing'];
128
+
129
+ test('THE ALWAYS-PRESENT LIST: one shared list, Builders first, inside every mode', () => {
130
+ const { craft } = boot();
131
+ assert.equal(craft.ALWAYS_TABS[0], 'roster', 'Builders (NAV id roster) heads the list (owner, 2026-10-01)');
132
+ for (const m of CRAFTS) {
133
+ const tabs = craft.tabsFor(m);
134
+ assert.ok(Array.isArray(tabs), `${m} has a named tab list`);
135
+ for (const id of craft.ALWAYS_TABS) assert.ok(tabs.includes(id), `${m} keeps ${id}`);
136
+ }
137
+ assert.match(CRAFT, /return own \? ALWAYS_TABS\.concat\(own\) : null;/, 'each list is ALWAYS_TABS plus its own, not a copy of it');
138
+ });
139
+
140
+ test('every tab a mode names is a real NAV item, and every NAV item lives in some mode', () => {
141
+ const { craft } = boot();
142
+ assert.ok(NAV_IDS.length >= 25, `read ${NAV_IDS.length} NAV ids from shell.js`);
143
+ const named = new Set(CRAFTS.flatMap((m) => craft.tabsFor(m)));
144
+ for (const id of named) assert.ok(NAV_IDS.includes(id), `${id} is a NAV id`);
145
+ for (const id of NAV_IDS) assert.ok(named.has(id), `${id} is reachable from some mode's nav`);
146
+ });
147
+
148
+ test('Government shows only in Governing, and Governing is the shortest list', () => {
149
+ const { craft } = boot();
150
+ assert.ok(GOV_IDS.length >= 5, `read ${GOV_IDS.length} Government ids`);
151
+ for (const m of CRAFTS) {
152
+ for (const id of GOV_IDS) assert.equal(craft.tabsFor(m).includes(id), m === 'governing', `${id} in ${m}`);
153
+ }
154
+ const lengths = CRAFTS.map((m) => craft.tabsFor(m).filter((id) => !GOV_IDS.includes(id)).length);
155
+ assert.equal(Math.min(...lengths), lengths[CRAFTS.indexOf('governing')], 'outside its own Government group, Governing shows the fewest tabs');
156
+ });
157
+
158
+ test('NO MODE AND LEGACY ARE TODAY\'S NAV: every tab the gate allows, nothing marked', () => {
159
+ const none = boot();
160
+ none.signIn();
161
+ const all = none.showing();
162
+ assert.deepEqual(all, NAV_IDS, 'an archon with no mode sees every tab');
163
+ const legacy = boot({ session: { 'hall-craft': 'legacy' } });
164
+ legacy.signIn();
165
+ assert.deepEqual(legacy.showing(), all, 'Legacy mode shows exactly the same');
166
+ assert.equal(legacy.craft.tabsFor('legacy'), null);
167
+ });
168
+
169
+ test('each mode shows its own tabs, and switching updates the nav live', () => {
170
+ const p = boot();
171
+ p.signIn();
172
+ for (const m of CRAFTS) {
173
+ p.craft.choose(m);
174
+ assert.equal(p.root.dataset.craft, m);
175
+ assert.deepEqual(new Set(p.showing()), new Set(p.craft.tabsFor(m)), `${m} shows exactly its list`);
176
+ assert.ok(p.showing().includes('roster'), `Builders stays in ${m}`);
177
+ assert.equal(p.headShowing('government'), m === 'governing', `the Government head shows only where its tabs do (${m})`);
178
+ }
179
+ p.craft.choose('legacy');
180
+ assert.deepEqual(p.showing(), NAV_IDS, 'back to Legacy, every tab returns without a reload');
181
+ });
182
+
183
+ test('a mode never shows a tab the gate hid, and a later gate pass keeps the mode', () => {
184
+ const p = boot({ session: { 'hall-craft': 'governing' } });
185
+ p.signIn('thetes');
186
+ assert.ok(!p.showing().includes('watch'), 'a thetes in Governing does not get the metic-floored Watch tab');
187
+ assert.ok(!p.headShowing('government'), 'nor its group head');
188
+ assert.deepEqual(p.showing(), ['blockers', 'board-room', 'roster'], 'what a thetes board member in Governing sees');
189
+ p.signIn('archon');
190
+ assert.ok(p.showing().includes('watch'), 'promoted, the gate lets it through and the mode keeps it');
191
+ assert.ok(p.headShowing('government'), 'and its group head comes back with it, which only a mode pass after the gate can know');
192
+ assert.ok(!p.showing().includes('work'), 'and the gate pass did not bring back a tab the mode left out');
193
+ });
194
+
195
+ test('shell.js applies the mode AFTER its gate loop, and style.css hides what the mode leaves out', () => {
196
+ const body = SHELL.slice(SHELL.indexOf('function applyAccess('));
197
+ const gate = body.indexOf('el.hidden = !ok;');
198
+ const mode = body.indexOf('CRAFT.applyNav()');
199
+ assert.ok(gate > 0 && mode > gate, 'applyNav runs after the gate loop settles `hidden`');
200
+ assert.match(read('style.css'), /#app-sidebar \[data-craft-off\] \{ display: none !important; \}/);
201
+ assert.doesNotMatch(CRAFT, /\.hidden\s*=/, 'the mode never writes `hidden`, which the gate owns');
202
+ });
@@ -635,7 +635,7 @@ try {
635
635
  assert.match(HTML, /<body data-page="tweak-editor"[^>]*data-fullscreen-switch/);
636
636
  assert.equal((HTML.match(/<h1\b/g) || []).length, 1);
637
637
  assert.ok(HTML.indexOf('tweak-editor-lib.js') < HTML.indexOf('tweak-editor.js?'), 'the lib loads first');
638
- assert.match(HTML, /shell\.js\?v=2026-10-02-craft/);
638
+ assert.match(HTML, /shell\.js\?v=2026-10-03-nav/);
639
639
  assert.equal(/fonts\.googleapis\.com\/css/.test(HTML), false);
640
640
  for (const id of ['tw-bar', 'tw-counter', 'tw-open', 'tw-open-more', 'tw-divider', 'tw-openwrap', 'tw-open-menu', 'tw-open-tab', 'tw-open-split', 'tw-frame', 'tw-frame-view', 'tw-split-close', 'tw-download', 'tw-upload', 'tw-file', 'tw-submit', 'tw-paper', 'tw-doc', 'tw-notice', 'tw-done', 'tw-take', 'tw-pick', 'tw-picks', 'tw-refused', 'tw-refused-title', 'tw-refused-list', 'tw-refused-next', 'tw-refused-download', 'tw-refused-close']) {
641
641
  assert.ok(new RegExp(`id="${id}"`).test(HTML), `#${id} is in the markup`);
@@ -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 = '8932abeda77f250a';
90
+ const SHELL_SHA = 'af1c61ca87767b44';
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);