@bongos/core 1.21.78 → 1.21.79

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 (69) hide show
  1. package/.bongos-core.json +95 -70
  2. package/.claude/skills/goal-create/SKILL.md +2 -1
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +8 -0
  5. package/clients/bongos-client/index.cjs +8 -0
  6. package/clients/bongos-client/index.d.ts +13 -0
  7. package/clients/bongos-client/index.mjs +8 -0
  8. package/docs/api/openapi.json +285 -3
  9. package/docs/api-reference.md +6 -2
  10. package/docs/copy-inventory.md +38 -31
  11. package/docs/copy-registry.json +114 -50
  12. package/docs/module-api-changelog.md +2 -0
  13. package/docs/page-inventory.json +4 -2
  14. package/docs/page-readings.json +215 -206
  15. package/migrations/core_273_module_install_reports.sql +66 -0
  16. package/modules/hall-ui/public/atlas.html +1 -1
  17. package/modules/hall-ui/public/blockers.html +1 -1
  18. package/modules/hall-ui/public/board-room.html +1 -1
  19. package/modules/hall-ui/public/city.html +1 -1
  20. package/modules/hall-ui/public/collab.html +1 -1
  21. package/modules/hall-ui/public/commands.html +1 -1
  22. package/modules/hall-ui/public/copy-desk.html +1 -1
  23. package/modules/hall-ui/public/deploy.html +1 -1
  24. package/modules/hall-ui/public/diagrams.html +1 -1
  25. package/modules/hall-ui/public/drachmae.html +1 -1
  26. package/modules/hall-ui/public/fleet.html +1 -1
  27. package/modules/hall-ui/public/gate.html +1 -1
  28. package/modules/hall-ui/public/goal-map.html +1 -1
  29. package/modules/hall-ui/public/goals.html +1 -1
  30. package/modules/hall-ui/public/government.html +1 -1
  31. package/modules/hall-ui/public/idea.html +1 -1
  32. package/modules/hall-ui/public/ideas.html +1 -1
  33. package/modules/hall-ui/public/index.html +1 -1
  34. package/modules/hall-ui/public/modules.html +1 -1
  35. package/modules/hall-ui/public/primer.html +1 -1
  36. package/modules/hall-ui/public/profile.html +1 -1
  37. package/modules/hall-ui/public/project-settings.html +1 -1
  38. package/modules/hall-ui/public/ranks.html +1 -1
  39. package/modules/hall-ui/public/roadmap.html +1 -1
  40. package/modules/hall-ui/public/roster.html +1 -1
  41. package/modules/hall-ui/public/sessions.html +1 -1
  42. package/modules/hall-ui/public/settings-usage-reports.js +96 -0
  43. package/modules/hall-ui/public/settings.html +19 -1
  44. package/modules/hall-ui/public/settings.states.json +1 -0
  45. package/modules/hall-ui/public/shell.js +61 -29
  46. package/modules/hall-ui/public/street.html +1 -1
  47. package/modules/hall-ui/public/studio.html +1 -1
  48. package/modules/hall-ui/public/task.html +1 -1
  49. package/modules/hall-ui/public/thinking.html +1 -1
  50. package/modules/hall-ui/public/tweak-editor.html +1 -1
  51. package/modules/hall-ui/public/watch.html +1 -1
  52. package/modules/hall-ui/public/work.html +1 -1
  53. package/package-lock.json +2 -2
  54. package/package.json +1 -1
  55. package/release-notes.json +18 -0
  56. package/scripts/gds/module-assess-installs.js +143 -0
  57. package/scripts/gds/module.js +29 -5
  58. package/scripts/hall-preview/live-identity.js +1 -0
  59. package/src/bongos/module-usage-reports.js +112 -0
  60. package/src/bongos/routes/modules.js +96 -0
  61. package/src/module-api.js +1 -1
  62. package/tests/goal_map_page.mjs +1 -1
  63. package/tests/hall_commands.mjs +1 -1
  64. package/tests/hall_nav.mjs +42 -0
  65. package/tests/hall_tweak_editor.mjs +1 -1
  66. package/tests/module_assess_installs.mjs +272 -0
  67. package/tests/module_install.mjs +3 -0
  68. package/tests/module_update.mjs +3 -0
  69. package/tests/nav_permission_atoms.mjs +1 -1
@@ -84,27 +84,59 @@
84
84
  }
85
85
 
86
86
  // Small stroke icon set (18px grid, currentColor) — hand-rolled, no framework.
87
+ //
88
+ // ONE GLYPH PER TAB (task 1004626). The rail is a 64px icon strip until it
89
+ // opens, so at rest the icon IS the tab's name — two tabs wearing one glyph
90
+ // are two tabs a builder cannot tell apart (nine Government tabs once shared a
91
+ // shield, all eight Docs tabs one page). Every NAV item names its own key here
92
+ // and no two items share one; tests/hall_nav.mjs fails on a repeat, on a key
93
+ // missing from this set, and on two keys drawing the same markup.
87
94
  var ICONS = {
95
+ // the project
88
96
  home: '<path d="M3 8.5 9 3l6 5.5V15a1 1 0 0 1-1 1h-3.5v-4h-3v4H4a1 1 0 0 1-1-1V8.5Z"/>',
89
- work: '<rect x="2.5" y="5.5" width="13" height="9" rx="1.5"/><path d="M6.5 5.5V4a1.5 1.5 0 0 1 1.5-1.5h2A1.5 1.5 0 0 1 11.5 4v1.5M2.5 9.5h13"/>',
90
- standing: '<circle cx="9" cy="6" r="3"/><path d="M3.5 15.5c.6-2.9 2.9-4.5 5.5-4.5s4.9 1.6 5.5 4.5"/>',
91
- profile: '<rect x="2.5" y="4" width="13" height="10" rx="1.5"/><circle cx="6.3" cy="8" r="1.6"/><path d="M4.1 12c.3-1.1 1.1-1.7 2.2-1.7s1.9.6 2.2 1.7"/><path d="M10.4 7.5h3M10.4 10.3h3"/>',
92
- goals: '<circle cx="9" cy="9" r="6.5"/><circle cx="9" cy="9" r="3.5"/><circle cx="9" cy="9" r="0.8" fill="currentColor"/>',
93
97
  roadmap: '<path d="M3 15.5V3.5l4 2 4-2 4 2v10l-4-2-4 2-4-2Z"/><path d="M7 3.5v10M11 5.5v10"/>',
94
- leaderboard: '<path d="M3.5 15.5v-6M9 15.5V3.5m5.5 12v-9"/>',
95
- docs: '<path d="M4 3.5h7l3 3v10H4v-13Z"/><path d="M11 3.5v3h3"/>',
98
+ goals: '<circle cx="9" cy="9" r="6.5"/><circle cx="9" cy="9" r="3.5"/><circle cx="9" cy="9" r="0.8" fill="currentColor"/>',
99
+ sky: '<circle cx="3.8" cy="12.5" r="1.3"/><circle cx="8.5" cy="5" r="1.3"/><circle cx="14.2" cy="8.5" r="1.3"/><circle cx="12" cy="14.8" r="1.3"/><path d="M4.5 11.4 7.8 6.1M9.6 5.7l3.5 2.1M13.7 9.8l-1.2 3.7"/>',
100
+ work: '<rect x="2.5" y="5.5" width="13" height="9" rx="1.5"/><path d="M6.5 5.5V4a1.5 1.5 0 0 1 1.5-1.5h2A1.5 1.5 0 0 1 11.5 4v1.5M2.5 9.5h13"/>',
101
+ blockers: '<path d="M6.3 2.5h5.4l3.8 3.8v5.4l-3.8 3.8H6.3l-3.8-3.8V6.3Z"/><path d="M6 9h6"/>',
102
+ boardroom: '<path d="M2.5 9h13v5.5a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1V9Z"/><path d="M5.5 9V2.5h7V9"/><path d="M7.3 5.8l1.2 1.2 2.2-2.3"/>',
103
+ thinking: '<path d="M9 2.5a4.5 4.5 0 0 0-2.7 8.1c.5 .4 .7 .9 .7 1.4v.5h4V12c0-.5.2-1 .7-1.4A4.5 4.5 0 0 0 9 2.5Z"/><path d="M7 15h4M7.8 16.8h2.4"/>',
96
104
  // task 1003117 (R06 of goal 1000074): the Studio (artist mode). A sprout —
97
105
  // the zen-garden reading of "a place an artist wants to be", so it does not
98
106
  // reuse a glyph that would read as one more docs/goals page.
99
107
  studio: '<path d="M9 15.5V8.5"/><path d="M9 8.5C9 5.5 6.5 3.5 3.5 3.5c0 3 2.5 5 5.5 5Z"/><path d="M9 10C9 7.5 11.2 5.5 14 5.5c0 2.7-2.2 4.5-5 4.5Z"/>',
108
+ // You
109
+ standing: '<circle cx="9" cy="6" r="3"/><path d="M3.5 15.5c.6-2.9 2.9-4.5 5.5-4.5s4.9 1.6 5.5 4.5"/>',
110
+ profile: '<rect x="2.5" y="4" width="13" height="10" rx="1.5"/><circle cx="6.3" cy="8" r="1.6"/><path d="M4.1 12c.3-1.1 1.1-1.7 2.2-1.7s1.9.6 2.2 1.7"/><path d="M10.4 7.5h3M10.4 10.3h3"/>',
111
+ // Community
112
+ collab: '<path d="M2.5 4a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v4.5a1 1 0 0 1-1 1H6.5L4 11.5v-2h-.5a1 1 0 0 1-1-1V4Z"/><path d="M11.5 6.5h3a1 1 0 0 1 1 1V12a1 1 0 0 1-1 1H14v2l-2.5-2H8.5a1 1 0 0 1-1-1V9.5"/>',
113
+ roster: '<circle cx="6.5" cy="6.3" r="2.3"/><path d="M2.5 15c.4-2.3 1.9-3.7 4-3.7s3.6 1.4 4 3.7"/><circle cx="12.5" cy="5.8" r="2"/><path d="M11.4 10.5c.4-.1.7-.2 1.1-.2 1.9 0 3.3 1.3 3.7 3.5"/>',
114
+ // Docs
115
+ primer: '<path d="M9 5C7.5 3.9 5.5 3.5 2.5 3.5v10c3 0 5 .4 6.5 1.5 1.5-1.1 3.5-1.5 6.5-1.5v-10c-3 0-5 .4-6.5 1.5Z"/><path d="M9 5v10"/>',
116
+ ranks: '<path d="M4 7.5 9 4l5 3.5M4 11 9 7.5l5 3.5M4 14.5 9 11l5 3.5"/>',
117
+ economy: '<circle cx="9" cy="9" r="6.5"/><path d="M11 6.7c-.4-.6-1.1-.9-2-.9-1.2 0-2 .6-2 1.5 0 2 4 1 4 3.1 0 .9-.9 1.5-2.1 1.5-.9 0-1.7-.4-2.1-1M9 4.7v1.1M9 12.2v1.1"/>',
118
+ commands: '<rect x="2.5" y="3.5" width="13" height="11" rx="1.5"/><path d="M5.5 7.3 7.7 9l-2.2 1.7M9.5 11.2h3"/>',
119
+ diagrams: '<rect x="2.5" y="2.5" width="5.5" height="4" rx="1"/><rect x="10" y="11.5" width="5.5" height="4" rx="1"/><path d="M5.2 6.5v4a1.5 1.5 0 0 0 1.5 1.5H10"/>',
120
+ atlas: '<circle cx="9" cy="9" r="6.5"/><path d="M2.5 9h13M9 2.5c1.7 1.8 2.6 4 2.6 6.5s-.9 4.7-2.6 6.5c-1.7-1.8-2.6-4-2.6-6.5S7.3 4.3 9 2.5Z"/>',
121
+ modules: '<rect x="2.5" y="2.5" width="5.5" height="5.5" rx="1"/><rect x="10" y="2.5" width="5.5" height="5.5" rx="1"/><rect x="2.5" y="10" width="5.5" height="5.5" rx="1"/><path d="M12.75 10.5v4.5M10.5 12.75h4.5"/>',
122
+ apidocs: '<path d="M6.5 3C5 3 4.5 3.7 4.5 5v2c0 1-.6 2-1.5 2 .9 0 1.5 1 1.5 2v2c0 1.3.5 2 2 2M11.5 3c1.5 0 2 .7 2 2v2c0 1 .6 2 1.5 2-.9 0-1.5 1-1.5 2v2c0 1.3-.5 2-2 2"/>',
123
+ // Government
124
+ watch: '<path d="M1.8 9S4.5 4 9 4s7.2 5 7.2 5-2.7 5-7.2 5S1.8 9 1.8 9Z"/><circle cx="9" cy="9" r="2.2"/>',
125
+ gate: '<path d="M9 2.5 15 5v4c0 3.5-2.4 6-6 7-3.6-1-6-3.5-6-7V5l6-2.5Z"/><path d="M6.5 9l1.8 1.8 3.4-3.6"/>',
126
+ sessions: '<path d="M7 5h8M7 9h8M7 13h8"/><circle cx="3.8" cy="5" r=".7" fill="currentColor"/><circle cx="3.8" cy="9" r=".7" fill="currentColor"/><circle cx="3.8" cy="13" r=".7" fill="currentColor"/>',
100
127
  settings: '<circle cx="9" cy="9" r="2.5"/><path d="M9 2.5v2m0 9v2M2.5 9h2m9 0h2M4.4 4.4l1.4 1.4m6.4 6.4 1.4 1.4m0-9.2-1.4 1.4M5.8 12.2l-1.4 1.4"/>',
101
- admin: '<path d="M9 2.5 15 5v4c0 3.5-2.4 6-6 7-3.6-1-6-3.5-6-7V5l6-2.5Z"/>',
128
+ deploy: '<path d="M9 12V3M5.5 6.5 9 3l3.5 3.5M3 12.5v2a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-2"/>',
129
+ fleet: '<path d="M4 2.5h10v13l-1.7-1-1.6 1-1.7-1-1.7 1-1.6-1-1.7 1v-13Z"/><path d="M6.5 6h5M6.5 9h5M6.5 12h3"/>',
130
+ permissions: '<circle cx="6" cy="12" r="3.5"/><path d="M8.5 9.5 15 3M12.5 5.5l2 2M10.5 7.5l1.5 1.5"/>',
102
131
  // The governor city: a pediment over columns, the Governing mode's own glyph.
103
132
  city: '<path d="M2.5 7 9 3l6.5 4M4 7.5v6.5M7 7.5v6.5M11 7.5v6.5M14 7.5v6.5M2.5 15.5h13"/>',
133
+ // the foot
104
134
  signout: '<path d="M7 3.5H4a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h3M12 12l3-3-3-3M15 9H7"/>',
105
135
  };
136
+ // An unknown name draws an EMPTY frame rather than borrowing another tab's
137
+ // glyph: a silent fallback is how a typo becomes a duplicate icon.
106
138
  function icon(name) {
107
- return '<svg class="nav-icon" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + (ICONS[name] || ICONS.docs) + '</svg>';
139
+ return '<svg class="nav-icon" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' + (ICONS[name] || '') + '</svg>';
108
140
  }
109
141
  function chevronIcon() {
110
142
  return '<svg class="nav-group__chevron" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 7l4 4 4-4"/></svg>';
@@ -166,7 +198,7 @@
166
198
  // (requireBuilderPage, the tier the map sat on). The id stays 'goal-map'
167
199
  // because the href does: Goals' own "see which goals wait on each other"
168
200
  // link and every bookmark land here unchanged.
169
- { id: 'goal-map', label: 'Sky', icon: 'goals', href: '/goal-map', gate: 'member' },
201
+ { id: 'goal-map', label: 'Sky', icon: 'sky', href: '/goal-map', gate: 'member' },
170
202
  // task 1966: Tasks (the board formerly labelled "Work") is visible to ANY
171
203
  // signed-in builder (incl. newcomer Xenos). Claimability is controlled
172
204
  // per-task, not by hiding the page — every row carries a rank tag and the
@@ -185,7 +217,7 @@
185
217
  // page can still gate cosmetically wider in the sidebar than the API it
186
218
  // fronts, showing the link to signed-in builders while a stranger who
187
219
  // already has the URL can still open it.
188
- { id: 'blockers', label: 'Blockers', icon: 'admin', href: '/blockers', gate: 'signed' },
220
+ { id: 'blockers', label: 'Blockers', icon: 'blockers', href: '/blockers', gate: 'signed' },
189
221
  // task 1003744 (goal 1000111): the Board Room lives HERE, in the project's
190
222
  // own group, and gates on the atom its page gates on.
191
223
  //
@@ -211,7 +243,7 @@
211
243
  // /me (boardVotesNeed), pre-filtered to what the vote route would accept —
212
244
  // so the badge counts ballots this viewer can actually cast, never "items
213
245
  // open somewhere".
214
- { id: 'board-room', label: 'Board Room', icon: 'admin', href: '/board-room', gate: 'perm:board.vote.cast', count: 'board_votes' },
246
+ { id: 'board-room', label: 'Board Room', icon: 'boardroom', href: '/board-room', gate: 'perm:board.vote.cast', count: 'board_votes' },
215
247
  // The governor city (task 1004524): the owner put it on the sidebar on
216
248
  // 2026-10-08, reversing "leave it off". It is the Governing mode's lead tab
217
249
  // (craft-mode.js folds that mode's other tabs into a dropdown under it).
@@ -239,7 +271,7 @@
239
271
  // that caused it. gate 'signed' — there is no permission gate on having a
240
272
  // thought, and the route reads the VIEWER's own trail, so there is
241
273
  // nothing here a rank could protect.
242
- { id: 'thinking', label: 'Your thinking', icon: 'goals', href: '/thinking', gate: 'signed' },
274
+ { id: 'thinking', label: 'Your thinking', icon: 'thinking', href: '/thinking', gate: 'signed' },
243
275
  // task 1003117 (R06 of goal 1000074): the Studio — artist mode, "a place an
244
276
  // artist wants to be, not a form". Here and not in Docs (a place you ACT,
245
277
  // not reference) or You (its subject is the product, not a person).
@@ -286,12 +318,12 @@
286
318
  // other builders reaching you — not your own numbers (that is You).
287
319
  // task 1003942: the second consumer of the count badge. `help_requests` is
288
320
  // the need id builder-needs.js emits with a `count`; applyCounts reads it.
289
- { id: 'collab', label: 'Collab', icon: 'leaderboard', href: '/collab', gate: 'signed', count: 'help_requests' },
321
+ { id: 'collab', label: 'Collab', icon: 'collab', href: '/collab', gate: 'signed', count: 'help_requests' },
290
322
  // task 1003441: was the #leaderboard hash view on the home page, which is
291
323
  // why this group read as one real page plus a link into index.html. Now a
292
324
  // page, so no `view` key. Labelled Builders — 'roster' is the word
293
325
  // modules/hall-ui/CLAUDE.md uses, but Builders is what a reader scans for.
294
- { id: 'roster', label: 'Builders', icon: 'leaderboard', href: '/roster', gate: 'signed' },
326
+ { id: 'roster', label: 'Builders', icon: 'roster', href: '/roster', gate: 'signed' },
295
327
  // task 1003444: 'People', the cross-project directory, was deleted (owner
296
328
  // call 2026-09-25, ADR 0337 D5). Cross-project discovery is the hub's, and
297
329
  // /people redirects to /roster.
@@ -302,8 +334,8 @@
302
334
  // are for the first-session newcomer above all — so the links follow it
303
335
  // down to 'signed'. Leaving them at 'member' would have ungated the pages
304
336
  // while still hiding them from the only readers the change was for.
305
- { id: 'primer', label: 'Primer', icon: 'docs', href: '/primer', gate: 'signed' },
306
- { id: 'ranks', label: 'Ranks', icon: 'docs', href: '/ranks', gate: 'signed' },
337
+ { id: 'primer', label: 'Primer', icon: 'primer', href: '/primer', gate: 'signed' },
338
+ { id: 'ranks', label: 'Ranks', icon: 'ranks', href: '/ranks', gate: 'signed' },
307
339
  // task 2175: /drachmae had no nav entry at all — reachable only from prose
308
340
  // links since task 2174 moved the bounty rate card onto it. It is reference
309
341
  // material (the reward schedule + how karma differs), so it belongs in Docs,
@@ -311,16 +343,16 @@
311
343
  // currency: the page covers karma too, which is not currency, and "Economy"
312
344
  // stays right whatever `branding.currency.label` is set to. Page is public
313
345
  // like /ranks — 'signed' is the same cosmetic "show it once you're in".
314
- { id: 'economy', label: 'Economy', icon: 'docs', href: '/drachmae', gate: 'signed' },
346
+ { id: 'economy', label: 'Economy', icon: 'economy', href: '/drachmae', gate: 'signed' },
315
347
  // task 1004473: every skill and every bongos CLI verb, read live. Its page and
316
348
  // both reads behind it are sign-in gated, so 'signed' is the real floor here.
317
- { id: 'commands', label: 'Commands', icon: 'docs', href: '/commands', gate: 'signed' },
318
- { id: 'diagrams', label: 'Diagrams', icon: 'docs', href: '/diagrams', gate: 'signed' },
319
- { id: 'atlas', label: 'Repo Atlas', icon: 'docs', href: '/atlas', gate: 'member' },
320
- { id: 'modules', label: 'Modules', icon: 'docs', href: '/modules', gate: 'member' },
349
+ { id: 'commands', label: 'Commands', icon: 'commands', href: '/commands', gate: 'signed' },
350
+ { id: 'diagrams', label: 'Diagrams', icon: 'diagrams', href: '/diagrams', gate: 'signed' },
351
+ { id: 'atlas', label: 'Repo Atlas', icon: 'atlas', href: '/atlas', gate: 'member' },
352
+ { id: 'modules', label: 'Modules', icon: 'modules', href: '/modules', gate: 'member' },
321
353
  // The rendered OpenAPI reference lives on the APEX (/docs), not this builders
322
354
  // subdomain — external so the rewrite shim doesn't turn it into /builders/docs.
323
- { id: 'apidocs', label: 'API docs', icon: 'docs', href: '/docs', gate: 'signed', external: true },
355
+ { id: 'apidocs', label: 'API docs', icon: 'apidocs', href: '/docs', gate: 'signed', external: true },
324
356
  ] },
325
357
  // Relabeled 'Archon' → 'Governance' in ADR 0157, then → 'Government' by ADR 0174: these pages are no longer
326
358
  // Archon-only, so a group header reading "Archon" over items a Metic can open
@@ -331,12 +363,12 @@
331
363
  // xenos floor) rather than this group's rank, and a group whose audience is
332
364
  // Metic+ oversight could not honestly hold it. Everything below IS a rank
333
365
  // surface, so this group's gate and audience are unchanged.
334
- { id: 'watch', label: 'Watch', icon: 'admin', href: '/watch', gate: 'government' },
335
- { id: 'gate', label: 'Gate', icon: 'admin', href: '/gate', gate: 'government' },
336
- { id: 'sessions', label: 'Sessions', icon: 'admin', href: '/sessions', gate: 'government' },
366
+ { id: 'watch', label: 'Watch', icon: 'watch', href: '/watch', gate: 'government' },
367
+ { id: 'gate', label: 'Gate', icon: 'gate', href: '/gate', gate: 'government' },
368
+ { id: 'sessions', label: 'Sessions', icon: 'sessions', href: '/sessions', gate: 'government' },
337
369
  // The project's own fundamentals (task 1003280, goal 1000072). Mirrors the
338
370
  // server's requireGovernmentPage floor; cosmetic only, as every item here is.
339
- { id: 'project-settings', label: 'Project settings', icon: 'admin', href: '/project-settings', gate: 'government' },
371
+ { id: 'project-settings', label: 'Project settings', icon: 'settings', href: '/project-settings', gate: 'government' },
340
372
  // The deploy door (task 1003159, ADR 0293). Gated 'archon' here to mirror the
341
373
  // `core.pin.move` atom its page and both its routes check — cosmetic only, as
342
374
  // every item in this list is; the wall is server-side.
@@ -346,18 +378,18 @@
346
378
  // dropped where the module is off — and since task 1004296 it STAYS dropped: /deploy
347
379
  // is the platform hall's page alone, and every other hall takes its update from
348
380
  // Settings → Software update, whose Update button carries the hub hand-off.
349
- { id: 'deploy', label: 'Deploy', icon: 'admin', href: '/deploy', gate: 'archon', module: 'provisioning render-deploy' },
381
+ { id: 'deploy', label: 'Deploy', icon: 'deploy', href: '/deploy', gate: 'archon', module: 'provisioning render-deploy' },
350
382
  // task 1001949: what each hosted project costs. 'government' mirrors the metic floor
351
383
  // of `provisioning.fleet.manage`, the atom its page and its one read check (the
352
384
  // Deploy precedent above); dropped where the provisioning module is not enabled.
353
- { id: 'fleet', label: 'Fleet costs', icon: 'admin', href: '/fleet', gate: 'government', module: 'provisioning' },
385
+ { id: 'fleet', label: 'Fleet costs', icon: 'fleet', href: '/fleet', gate: 'government', module: 'provisioning' },
354
386
  // BV1.R113: the roles-and-permissions tab (ADR 0151). The one genuinely
355
387
  // Archon-only item the 'government' gate comment anticipated — cosmetic
356
388
  // only. The wall is the `government.manage` atom checked on each of the
357
389
  // tab's nine data routes (route-rank-check.js); the page SHELL itself is
358
390
  // metic+, because R16 made /government three rooms and only this one is
359
391
  // the archon surface.
360
- { id: 'government', label: 'Permissions', icon: 'admin', href: '/government', gate: 'archon' } // NOT 'Ranks': /ranks is the signed-in ladder explainer and two nav items
392
+ { id: 'government', label: 'Permissions', icon: 'permissions', href: '/government', gate: 'archon' } // NOT 'Ranks': /ranks is the signed-in ladder explainer and two nav items
361
393
  // reading 'Ranks' is the confusion this rename exists to remove. This tab
362
394
  // is where a rank's permissions are granted, so it says what it does.,
363
395
  ] },
@@ -205,7 +205,7 @@
205
205
 
206
206
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
207
207
  <script src="/builders/craft-mode.js?v=2026-10-08-city-tab"></script>
208
- <script src="/builders/shell.js?v=2026-10-08-city-tab"></script>
208
+ <script src="/builders/shell.js?v=2026-10-08-glyph-fix"></script>
209
209
  <script src="/builders/palette.js" defer></script>
210
210
  <script src="/builders/city-draw.js?v=2026-10-04-pins1"></script>
211
211
  <script src="/builders/street-draw.js?v=2026-10-04-pins1"></script>
@@ -184,7 +184,7 @@
184
184
 
185
185
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
186
186
  <script src="/builders/craft-mode.js?v=2026-10-08-city-tab"></script>
187
- <script src="/builders/shell.js?v=2026-10-08-city-tab"></script>
187
+ <script src="/builders/shell.js?v=2026-10-08-glyph-fix"></script>
188
188
  <script src="/builders/palette.js" defer></script>
189
189
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
190
190
  <!-- The kit, for its ONE renderer: the full-size ship-time visual figure
@@ -56,7 +56,7 @@
56
56
  time so the chrome exists before the deferred page scripts execute. -->
57
57
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
58
58
  <script src="/builders/craft-mode.js?v=2026-10-08-city-tab"></script>
59
- <script src="/builders/shell.js?v=2026-10-08-city-tab"></script>
59
+ <script src="/builders/shell.js?v=2026-10-08-glyph-fix"></script>
60
60
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
61
61
  <script src="/builders/palette.js" defer></script>
62
62
  <!-- shared frontend toolkit (window.OTB) — loaded FIRST so task.js can pull from it. -->
@@ -140,7 +140,7 @@
140
140
 
141
141
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
142
142
  <script src="/builders/craft-mode.js?v=2026-10-08-city-tab"></script>
143
- <script src="/builders/shell.js?v=2026-10-08-city-tab"></script>
143
+ <script src="/builders/shell.js?v=2026-10-08-glyph-fix"></script>
144
144
  <script src="/builders/palette.js" defer></script>
145
145
 
146
146
  <script src="/builders/idea-objects.js?v=2026-09-27-split"></script>
@@ -144,7 +144,7 @@
144
144
 
145
145
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
146
146
  <script src="/builders/craft-mode.js?v=2026-10-08-city-tab"></script>
147
- <script src="/builders/shell.js?v=2026-10-08-city-tab"></script>
147
+ <script src="/builders/shell.js?v=2026-10-08-glyph-fix"></script>
148
148
  <script src="/builders/palette.js" defer></script>
149
149
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
150
150
  <script src="/builders/brand-holes.js?v=2026-09-29-aqfix"></script>
@@ -167,7 +167,7 @@
167
167
  chrome exists before the page script runs. -->
168
168
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
169
169
  <script src="/builders/craft-mode.js?v=2026-10-08-city-tab"></script>
170
- <script src="/builders/shell.js?v=2026-10-08-city-tab"></script>
170
+ <script src="/builders/shell.js?v=2026-10-08-glyph-fix"></script>
171
171
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
172
172
  <script src="/builders/palette.js" defer></script>
173
173
  <!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
@@ -173,7 +173,7 @@
173
173
  time so the chrome exists before the deferred page scripts execute. -->
174
174
  <script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
175
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>
176
+ <script src="/builders/shell.js?v=2026-10-08-glyph-fix"></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.78",
3
+ "version": "1.21.79",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.78",
9
+ "version": "1.21.79",
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.78",
3
+ "version": "1.21.79",
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",
@@ -9278,5 +9278,23 @@
9278
9278
  "id": "1004587",
9279
9279
  "text": "Goal pages now have a Plan with Claude button that copies a ready-made planning prompt."
9280
9280
  }
9281
+ ],
9282
+ "1.21.79": [
9283
+ {
9284
+ "id": "1004618",
9285
+ "text": "Dry-ran the hall-goal spec flow end to end; it works. The skill now documents the two rough edges found."
9286
+ },
9287
+ {
9288
+ "id": "1004639",
9289
+ "text": "The hall's new sidebar icons were stuck: one picture's drawing contained a run of numbers that looked like an internet address, so the release safety scan refused to package the update. The picture is redrawn identically witho"
9290
+ },
9291
+ {
9292
+ "id": "1003793",
9293
+ "text": "Store modules now get an Install score from real installs: each install or update quietly reports whether it worked (just yes/no and a short reason, nothing personal). Module authors can't pad their own score, and anyone can s"
9294
+ },
9295
+ {
9296
+ "id": "1004626",
9297
+ "text": "Every tab in the hall sidebar now has its own icon. Before, nineteen tabs shared just four pictures - all nine Government pages wore the same shield and every Docs page the same sheet of paper - so the collapsed sidebar was ha"
9298
+ }
9281
9299
  ]
9282
9300
  }
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/module-assess-installs.js — the Install and Reliability parts of a
3
+ // module's assessment, worked out from what real installs report (task 1003793,
4
+ // ADR 0343 D1; reports from core_273, signal rows into core_265).
5
+ //
6
+ // WHY these two parts. Tests and Docs are things an author writes; Install and
7
+ // Reliability are what happens when OTHER people run the module, which is the
8
+ // one signal an author cannot fake. So two rules keep it honest:
9
+ // - the module's own author is never counted, and
10
+ // - each holder counts once per version for Install (core_273 keeps only their
11
+ // latest attempt), so one account retrying cannot outweigh many — and neither part is scored at
12
+ // all until MIN_HOLDERS different holders have reported (`pending` before,
13
+ // never a number resting on one friend's install).
14
+ //
15
+ // Install = round(100 × holders whose latest install worked / holders).
16
+ // Reliability = round(100 × holder-days with zero runtime errors / holder-days
17
+ // reported), over the last ERROR_DAY_WINDOW days.
18
+ // No reports → no_data (ADR 0343 D3: left out of the average, never a zero).
19
+ //
20
+ // What the reports may contain is fixed by the owner's privacy posture and
21
+ // enforced at intake (src/bongos/module-usage-reports.js): counts and short codes
22
+ // only, and nothing from a holder who switched reporting off.
23
+ //
24
+ // A new signal row is appended only when a part's result actually moves, so the
25
+ // append-only history (core_265) is not one row per report.
26
+ //
27
+ // node scripts/gds/module-assess-installs.js <store_module_versions.id> re-score + print
28
+
29
+ const { ERROR_DAY_WINDOW } = require('../../src/bongos/module-usage-reports');
30
+
31
+ const MIN_HOLDERS = 3;
32
+
33
+ // The not-yet-scored outcomes, shared by both parts. Pure.
34
+ function notYet(holders, what) {
35
+ if (!holders) return { outcome: 'no_data', score: null, sample_size: 0, reason: `no ${what} reported yet` };
36
+ return {
37
+ outcome: 'pending', score: null, sample_size: holders,
38
+ reason: `${holders} of the ${MIN_HOLDERS} installs needed have reported ${what}`,
39
+ };
40
+ }
41
+
42
+ // Install from each holder's LATEST attempt ({ ok, error_code } per holder). Pure.
43
+ function assessInstall(latest = []) {
44
+ const n = latest.length;
45
+ const failedCodes = {};
46
+ for (const r of latest) if (!r.ok) failedCodes[r.error_code] = (failedCodes[r.error_code] || 0) + 1;
47
+ const worked = n - Object.values(failedCodes).reduce((a, b) => a + b, 0);
48
+ const detail = { holders: n, worked, failed: n - worked, failed_codes: failedCodes };
49
+ if (n < MIN_HOLDERS) return { ...notYet(n, 'an install'), detail };
50
+ return {
51
+ outcome: 'scored', score: Math.round((100 * worked) / n), sample_size: n,
52
+ reason: `${worked} of ${n} installs worked`, detail,
53
+ };
54
+ }
55
+
56
+ // Reliability from daily counts ({ holder_kind, holder_ref, error_count } per
57
+ // holder-day). sample_size is distinct holders, the same unit as Install. Pure.
58
+ function assessReliability(days = []) {
59
+ const holders = new Set(days.map((d) => `${d.holder_kind}:${d.holder_ref}`)).size;
60
+ const clean = days.filter((d) => Number(d.error_count) === 0).length;
61
+ const errors = days.reduce((sum, d) => sum + Number(d.error_count), 0);
62
+ const detail = { holders, days: days.length, clean_days: clean, errors, window_days: ERROR_DAY_WINDOW };
63
+ if (holders < MIN_HOLDERS) return { ...notYet(holders, 'runtime errors'), detail };
64
+ return {
65
+ outcome: 'scored', score: Math.round((100 * clean) / days.length), sample_size: holders,
66
+ reason: `${clean} of ${days.length} days ran with no errors, across ${holders} installs`, detail,
67
+ };
68
+ }
69
+
70
+ // The author's own reports never count. Shared by both reads.
71
+ const NOT_AUTHOR = `NOT (r.holder_kind = 'builder' AND r.holder_ref = m.author_id::text)`;
72
+
73
+ async function readReports(versionId, pool) {
74
+ const { rows: latest } = await pool.query(
75
+ `SELECT r.ok, r.error_code
76
+ FROM module_install_reports r
77
+ JOIN store_module_versions v ON v.id = r.version_id
78
+ JOIN store_modules m ON m.module_key = v.module_key
79
+ WHERE r.version_id = $1 AND ${NOT_AUTHOR}`, [versionId]);
80
+ const { rows: days } = await pool.query(
81
+ `SELECT r.holder_kind, r.holder_ref, r.error_count
82
+ FROM module_runtime_error_counts r
83
+ JOIN store_module_versions v ON v.id = r.version_id
84
+ JOIN store_modules m ON m.module_key = v.module_key
85
+ WHERE r.version_id = $1 AND r.day >= current_date - $2::int AND ${NOT_AUTHOR}`,
86
+ [versionId, ERROR_DAY_WINDOW]);
87
+ return { latest, days };
88
+ }
89
+
90
+ const sameResult = (cur, s) => !!cur && cur.outcome === s.outcome
91
+ && (cur.score == null ? null : Number(cur.score)) === s.score
92
+ && Number(cur.sample_size) === s.sample_size;
93
+
94
+ // Re-score Install and Reliability for one version from its reports, append a
95
+ // signal row for each part whose result moved, then re-compose the overall score
96
+ // (module-assess-score.js). Resolves { ok, written: [part…], score }.
97
+ async function recordInstallSignals(versionId, { db } = {}) {
98
+ const pool = db || require('../../src/bongos/pool').pool;
99
+ const { latest, days } = await readReports(versionId, pool);
100
+ const next = { install: assessInstall(latest), reliability: assessReliability(days) };
101
+
102
+ const { rows: current } = await pool.query(
103
+ `SELECT DISTINCT ON (part) part, outcome, score, sample_size
104
+ FROM module_assessment_signals WHERE version_id = $1 AND part IN ('install', 'reliability')
105
+ ORDER BY part, measured_at DESC, id DESC`, [versionId]);
106
+ const byPart = Object.fromEntries(current.map((r) => [r.part, r]));
107
+
108
+ const written = [];
109
+ for (const part of ['install', 'reliability']) {
110
+ const s = next[part];
111
+ if (sameResult(byPart[part], s)) continue;
112
+ await pool.query(
113
+ `INSERT INTO module_assessment_signals (version_id, part, outcome, score, sample_size, reason, detail)
114
+ VALUES ($1, $2, $3, $4, $5, $6, $7)`,
115
+ [versionId, part, s.outcome, s.score, s.sample_size, s.reason, JSON.stringify(s.detail)]);
116
+ written.push(part);
117
+ }
118
+ const { score } = written.length
119
+ ? await require('./module-assess-score').recomposeScore(versionId, { db: pool })
120
+ : { score: null };
121
+ return { ok: true, written, signals: next, score };
122
+ }
123
+
124
+ async function main(argv) {
125
+ const id = argv[0];
126
+ if (!/^\d+$/.test(String(id || ''))) {
127
+ console.error('usage: node scripts/gds/module-assess-installs.js <store_module_versions.id>');
128
+ return 2;
129
+ }
130
+ const res = await recordInstallSignals(id);
131
+ for (const part of ['install', 'reliability']) {
132
+ const s = res.signals[part];
133
+ console.log(`${part}: ${s.outcome}${s.score != null ? ` ${s.score}/100` : ''} — ${s.reason}${res.written.includes(part) ? '' : ' (unchanged)'}`);
134
+ }
135
+ await require('./module-assess-score').printRecomposed(id);
136
+ return 0;
137
+ }
138
+
139
+ if (require.main === module) {
140
+ main(process.argv.slice(2)).then((code) => { process.exitCode = code; }, (e) => { console.error(e.stack || e.message); process.exitCode = 1; });
141
+ }
142
+
143
+ module.exports = { MIN_HOLDERS, assessInstall, assessReliability, recordInstallSignals };
@@ -1138,14 +1138,34 @@ async function fetchVerified(key, { wanted, call, core, log, errlog, verb, shoul
1138
1138
 
1139
1139
  const artifact = require('./module-artifact');
1140
1140
  const verified = await artifact.verifyModuleArtifact(dl.buf, { key, includeFiles: true });
1141
- if (!verified.ok) return fail(1, `REFUSING: the downloaded tarball failed verification (${verified.code}): ${verified.message}`);
1141
+ if (!verified.ok) {
1142
+ await reportInstall(call, key, described.version, 'verify_failed');
1143
+ return fail(1, `REFUSING: the downloaded tarball failed verification (${verified.code}): ${verified.message}`);
1144
+ }
1142
1145
  if (verified.version !== described.version || verified.tarballSha256 !== described.tarball_sha256 || verified.treeSha256 !== described.tree_sha256) {
1146
+ await reportInstall(call, key, described.version, 'hash_mismatch');
1143
1147
  return fail(1, `REFUSING: the download is not the ${described.version} the store described (version or hash differs).`);
1144
1148
  }
1145
1149
  log(` ✓ verified ${verified.files.length} file(s), ${verified.bytes} bytes — tree ${verified.treeSha256.slice(0, 12)}…`);
1146
1150
  return { ok: true, described, verified };
1147
1151
  }
1148
1152
 
1153
+ // Tell the store whether an install or update of this exact version worked (task
1154
+ // 1003793): it feeds the module's Install score, the part an author cannot fake.
1155
+ // Counts only — yes, or a short code saying where it failed; never a message, a
1156
+ // path or anything about this machine (owner's privacy posture, 2026-10-01). The
1157
+ // store drops it if the caller switched reporting off in Settings, so there is no
1158
+ // client-side switch to keep in step. Best-effort and silent: a report that does
1159
+ // not arrive changes nothing about the install. Only failures that are the
1160
+ // module's own count: a refused or unreachable store, or a core the module does
1161
+ // not support, says nothing about the module and is not reported.
1162
+ async function reportInstall(call, key, version, errorCode = null) {
1163
+ const body = errorCode ? { ok: false, error_code: errorCode } : { ok: true };
1164
+ try {
1165
+ await call('POST', `/store/modules/${encodeURIComponent(key)}/versions/${encodeURIComponent(version)}/install-report`, { json: body });
1166
+ } catch { /* best-effort */ }
1167
+ }
1168
+
1149
1169
  // Tell the store which exact version was placed. A failure here never undoes the
1150
1170
  // placement: the files are good, and the next install or update records it.
1151
1171
  async function recordPlaced(key, version, { call, errlog, verb }) {
@@ -1190,10 +1210,12 @@ async function cmdInstall(args, {
1190
1210
  if (!got.ok) return got.code;
1191
1211
 
1192
1212
  try { placeModule(got.verified.files, target); }
1193
- catch (e) { errlog(`bongos module install: could not place the module — ${e.message}`); return 1; }
1213
+ catch (e) { await reportInstall(call, key, got.described.version, 'place_failed'); errlog(`bongos module install: could not place the module — ${e.message}`); return 1; }
1194
1214
  log(` ✓ placed at ${target}`);
1195
1215
 
1196
- if (!(await recordPlaced(key, got.described.version, { call, errlog, verb: 'install' }))) return 1;
1216
+ const recorded = await recordPlaced(key, got.described.version, { call, errlog, verb: 'install' });
1217
+ await reportInstall(call, key, got.described.version);
1218
+ if (!recorded) return 1;
1197
1219
  log(`\n✓ Installed "${key}" ${got.described.version}. It is not switched on: enable it from the hall's Modules tab when you want it.`);
1198
1220
  const howto = howtoLink(key, got.described.version, got.verified.files, call.apiBase);
1199
1221
  if (howto) log(` How to use it: ${howto}`);
@@ -1276,10 +1298,12 @@ async function cmdUpdate(args, {
1276
1298
  if (!got.ok) return got.code;
1277
1299
 
1278
1300
  try { replaceModule(got.verified.files, target); }
1279
- catch (e) { errlog(`bongos module update: could not replace the module, ${installed} is still in place — ${e.message}`); return 1; }
1301
+ catch (e) { await reportInstall(call, key, got.described.version, 'place_failed'); errlog(`bongos module update: could not replace the module, ${installed} is still in place — ${e.message}`); return 1; }
1280
1302
  log(` ✓ replaced ${installed} with ${got.described.version} at ${target}`);
1281
1303
 
1282
- if (!(await recordPlaced(key, got.described.version, { call, errlog, verb: 'update' }))) return 1;
1304
+ const recorded = await recordPlaced(key, got.described.version, { call, errlog, verb: 'update' });
1305
+ await reportInstall(call, key, got.described.version);
1306
+ if (!recorded) return 1;
1283
1307
  log(`\n✓ Updated "${key}" ${installed} → ${got.described.version}. Its on/off switch is unchanged.`);
1284
1308
  return 0;
1285
1309
  }
@@ -34,6 +34,7 @@ const ME_SCOPED_PATHS = [
34
34
  'me/sound-prefs',
35
35
  'me/event-sound-prefs',
36
36
  'me/art-key/own',
37
+ 'store/usage-reporting',
37
38
  'builders/:id/profile',
38
39
  'builders/:id/active-claims',
39
40
  'builders/:id/activity?section=tasks',