@bongos/core 1.21.78 → 1.21.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +108 -78
- package/.claude/skills/goal-create/SKILL.md +2 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +8 -0
- package/clients/bongos-client/index.cjs +8 -0
- package/clients/bongos-client/index.d.ts +13 -0
- package/clients/bongos-client/index.mjs +8 -0
- package/docs/adr/0155-adopt-private-repo-widen-oauth-scope.md +1 -0
- package/docs/adr/0370-a-repo-we-create-is-private.md +29 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +285 -3
- package/docs/api-reference.md +6 -2
- package/docs/copy-inventory.md +40 -33
- package/docs/copy-registry.json +125 -61
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +4 -2
- package/docs/page-readings.json +216 -207
- package/migrations/core_273_module_install_reports.sql +66 -0
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/city.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings-usage-reports.js +96 -0
- package/modules/hall-ui/public/settings.html +19 -1
- package/modules/hall-ui/public/settings.states.json +1 -0
- package/modules/hall-ui/public/shell.js +61 -29
- package/modules/hall-ui/public/street.html +1 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor-lib.js +22 -3
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/tweak-editor.md +1 -1
- package/modules/public-landing/public/projects.html +5 -5
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +28 -0
- package/scripts/gds/module-assess-installs.js +143 -0
- package/scripts/gds/module.js +29 -5
- package/scripts/hall-preview/live-identity.js +1 -0
- package/src/bongos/github-repos.js +7 -6
- package/src/bongos/module-usage-reports.js +112 -0
- package/src/bongos/routes/modules.js +96 -0
- package/src/module-api.js +1 -1
- package/tests/github_repos_helpers.mjs +5 -5
- package/tests/goal_map_page.mjs +1 -1
- package/tests/hall_commands.mjs +1 -1
- package/tests/hall_nav.mjs +42 -0
- package/tests/hall_tweak_editor.mjs +13 -1
- package/tests/module_assess_installs.mjs +272 -0
- package/tests/module_install.mjs +3 -0
- package/tests/module_update.mjs +3 -0
- 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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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] ||
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
306
|
-
{ id: 'ranks', label: 'Ranks', icon: '
|
|
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: '
|
|
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: '
|
|
318
|
-
{ id: 'diagrams', label: 'Diagrams', icon: '
|
|
319
|
-
{ id: 'atlas', label: 'Repo Atlas', icon: '
|
|
320
|
-
{ id: 'modules', label: 'Modules', icon: '
|
|
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: '
|
|
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: '
|
|
335
|
-
{ id: 'gate', label: 'Gate', icon: '
|
|
336
|
-
{ id: 'sessions', label: 'Sessions', icon: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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>
|
|
@@ -189,8 +189,27 @@
|
|
|
189
189
|
// another host than the editor's cannot be framed: a hall page can, a landing
|
|
190
190
|
// or status page cannot. For those the split view is a second window docked to
|
|
191
191
|
// the bottom half of the screen.
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
//
|
|
193
|
+
// The SIBLINGS (task 1004393): once the proxy answers the apex and status hosts
|
|
194
|
+
// with `frame-ancestors 'self' https://builders.<apex>` instead of SAMEORIGIN,
|
|
195
|
+
// those two pages may split in place too. Until then framing them shows the
|
|
196
|
+
// browser's refusal page, so FRAME_SIBLINGS stays false and flipping it is the
|
|
197
|
+
// step after the proxy change (the owner's steps are on task 1004393). Only the
|
|
198
|
+
// apex and status.<apex> are siblings, never another subdomain: the proxy keeps
|
|
199
|
+
// every other host unframeable, sign-in and settings included.
|
|
200
|
+
const FRAME_SIBLINGS = false;
|
|
201
|
+
function siblingOrigins(loc) {
|
|
202
|
+
const here = new URL(loc.href || loc.origin);
|
|
203
|
+
if (!/^builders\./.test(here.hostname)) return [];
|
|
204
|
+
const apex = here.host.replace(/^builders\./, '');
|
|
205
|
+
return [`${here.protocol}//${apex}`, `${here.protocol}//status.${apex}`];
|
|
206
|
+
}
|
|
207
|
+
function canFrame(url, loc, siblings = FRAME_SIBLINGS) {
|
|
208
|
+
try {
|
|
209
|
+
const origin = new URL(url, loc.href || loc.origin).origin;
|
|
210
|
+
if (origin === (loc.origin || new URL(loc.href).origin)) return true;
|
|
211
|
+
return siblings === true && siblingOrigins(loc).includes(origin);
|
|
212
|
+
} catch (_) { return false; }
|
|
194
213
|
}
|
|
195
214
|
function splitWindowFeatures(scr) {
|
|
196
215
|
const s = scr || {};
|
|
@@ -229,7 +248,7 @@
|
|
|
229
248
|
return null;
|
|
230
249
|
}
|
|
231
250
|
|
|
232
|
-
const api = { OPEN_WORDS, openChoice, clampSplit, splitFromPointer, splitFromKey, canFrame, splitWindowFeatures, HINT, nextLineKey, confirmedCount, hintText, confirmStoreKey, parseConfirmed, NOTES, sectionsOf, isChanged, noteFor, draftBody, changedCount, counterText, initialValues, rowsFor, liveUrl, uploadedLabel, uploadRefusal, displayTitle, pageIdFrom };
|
|
251
|
+
const api = { OPEN_WORDS, openChoice, clampSplit, splitFromPointer, splitFromKey, canFrame, FRAME_SIBLINGS, splitWindowFeatures, HINT, nextLineKey, confirmedCount, hintText, confirmStoreKey, parseConfirmed, NOTES, sectionsOf, isChanged, noteFor, draftBody, changedCount, counterText, initialValues, rowsFor, liveUrl, uploadedLabel, uploadRefusal, displayTitle, pageIdFrom };
|
|
233
252
|
if (typeof window !== 'undefined') window.OTBTweakEditor = api;
|
|
234
253
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
235
254
|
})();
|
|
@@ -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-
|
|
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-
|
|
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-
|
|
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. -->
|
|
@@ -27,7 +27,7 @@ The paper is paper and the room is the room whatever the hall's theme: the stage
|
|
|
27
27
|
- **Boxes grow to their words** rather than the artboard's rows 1 or 2.
|
|
28
28
|
- **A refused upload opens a panel** (task 1004354, `#tw-refused`): the "is in" panel's glass and scrim over the whole window, a headline per refusal (`lib.uploadRefusal`), the server's words, the lines it names, "what to do", and "download this page again" or close (Escape too). An artist uploads from the bar, often far down the page, where the paper's notice was off screen. The notice stays on the paper after the panel closes. The kit has no file-upload action, so the panel is proven in `tests/hall_tweak_editor.mjs` and by shots of a real browser at 1440 and 390, not by a states.json state.
|
|
29
29
|
- **The check, the restore and Shift + Enter (task 1004385, owner-asked).** Beside each line a round restore button (only on a changed line: it puts the page's words back and takes the check off) and a round green check. A checked line takes a light green wash; the owner picked the check over a tint alone. **Shift + Enter** in a line checks it and lands in the next, and a green hint above the words says so (with "N of M checked"), because it is the quickest way through a page. Enter alone is unchanged. The checks are the artist's place-keeper and stay in the browser (`localStorage`, keyed by page and reading hash, so a page that moved never ticks the wrong words); the server has no such field and `WRITES` is unchanged. A page the caller cannot write has none of it. Rules in `tweak-editor-lib.js` (`nextLineKey`, `confirmedCount`, `parseConfirmed`, `confirmStoreKey`, `hintText`).
|
|
30
|
-
- **No page title or sub text at the top of the paper, and "open the page" is a menu (task 1004386, owner-asked).** The bar already names the page, so the paper opens on its first section; the h1 stays, visually hidden, for the one-h1 floor. "open the page" is a **split button** (owner-amended mid-build): the main half repeats the artist's last choice ("open the page ↗" for a new tab, "open in split view"), and the ▾ beside it opens **open in new tab** and **open in split view**. Click, keyboard and touch; hover alone opens nothing, because a hover menu closes when the pointer wanders and does not exist on touch. Split view has a **draggable divider** (mouse, touch, or the arrow keys and Home/End on the focused divider; the live page takes 20% to 80% of the window, `lib.clampSplit`), and the frame stops taking the pointer while it is dragged. The choice, the size and whether split view was open are remembered (`hall-tweak-open-as`, `hall-tweak-split-size`, `hall-tweak-split`), so the next page opens the same way; a split that would be a second window never reopens on load, because a browser blocks a popup nobody clicked. At phone width the live page takes at most half the screen, whatever the saved size, because the two-row bar is tall there. Split view pins the live page under the words (words above, page below, both scroll; "close split view" unloads it; the frame loads only when opened). **The live site answers `X-Frame-Options: SAMEORIGIN`** (set at the proxy in front of the app, not in this repo), so only a page on the editor's own host can be framed. A landing or status page is on another host, so its split view opens the page in a second window docked to the bottom half of the screen (`lib.canFrame`, `lib.splitWindowFeatures`).
|
|
30
|
+
- **No page title or sub text at the top of the paper, and "open the page" is a menu (task 1004386, owner-asked).** The bar already names the page, so the paper opens on its first section; the h1 stays, visually hidden, for the one-h1 floor. "open the page" is a **split button** (owner-amended mid-build): the main half repeats the artist's last choice ("open the page ↗" for a new tab, "open in split view"), and the ▾ beside it opens **open in new tab** and **open in split view**. Click, keyboard and touch; hover alone opens nothing, because a hover menu closes when the pointer wanders and does not exist on touch. Split view has a **draggable divider** (mouse, touch, or the arrow keys and Home/End on the focused divider; the live page takes 20% to 80% of the window, `lib.clampSplit`), and the frame stops taking the pointer while it is dragged. The choice, the size and whether split view was open are remembered (`hall-tweak-open-as`, `hall-tweak-split-size`, `hall-tweak-split`), so the next page opens the same way; a split that would be a second window never reopens on load, because a browser blocks a popup nobody clicked. At phone width the live page takes at most half the screen, whatever the saved size, because the two-row bar is tall there. Split view pins the live page under the words (words above, page below, both scroll; "close split view" unloads it; the frame loads only when opened). **The live site answers `X-Frame-Options: SAMEORIGIN`** (set at the proxy in front of the app, not in this repo), so only a page on the editor's own host can be framed. A landing or status page is on another host, so its split view opens the page in a second window docked to the bottom half of the screen (`lib.canFrame`, `lib.splitWindowFeatures`). **Letting them split in place (task 1004393)** is two steps, in this order. First the proxy answers the apex and `status.<apex>` with `<redacted>: frame-ancestors 'self' https://builders.<apex>` in place of `X-Frame-Options: SAMEORIGIN`, and every other host keeps SAMEORIGIN (sign-in and settings stay unframeable). Then `FRAME_SIBLINGS` in `tweak-editor-lib.js` flips to `true`. `lib.canFrame` then also admits exactly those two origins, same scheme and port, and only from the builders host; never another subdomain or a lookalike. Flipping it before the proxy changes would put the browser's refusal page in the split, which is why it ships off.
|
|
31
31
|
- **Not on the artboard at all:** the phone layout (the bar becomes a two-row glass card, notes drop under their lines), the shell's full-screen button top right, the read-only and refusal notices on the paper, the chrome view (the switch off), and light mode.
|
|
32
32
|
|
|
33
33
|
## The proof
|
|
@@ -1696,7 +1696,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1696
1696
|
<div class="choices" role="group" aria-label="Starting point">
|
|
1697
1697
|
<button type="button" class="choice" id="c-green" aria-pressed="true">
|
|
1698
1698
|
<b>A new repo</b>
|
|
1699
|
-
<p>
|
|
1699
|
+
<p>Private, set up for you</p>
|
|
1700
1700
|
</button>
|
|
1701
1701
|
<button type="button" class="choice" id="c-adopt" aria-pressed="false">
|
|
1702
1702
|
<b>A repo I have</b>
|
|
@@ -1726,7 +1726,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1726
1726
|
<input type="text" id="ghNewName" placeholder="moonbase" autocomplete="off" maxlength="100" aria-label="New repository name">
|
|
1727
1727
|
<!-- visibility is stated on EVERY repo path (task 1002721); updateFreshPreview()
|
|
1728
1728
|
replaces this floor with the same claim plus the resolved repo URL -->
|
|
1729
|
-
<div class="hint" id="ghFreshHint">A new <strong>
|
|
1729
|
+
<div class="hint" id="ghFreshHint">A new <strong>private</strong> repo. You can make it public later.</div>
|
|
1730
1730
|
</div>
|
|
1731
1731
|
<div class="row2" id="manualHome">
|
|
1732
1732
|
<label class="wizField"><span>GitHub owner <small>(user or org)</small></span>
|
|
@@ -5291,9 +5291,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5291
5291
|
hint.className = 'hint err'; hint.innerHTML = 'You already have a repo named <strong>' + esc(state.repo) + '</strong> — pick another name, or adopt it instead.';
|
|
5292
5292
|
} else {
|
|
5293
5293
|
/* Visibility stated up front (task 1002694 / idea 1000684): repos WE create are
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
hint.className = 'hint'; hint.innerHTML = 'A new <strong>
|
|
5294
|
+
private by default (github-repos.js, ADR 0370); the owner can make one
|
|
5295
|
+
public later from the project's manage page. */
|
|
5296
|
+
hint.className = 'hint'; hint.innerHTML = 'A new <strong>private</strong> repo at <code style="font-family:var(--font-mono)">github.com/' + esc(myLogin()) + '/' + esc(state.repo || 'name') + '</code>. You can make it public later.';
|
|
5297
5297
|
}
|
|
5298
5298
|
}
|
|
5299
5299
|
$('ghNewName').addEventListener('input', function () { state.repo = this.value.trim(); updateFreshPreview(); saveDraft(); });
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.80",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.80",
|
|
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.
|
|
3
|
+
"version": "1.21.80",
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -9278,5 +9278,33 @@
|
|
|
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
|
+
}
|
|
9299
|
+
],
|
|
9300
|
+
"1.21.80": [
|
|
9301
|
+
{
|
|
9302
|
+
"id": "1004645",
|
|
9303
|
+
"text": "New projects now get a private GitHub repository by default. Before, the code of every new project was public on the internet from the first save. Owners can still make their repo public later from the project page, and existi"
|
|
9304
|
+
},
|
|
9305
|
+
{
|
|
9306
|
+
"id": "1004393",
|
|
9307
|
+
"text": "The tweak editor is ready to show landing and status pages side by side with the words, instead of in a separate window. It stays off until a small web-server change is made on the host; those steps are waiting in the owner's"
|
|
9308
|
+
}
|
|
9281
9309
|
]
|
|
9282
9310
|
}
|
|
@@ -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 };
|