@bongos/core 1.21.72 → 1.21.73

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 (51) hide show
  1. package/.bongos-core.json +55 -55
  2. package/docs/copy-inventory.md +22 -22
  3. package/docs/copy-registry.json +25 -25
  4. package/docs/module-api-changelog.md +2 -0
  5. package/docs/page-readings.json +324 -325
  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 +69 -1
  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 +9 -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/task.html +3 -3
  38. package/modules/hall-ui/public/thinking.html +3 -3
  39. package/modules/hall-ui/public/tweak-editor.html +3 -3
  40. package/modules/hall-ui/public/watch.html +3 -3
  41. package/modules/hall-ui/public/work.html +3 -3
  42. package/package-lock.json +2 -2
  43. package/package.json +1 -1
  44. package/release-notes.json +6 -0
  45. package/src/module-api.js +1 -1
  46. package/tests/craft_mode_nav.mjs +117 -1
  47. package/tests/hall_city.mjs +5 -3
  48. package/tests/hall_nav.mjs +3 -1
  49. package/tests/hall_street.mjs +2 -1
  50. package/tests/hall_tweak_editor.mjs +1 -1
  51. 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-04-lightbulb"></script>
175
- <script src="/builders/craft-mode.js?v=2026-10-04-lightbulb"></script>
176
- <script src="/builders/shell.js?v=2026-10-04-lightbulb"></script>
174
+ <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
175
+ <script src="/builders/craft-mode.js?v=2026-10-08-city-tab"></script>
176
+ <script src="/builders/shell.js?v=2026-10-08-city-tab"></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. -->
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.72",
3
+ "version": "1.21.73",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.72",
9
+ "version": "1.21.73",
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.72",
3
+ "version": "1.21.73",
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",
@@ -9242,5 +9242,11 @@
9242
9242
  "id": "1004578",
9243
9243
  "text": "While a new project is being set up, founders now get a little cloud-hopping game: each cloud is a public project on the platform, in its own planet colour. When the project goes live, clouds and stars fill the screen and fade"
9244
9244
  }
9245
+ ],
9246
+ "1.21.73": [
9247
+ {
9248
+ "id": "1004625",
9249
+ "text": "The governor city now has its own tab, and in Governing mode it leads the sidebar with every other governing page tucked into one dropdown under it."
9250
+ }
9245
9251
  ]
9246
9252
  }
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.72'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.73'; // 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');
@@ -187,7 +187,7 @@ test('a mode never shows a tab the gate hid, and a later gate pass keeps the mod
187
187
  p.signIn('thetes');
188
188
  assert.ok(!p.showing().includes('watch'), 'a thetes in Governing does not get the metic-floored Watch tab');
189
189
  assert.ok(!p.headShowing('government'), 'nor its group head');
190
- assert.deepEqual(p.showing(), ['blockers', 'board-room', 'profile', 'collab', 'roster'], 'what a thetes board member in Governing sees');
190
+ assert.deepEqual(p.showing(), ['blockers', 'board-room', 'city', 'profile', 'collab', 'roster'], 'what a thetes board member in Governing sees');
191
191
  p.signIn('archon');
192
192
  assert.ok(p.showing().includes('watch'), 'promoted, the gate lets it through and the mode keeps it');
193
193
  assert.ok(p.headShowing('government'), 'and its group head comes back with it, which only a mode pass after the gate can know');
@@ -202,3 +202,119 @@ test('shell.js applies the mode AFTER its gate loop, and style.css hides what th
202
202
  assert.match(read('style.css'), /#app-sidebar \[data-craft-off\] \{ display: none !important; \}/);
203
203
  assert.doesNotMatch(CRAFT, /\.hidden\s*=/, 'the mode never writes `hidden`, which the gate owns');
204
204
  });
205
+
206
+ // ---------- the Governing fold (owner, 2026-10-08) ----------
207
+ // Governing leads with the City and folds its other own tabs into one dropdown
208
+ // under it. The fold MOVES nodes, which the parsed sidebar above cannot model, so
209
+ // these cases run the real craft-mode.js against a small working tree: nodes with
210
+ // parents, insertBefore and comments, laid out as shell.js's NAV groups are.
211
+ function treeNode(tag, nodeType = 1) {
212
+ const attrs = {};
213
+ const n = {
214
+ tagName: tag, nodeType, id: '', className: '', dataset: {}, hidden: false, childNodes: [], parentNode: null, _html: '', type: '',
215
+ setAttribute(k, v) { attrs[k] = String(v); }, getAttribute(k) { return k in attrs ? attrs[k] : null; },
216
+ hasAttribute(k) { return k in attrs; }, removeAttribute(k) { delete attrs[k]; },
217
+ get classList() {
218
+ const self = this;
219
+ const list = () => self.className.split(/\s+/).filter(Boolean);
220
+ return {
221
+ contains: (c) => list().includes(c),
222
+ add: (c) => { if (!list().includes(c)) self.className = list().concat(c).join(' '); },
223
+ remove: (c) => { self.className = list().filter((x) => x !== c).join(' '); },
224
+ toggle(c, on) { if (on === undefined) on = !this.contains(c); if (on) this.add(c); else this.remove(c); return on; },
225
+ };
226
+ },
227
+ get nextSibling() { const p = this.parentNode; return p ? p.childNodes[p.childNodes.indexOf(this) + 1] || null : null; },
228
+ set innerHTML(v) { this._html = String(v); }, get innerHTML() { return this._html; },
229
+ appendChild(c) { return this.insertBefore(c, null); },
230
+ insertBefore(c, ref) {
231
+ if (c.parentNode) c.parentNode.removeChild(c);
232
+ const i = ref ? this.childNodes.indexOf(ref) : -1;
233
+ if (i === -1) this.childNodes.push(c); else this.childNodes.splice(i, 0, c);
234
+ c.parentNode = this;
235
+ return c;
236
+ },
237
+ removeChild(c) { this.childNodes.splice(this.childNodes.indexOf(c), 1); c.parentNode = null; return c; },
238
+ walk(fn) { for (const c of this.childNodes) { if (c.nodeType === 1) { fn(c); c.walk(fn); } } },
239
+ querySelectorAll(sel) { const out = []; this.walk((c) => { if (sel.startsWith('.') && c.classList.contains(sel.slice(1))) out.push(c); }); return out; },
240
+ querySelector(sel) { return this.querySelectorAll(sel)[0] || null; },
241
+ addEventListener() {}, focus() {},
242
+ };
243
+ return n;
244
+ }
245
+ function bootTree({ mode = null, activeId = null } = {}) {
246
+ const el = (tag, cls, id) => { const n = treeNode(tag); n.className = cls || ''; n.id = id || ''; return n; };
247
+ const sidebar = el('NAV', '', 'app-sidebar');
248
+ // shell.js's NAV, group by group: the unlabeled one holds its items directly,
249
+ // a labeled one a head and a .nav-items wrapper (renderSidebar's shape).
250
+ for (const block of SHELL.slice(SHELL.indexOf('var NAV = ['), SHELL.indexOf('// ---------- nav group collapse')).split('{ group: ').slice(1)) {
251
+ const label = /^'([^']*)'/.exec(block)[1];
252
+ const group = el('DIV', 'nav-group' + (label ? ' nav-group--collapsible' : ''));
253
+ let into = group;
254
+ if (label) {
255
+ const head = el('BUTTON', 'nav-group__head');
256
+ head.dataset.group = label.toLowerCase();
257
+ group.appendChild(head);
258
+ into = group.appendChild(el('DIV', 'nav-items', 'nav-items-' + label.toLowerCase()));
259
+ }
260
+ for (const m of block.matchAll(/\{ id: '([a-z-]+)', label:/g)) {
261
+ into.appendChild(el('A', 'nav-item' + (m[1] === activeId ? ' nav-item--active' : ''), 'nav-' + m[1]));
262
+ }
263
+ sidebar.appendChild(group);
264
+ }
265
+ const root = treeNode('HTML');
266
+ const byId = (id) => { if (sidebar.id === id) return sidebar; let hit = null; sidebar.walk((c) => { if (!hit && c.id === id) hit = c; }); return hit; };
267
+ const mk = (obj) => ({ getItem: (k) => (k in obj ? obj[k] : null), setItem: (k, v) => { obj[k] = String(v); }, removeItem: (k) => { delete obj[k]; } });
268
+ const documentObj = {
269
+ body: treeNode('BODY'), documentElement: root, activeElement: null,
270
+ getElementById: byId, querySelector: () => null, querySelectorAll: () => [],
271
+ createElement: (t) => treeNode(t.toUpperCase()), createComment: (t) => Object.assign(treeNode('#comment', 8), { data: t }),
272
+ addEventListener() {}, removeEventListener() {},
273
+ };
274
+ const windowObj = {
275
+ location: { search: '' }, matchMedia: () => ({ matches: false }),
276
+ localStorage: mk({}), sessionStorage: mk(mode ? { 'hall-craft': mode } : {}),
277
+ };
278
+ const sandbox = { window: windowObj, document: documentObj, console, CustomEvent: class { constructor(t, i) { this.type = t; Object.assign(this, i); } } };
279
+ sandbox.globalThis = sandbox;
280
+ vm.createContext(sandbox);
281
+ vm.runInContext(CRAFT, sandbox, { filename: 'craft-mode.js' });
282
+ const craft = windowObj.OTBCraftMode;
283
+ craft.mount();
284
+ const fold = () => byId('craft-fold');
285
+ const ids = (n) => n.querySelectorAll('.nav-item').map((a) => a.id.slice(4));
286
+ const order = () => ids(sidebar);
287
+ return { craft, sidebar, fold, ids, order, byId };
288
+ }
289
+
290
+ test('GOVERNING LEADS WITH THE CITY: its other tabs fold into one dropdown right under it', () => {
291
+ const before = bootTree().order();
292
+ const p = bootTree({ mode: 'governing' });
293
+ const fold = p.fold();
294
+ assert.ok(fold, 'the dropdown exists in Governing');
295
+ assert.equal(p.byId('nav-city').nextSibling, fold, 'directly under the City tab');
296
+ const own = p.craft.tabsFor('governing').filter((id) => !p.craft.ALWAYS_TABS.includes(id) && id !== 'city');
297
+ assert.deepEqual(JSON.parse(JSON.stringify(p.ids(fold))), JSON.parse(JSON.stringify(own)), 'every other Governing tab is in it, in the mode\'s order');
298
+ assert.ok(fold.classList.contains('nav-group--collapsed'), 'a dropdown starts closed');
299
+ const head = fold.querySelector('.nav-group__head');
300
+ assert.equal(head.getAttribute('aria-expanded'), 'false');
301
+ assert.equal(head.getAttribute('aria-controls'), 'nav-items-craft-fold', 'the head names the region it opens');
302
+ assert.match(head.innerHTML, /More governing/);
303
+ assert.ok(head.hasAttribute('data-craft-off') === false, 'its head shows while a tab inside does');
304
+ assert.ok(p.sidebar.querySelectorAll('.nav-group__head').find((h) => h.dataset.group === 'government').hasAttribute('data-craft-off'),
305
+ 'the emptied Government head hides, so no label sits over nothing');
306
+ p.craft.choose('building');
307
+ assert.equal(p.fold(), null, 'leaving Governing removes the dropdown');
308
+ assert.deepEqual(p.order(), before, 'and puts every tab back exactly where it was');
309
+ p.craft.choose('governing');
310
+ p.craft.choose('governing');
311
+ assert.equal(p.sidebar.querySelectorAll('.craft-fold').length, 1, 'choosing it again builds no second dropdown');
312
+ });
313
+
314
+ test('the dropdown opens on its own when the page you are on is inside it', () => {
315
+ const p = bootTree({ mode: 'governing', activeId: 'watch' });
316
+ assert.ok(!p.fold().classList.contains('nav-group--collapsed'), 'on Watch, the dropdown is open');
317
+ assert.equal(p.fold().querySelector('.nav-group__head').getAttribute('aria-expanded'), 'true');
318
+ assert.equal(bootTree({ mode: 'artistry' }).fold(), null, 'no other mode folds anything');
319
+ assert.equal(bootTree().fold(), null, 'nor does no mode');
320
+ });
@@ -271,8 +271,9 @@ test('the page holds only the wordmark, the switch, the city, one button and one
271
271
  });
272
272
 
273
273
  // task 1004544 (owner decision 2026-10-02): the city sits in the hall chrome
274
- // behind the shell's full-screen switch, the sky's mechanism (task 1004327),
275
- // and still has no sidebar link.
274
+ // behind the shell's full-screen switch, the sky's mechanism (task 1004327).
275
+ // The owner put the CITY on the sidebar on 2026-10-08 (the Governing mode's
276
+ // lead tab); the street still has no link of its own.
276
277
  test('the city sits in the hall chrome, behind the shell\'s full-screen switch', () => {
277
278
  const html = read('city.html').replace(/<!--[\s\S]*?-->/g, '');
278
279
  assert.match(html, /<body data-page="city"[^>]*\sdata-fullscreen-switch>/, 'the bare attribute: the page lays itself out for full screen');
@@ -285,7 +286,8 @@ test('the city sits in the hall chrome, behind the shell\'s full-screen switch',
285
286
  assert.match(css, /html\[data-fullscreen="on"\] \.city-main \{ top: 0; left: 0; \}/);
286
287
  assert.match(css, /html\[data-fullscreen="on"\] \.city-top \{ right: 184px; \}/, 'the strip steps clear of the corner exit');
287
288
  const shell = read('shell.js');
288
- assert.doesNotMatch(shell, /href:\s*'(\/builders)?\/(city|street)'/, 'no sidebar link (owner: leave it off the sidebar)');
289
+ assert.match(shell, /\{ id: 'city', label: 'City', icon: 'city', href: '\/city', gate: 'perm:board\.vote\.cast' \}/, 'the city is a tab, gated on its page\'s atom');
290
+ assert.doesNotMatch(shell, /href:\s*'(\/builders)?\/street'/, 'the street has no sidebar link: the way in is the city');
289
291
  });
290
292
 
291
293
  test('motion is CSS only and dies under reduced motion; no page :root; no innerHTML', () => {
@@ -267,7 +267,9 @@ test('sidebar groups read by audience, in order', () => {
267
267
  // purpose (task 1003479, and tests/goal_map_page.mjs pinned it); the owner
268
268
  // granted the sky that slot on 2026-09-24 (ADR 0227 §11), under Goals because
269
269
  // it is the goals seen as one picture.
270
- assert.deepEqual(nav[0].items.map((i) => i.id), ['home', 'roadmap', 'goals', 'goal-map', 'work', 'blockers', 'board-room', 'thinking', 'studio'],
270
+ // 2026-10-08: 'city' — the governor city — joins after the Board Room, on the
271
+ // same atom and for the same reason (owner call; it leads the Governing mode).
272
+ assert.deepEqual(nav[0].items.map((i) => i.id), ['home', 'roadmap', 'goals', 'goal-map', 'work', 'blockers', 'board-room', 'city', 'thinking', 'studio'],
271
273
  'the unlabeled group is the project itself (task 1002425 order)');
272
274
  assert.deepEqual(nav[1].items.map((i) => i.id), ['standing', 'profile'], 'You = your own numbers');
273
275
  // task 1003444: 'people' LEFT the group — the owner deleted the hall's
@@ -504,7 +504,8 @@ test('the street sits in the hall chrome, behind the shell\'s full-screen switch
504
504
  assert.match(css, /\.street-stage \{ position: fixed; top: var\(--topbar-h, 56px\); left: var\(--sidebar-w-rail, 64px\);[^}]*contain: layout paint;/);
505
505
  assert.match(css, /html\[data-fullscreen="on"\] \.street-stage \{ top: 0; left: 0; \}/);
506
506
  assert.match(css, /html\[data-fullscreen="on"\] \.street-top \{ padding-right: 184px; \}/, 'the strip steps clear of the corner exit');
507
- assert.doesNotMatch(read('shell.js'), /href:\s*'(\/builders)?\/(city|street)'/, 'no sidebar link (owner: leave it off the sidebar)');
507
+ // The city became a tab on 2026-10-08 (owner); the street is still reached through it.
508
+ assert.doesNotMatch(read('shell.js'), /href:\s*'(\/builders)?\/street'/, 'no sidebar link (owner: leave it off the sidebar)');
508
509
  });
509
510
 
510
511
  test('motion is CSS only and dies under reduced motion; no page :root; no innerHTML', () => {
@@ -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-04-lightbulb/);
638
+ assert.match(HTML, /shell\.js\?v=2026-10-08-city-tab/);
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 = '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);