@bongos/core 1.19.468 → 1.19.470

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 (41) hide show
  1. package/.bongos-core.json +94 -44
  2. package/docs/adr/0227-a-task-network-map-is-refused-the-goal-graph-is-the-map.md +169 -0
  3. package/docs/adr/README.md +1 -0
  4. package/docs/copy-inventory.md +42 -50
  5. package/docs/copy-registry.json +167 -271
  6. package/docs/file-map.md +5 -1
  7. package/docs/module-api-changelog.md +4 -0
  8. package/docs/research/task-network-map/a-every-task-p2.png +0 -0
  9. package/docs/research/task-network-map/a-every-task-p40.png +0 -0
  10. package/docs/research/task-network-map/b-dependency-forest-p2.png +0 -0
  11. package/docs/research/task-network-map/b-dependency-forest-p40.png +0 -0
  12. package/docs/research/task-network-map/c-goals-p2.png +0 -0
  13. package/docs/research/task-network-map/c-goals-p40.png +0 -0
  14. package/docs/research/task-network-map/d-joined-by-goal.png +0 -0
  15. package/docs/research/task-network-map-readability.md +191 -0
  16. package/modules/hall-ui/public/atlas.css +96 -88
  17. package/modules/hall-ui/public/atlas.html +43 -20
  18. package/modules/hall-ui/public/atlas.js +118 -20
  19. package/modules/hall-ui/public/atlas.states.json +52 -1
  20. package/modules/hall-ui/public/modules.css +67 -86
  21. package/modules/hall-ui/public/modules.html +46 -35
  22. package/modules/hall-ui/public/modules.js +195 -103
  23. package/modules/hall-ui/public/modules.states.json +48 -1
  24. package/modules/hall-ui/public/people.css +3 -2
  25. package/modules/hall-ui/public/people.html +2 -2
  26. package/modules/hall-ui/public/people.js +5 -4
  27. package/modules/hall-ui/public/people.states.json +1 -1
  28. package/modules/hall-ui/public/settings.js +1 -1
  29. package/modules/hall-ui/public/style.css +12 -11
  30. package/package-lock.json +2 -2
  31. package/package.json +1 -1
  32. package/scripts/gds/spike-task-map.js +730 -0
  33. package/src/bongos/serve-internal.js +20 -8
  34. package/src/module-api.js +1 -1
  35. package/tests/hall_catalog_world.mjs +181 -0
  36. package/tests/hall_page_gate_map.mjs +4 -2
  37. package/tests/hall_reading_world.mjs +2 -2
  38. package/tests/hall_roster_world.mjs +40 -12
  39. package/tests/spike_task_map.mjs +0 -0
  40. package/modules/hall-ui/public/scouting.html +0 -69
  41. package/modules/hall-ui/public/scouting.states.json +0 -59
@@ -1,20 +1,23 @@
1
1
  // public-builders/modules.js — the hall "Modules" tab (/builders/modules).
2
2
  //
3
- // ADR 0107 §9 (task 1776) + §2/§3 the browsable catalog (task 1001772). Jobs:
3
+ // ADR 0107 §9 (task 1776) + §2/§3 the browsable catalog (task 1001772);
4
+ // redesigned onto the oversight family's row in task 1003472. Jobs:
4
5
  // 1. BROWSE the catalog — every module the core ships that this instance can
5
- // enable, each with its author + origin credit, filterable by a single
6
- // search box over name/author/origin/licence. Split from the instance's
7
- // OWN modules by `source` (which TREE it ships from — the core package or
8
- // this repo), which GET /api/bongos/modules reads structurally off the
9
- // loader's discovery roots. That is a different axis from `provenance`
10
- // (WHO built it, read off the manifest's origin) and both are shown: the
11
- // section says where it ships from, the chips say who to credit.
6
+ // enable, each with its author + origin credit, filterable by one search
7
+ // over name/author/origin/licence and a state lens. Split from the
8
+ // instance's OWN modules by `source` (which TREE it ships from — the core
9
+ // package or this repo), which GET /api/bongos/modules reads structurally
10
+ // off the loader's discovery roots. That is a different axis from
11
+ // `provenance` (WHO built it, read off the manifest's origin) and both are
12
+ // shown: the group says where it ships from, the facts say who to credit.
12
13
  // 2. Enable/disable — a toggle over config/modules.json (POST /modules/:key/enable).
13
14
  // Any signed-in builder sees the state; only metic+archon can flip it (the
14
15
  // server enforces this — a lower-rank click just gets a 403 + a friendly
15
- // toast, same as every other rank-gated write in this hall).
16
+ // toast, same as every other rank-gated write in this hall). A viewer below
17
+ // the floor sees the state as a pill rather than a dead control.
16
18
  // 3. "Submit for review" front door — files one of this project's own modules
17
19
  // into the upstreaming review queue (POST /modules/:key/submit, task 1770).
20
+ // The door sits on the module's own row.
18
21
  //
19
22
  // The rows come from the SAME builder as `bongos module list --catalog`
20
23
  // (src/module-loader/catalog.js), so the two surfaces cannot drift.
@@ -27,31 +30,41 @@
27
30
  // /lib/bongos-client.js. baseUrl:'' passes the full `${API}/...` URLs below
28
31
  // through unchanged; result-mode ({ status, ok, data }) preserves control flow 1:1.
29
32
  const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
30
- const { escapeHtml, $, toast } = window.OTB;
33
+ const { escapeHtml, $, toast, rankLabel } = window.OTB;
34
+ const kit = window.OTBKit;
31
35
 
32
36
  let viewerRank = 'xenos';
33
37
  let canToggle = false;
34
- // The last payload, held so the search box can re-filter without a refetch.
38
+ let submitLive = false;
39
+ // The last payload, held so the filter bar can re-render without a refetch.
35
40
  let allModules = [];
41
+ // The filter bar's current values (search + state lens).
42
+ let query = '';
43
+ let stateLens = '';
44
+ // Which rows a builder has opened, by key, so a re-render (a toggle, a
45
+ // filter) does not fold the description they were reading.
46
+ const openKeys = new Set();
36
47
 
37
- function provenanceChip(m) {
48
+ const floor = () => rankLabel('metic');
49
+
50
+ function provenancePill(m) {
38
51
  return m.provenance === 'core'
39
- ? '<span class="mod-chip mod-chip--core">core-loaded</span>'
40
- : '<span class="mod-chip mod-chip--own">built here</span>';
52
+ ? '<span class="fact-pill fact-pill--info">core-loaded</span>'
53
+ : '<span class="fact-pill">built here</span>';
41
54
  }
42
55
 
43
56
  // Author + origin credit (ADR 0107 §2/§3 — credit now, money later). Reads
44
57
  // `credit`, not the raw manifest fields, so a core module whose manifest
45
- // declares no author still shows who ships it rather than a blank card; an
58
+ // declares no author still shows who ships it rather than a blank row; an
46
59
  // INFERRED credit is marked so it never reads as something the author typed.
47
- function attributionChips(m) {
60
+ function attributionFacts(m) {
48
61
  const credit = m.credit || { author: m.author, origin: m.origin, inferred: false };
49
62
  const mark = credit.inferred ? '<abbr class="mod-inferred" title="Not declared in module.json — this is who ships the module.">*</abbr>' : '';
50
- const chips = [];
51
- if (credit.author) chips.push(`<span class="mod-chip">by ${escapeHtml(credit.author)}${mark}</span>`);
52
- if (credit.origin && credit.origin !== credit.author) chips.push(`<span class="mod-chip">from ${escapeHtml(credit.origin)}${mark}</span>`);
53
- if (m.license) chips.push(`<span class="mod-chip">${escapeHtml(m.license)}</span>`);
54
- return chips.join('');
63
+ const facts = [];
64
+ if (credit.author) facts.push(`<span class="ov-fact">by <b>${escapeHtml(credit.author)}</b>${mark}</span>`);
65
+ if (credit.origin && credit.origin !== credit.author) facts.push(`<span class="ov-fact">from <b>${escapeHtml(credit.origin)}</b>${mark}</span>`);
66
+ if (m.license) facts.push(`<span class="fact-chip">${escapeHtml(m.license)}</span>`);
67
+ return facts.join('');
55
68
  }
56
69
 
57
70
  // Free-text browse over the fields a builder would search by. Mirrors
@@ -63,41 +76,77 @@
63
76
  return [m.key, m.title, m.description, credit.author, credit.origin, m.license]
64
77
  .some((f) => f && String(f).toLowerCase().includes(q));
65
78
  }
79
+ function matchesLens(m) {
80
+ if (stateLens === 'enabled') return !!m.enabled;
81
+ if (stateLens === 'disabled') return !m.enabled;
82
+ return true;
83
+ }
66
84
 
67
85
  // Upkeep posture (ADR 0107 §7 / ADR 0166) — who keeps this module true as the
68
- // core advances. A module with no declared maintenance is shown as such rather
69
- // than silently: an unowned catalog entry is the thing the policy exists to
70
- // catch, so it must be visible here, not only in `bongos upgrade`'s output.
71
- function maintenanceChip(m) {
86
+ // core advances. Colour marks the EXCEPTION: an unowned entry is the thing the
87
+ // policy exists to catch, so it is the warn pill; a maintained one is a plain
88
+ // fact, since seventeen "core-maintained" pills would be the loudest thing on
89
+ // the page and none of them would be news.
90
+ function upkeepFact(m) {
72
91
  const mt = m.maintenance;
73
- if (!mt || !mt.status) return '<span class="mod-chip" title="No maintenance declared — nobody is on the hook for this module as the core advances.">upkeep: undeclared</span>';
74
- const detail = [
75
- mt.status === 'maintained' && m.maintainer ? `Maintainer: ${m.maintainer}` : '',
76
- mt.successor ? `Superseded by "${mt.successor}"` : '',
77
- mt.removeAfter ? `Removable after core ${mt.removeAfter}` : '',
78
- mt.note || '',
79
- ].filter(Boolean).join(' — ');
80
- return `<span class="mod-chip" title="${escapeHtml(detail || mt.status)}">upkeep: ${escapeHtml(mt.status)}</span>`;
92
+ if (!mt || !mt.status) return '<span class="fact-pill fact-pill--warn" title="No maintenance declared — nobody is on the hook for this module as the core advances.">upkeep undeclared</span>';
93
+ const status = String(mt.status);
94
+ if (status === 'deprecated' || status === 'superseded' || mt.successor || mt.removeAfter) {
95
+ return `<span class="fact-pill fact-pill--warn">${escapeHtml(status)}</span>`;
96
+ }
97
+ return `<span class="ov-fact">upkeep <b>${escapeHtml(status)}</b></span>`;
81
98
  }
82
99
 
83
- function cardHTML(m) {
100
+ // The lines the row's details disclosure opens onto: the facts the payload
101
+ // carries that the card never rendered — what the module contributes, the
102
+ // core it needs, who maintains it and what it hands off to.
103
+ function detailLines(m) {
104
+ const lines = [];
105
+ const line = (k, v) => { if (v) lines.push(`<li><span class="ov-key ov-key--faint">${escapeHtml(k)}</span><span class="ov-lines__v">${v}</span></li>`); };
106
+ line('key', `<span class="ov-mono">${escapeHtml(m.key)}</span>`);
107
+ if (m.coreVersion) line('needs core', `<span class="ov-mono">${escapeHtml(String(m.coreVersion))}</span>`);
108
+ const c = m.contributes || {};
109
+ for (const kind of Object.keys(c)) {
110
+ const items = Array.isArray(c[kind]) ? c[kind] : [];
111
+ if (items.length) line(kind, items.map((x) => `<span class="fact-chip">${escapeHtml(String(x))}</span>`).join(''));
112
+ }
113
+ const mt = m.maintenance || {};
114
+ if (m.maintainer) line('maintainer', escapeHtml(String(m.maintainer)));
115
+ if (mt.successor) line('superseded by', escapeHtml(String(mt.successor)));
116
+ if (mt.removeAfter) line('removable after core', `<span class="ov-mono">${escapeHtml(String(mt.removeAfter))}</span>`);
117
+ if (mt.note) line('note', escapeHtml(String(mt.note)));
118
+ if (m.default != null) line('ships', m.default ? 'default on' : 'default off');
119
+ return lines.length ? `<ul class="ov-lines">${lines.join('')}</ul>` : '';
120
+ }
121
+
122
+ // The right column: ONE control. The family's grant toggle when the viewer
123
+ // can flip it (enabled in ink, disabled faint, the accent on hover); the
124
+ // state as a pill when they cannot. A module built here also carries its
125
+ // submit door, since that door is about this module and no other.
126
+ function rightHtml(m) {
127
+ const on = !!m.enabled;
128
+ const control = canToggle
129
+ ? `<button type="button" class="ov-toggle mod-toggle" aria-pressed="${on ? 'true' : 'false'}" data-key="${escapeHtml(m.key)}">${on ? 'enabled' : 'disabled'}</button>`
130
+ : `<span class="fact-pill ${on ? 'fact-pill--ok' : ''}" title="${escapeHtml(floor())} and above can enable or disable a module">${on ? 'enabled' : 'disabled'}</span>`;
131
+ const submit = sourceOf(m) === 'instance'
132
+ ? `<button type="button" class="btn-ghost btn--sm mod-submit" data-submit-key="${escapeHtml(m.key)}"${submitLive ? '' : ' disabled title="Submit pipeline not live yet — tracked by task 1770"'}>submit for review</button>`
133
+ : '';
134
+ return control + submit;
135
+ }
136
+
137
+ function rowHTML(m) {
138
+ const desc = String(m.description || '').trim();
139
+ const lines = detailLines(m);
84
140
  return `
85
- <div class="mod-card" data-key="${escapeHtml(m.key)}">
86
- <div class="mod-card__head">
87
- <div>
88
- <div class="mod-card__title">${escapeHtml(m.title || m.key)}</div>
89
- <div class="mod-card__key">${escapeHtml(m.key)}</div>
90
- </div>
91
- </div>
92
- <p class="mod-card__desc">${escapeHtml(m.description || '')}</p>
93
- <div class="mod-chips">${provenanceChip(m)}${attributionChips(m)}${maintenanceChip(m)}</div>
94
- <div class="mod-card__actions">
95
- <button type="button" class="mod-toggle" data-on="${m.enabled ? '1' : '0'}"
96
- ${canToggle ? '' : 'disabled title="Metic+ can enable or disable a module"'}>
97
- ${m.enabled ? 'Enabled' : 'Disabled'}
98
- </button>
141
+ <li class="ov-row mod-row" data-key="${escapeHtml(m.key)}">
142
+ <div class="ov-row__main">
143
+ <span class="ov-row__title">${escapeHtml(m.title || m.key)}<span class="ov-mono">${escapeHtml(m.key)}</span></span>
144
+ ${desc ? `<p class="ov-row__sub mod-row__desc">${escapeHtml(desc)}</p>` : ''}
145
+ <div class="ov-row__facts">${provenancePill(m)}${attributionFacts(m)}${upkeepFact(m)}</div>
146
+ ${lines ? `<details class="mod-more"${openKeys.has(m.key) ? ' open' : ''}><summary aria-label="Details for ${escapeHtml(m.title || m.key)}"></summary>${lines}</details>` : ''}
99
147
  </div>
100
- </div>`;
148
+ <div class="ov-row__right mod-row__right">${rightHtml(m)}</div>
149
+ </li>`;
101
150
  }
102
151
 
103
152
  // `source` splits the page: 'core' = shipped by the core package (the
@@ -109,81 +158,126 @@
109
158
  return m.provenance === 'core' ? 'core' : 'instance';
110
159
  }
111
160
 
112
- function renderGrids() {
113
- const input = $('#mod-search');
114
- const q = input ? input.value.trim().toLowerCase() : '';
115
- const matching = allModules.filter((m) => matchesQuery(m, q));
161
+ // The figure band: the four numbers the page head used to carry as prose.
162
+ function figsHTML() {
163
+ const core = allModules.filter((m) => sourceOf(m) === 'core');
164
+ const own = allModules.filter((m) => sourceOf(m) === 'instance');
165
+ const enabled = allModules.filter((m) => m.enabled).length;
166
+ const undeclared = allModules.filter((m) => !m.maintenance || !m.maintenance.status).length;
167
+ const fig = (l, v, s) => `<div class="ov-fig"><span class="ov-fig__l">${l}</span><span class="ov-fig__v">${v}</span>${s ? `<span class="ov-fig__s">${s}</span>` : ''}</div>`;
168
+ // Short labels on purpose: the band is two columns at 390px and a label
169
+ // that wraps a phone's column is cut with an ellipsis.
170
+ return fig('modules', allModules.length, `${core.length} from the core`)
171
+ + fig('enabled', enabled, `of ${allModules.length}`)
172
+ + fig('built here', own.length, own.length ? 'can go upstream' : 'none on disk')
173
+ + fig('no upkeep', undeclared, undeclared ? 'nobody on the hook' : 'every module owned');
174
+ }
175
+
176
+ function renderRows() {
177
+ const matching = allModules.filter((m) => matchesQuery(m, query) && matchesLens(m));
116
178
  const core = matching.filter((m) => sourceOf(m) === 'core');
117
179
  const own = matching.filter((m) => sourceOf(m) === 'instance');
180
+ const narrowed = !!(query || stateLens);
118
181
 
119
- const coreGrid = $('#core-grid');
120
- if (coreGrid) {
121
- coreGrid.innerHTML = core.length ? core.map(cardHTML).join('')
122
- : `<div class="mod-empty">${q ? 'No catalog module matches that search.' : 'The core ships no modules this instance can see.'}</div>`;
182
+ const coreList = $('#core-grid');
183
+ if (coreList) {
184
+ coreList.innerHTML = core.length ? core.map(rowHTML).join('')
185
+ : `<li>${kit.emptyStateHtml(narrowed ? 'No catalog module matches that search.' : 'The core ships no modules this instance can see.')}</li>`;
123
186
  }
124
- const ownGrid = $('#own-grid');
125
- if (ownGrid) {
126
- ownGrid.innerHTML = own.length ? own.map(cardHTML).join('')
127
- : `<div class="mod-empty">${q ? 'None of this project\'s own modules match that search.' : 'None on disk — everything here ships with the core.'}</div>`;
187
+ const ownList = $('#own-grid');
188
+ if (ownList) {
189
+ ownList.innerHTML = own.length ? own.map(rowHTML).join('')
190
+ : `<li>${kit.emptyStateHtml(narrowed ? 'None of this project\'s own modules match that search.' : 'None on disk — everything here ships with the core.')}</li>`;
128
191
  }
192
+ const coreN = $('#core-n');
193
+ if (coreN) coreN.textContent = String(core.length);
194
+ const ownN = $('#own-n');
195
+ if (ownN) ownN.textContent = String(own.length);
129
196
 
130
- const count = $('#mod-browse-count');
197
+ // The count line speaks only while a filter narrows the list — the figure
198
+ // band under it already says how many there are.
199
+ const count = $('#mod-count');
131
200
  if (count) {
132
- count.textContent = q
133
- ? `${matching.length} of ${allModules.length} module(s) match “${input.value.trim()}”.`
201
+ const n = allModules.length;
202
+ count.textContent = narrowed
203
+ ? `${matching.length} of ${n} ${n === 1 ? 'module' : 'modules'} match${query ? ` “${query}”` : ''}${stateLens ? ` · ${stateLens}` : ''}.`
134
204
  : '';
135
205
  }
206
+ }
136
207
 
137
- document.querySelectorAll('.mod-toggle').forEach((btn) => btn.addEventListener('click', onToggle));
208
+ // One listener on the panel outlives every re-render: the toggle, the submit
209
+ // door and the details disclosure all bubble to it.
210
+ let wired = false;
211
+ function wire() {
212
+ if (wired) return;
213
+ wired = true;
214
+ const panel = $('#core-scroll');
215
+ if (!panel) return;
216
+ panel.addEventListener('click', (ev) => {
217
+ const toggle = ev.target.closest('.mod-toggle');
218
+ if (toggle) { onToggle(toggle); return; }
219
+ const submit = ev.target.closest('.mod-submit');
220
+ if (submit) onSubmit(submit);
221
+ });
222
+ panel.addEventListener('toggle', (ev) => {
223
+ const d = ev.target;
224
+ if (!(d instanceof HTMLDetailsElement) || !d.classList.contains('mod-more')) return;
225
+ const row = d.closest('.mod-row');
226
+ const key = row && row.dataset.key;
227
+ if (!key) return;
228
+ if (d.open) openKeys.add(key); else openKeys.delete(key);
229
+ }, true);
138
230
  }
139
231
 
140
232
  function render(data) {
141
233
  allModules = Array.isArray(data.modules) ? data.modules : [];
142
- const core = allModules.filter((m) => sourceOf(m) === 'core');
143
- const own = allModules.filter((m) => sourceOf(m) === 'instance');
144
- const enabled = core.filter((m) => m.enabled).length;
234
+ submitLive = !!data.submit_pipeline_live;
235
+
236
+ const figs = $('#mod-figs');
237
+ if (figs) figs.innerHTML = figsHTML();
145
238
 
146
- const sub = $('#modules-sub');
147
- if (sub) {
148
- sub.textContent = `${allModules.length} module(s) — ${core.length} in the core catalog (${enabled} enabled here), `
149
- + `${own.length} built by this project.`;
239
+ const mount = $('#mod-filters');
240
+ if (mount && !mount.childElementCount) {
241
+ kit.makeFilterBar({
242
+ mount,
243
+ search: { id: 'mod-search', label: 'Search', placeholder: 'name, author, origin or licence' },
244
+ primary: [
245
+ { id: 'mod-state', label: 'State', options: [
246
+ { value: '', label: 'all' }, { value: 'enabled', label: 'enabled' }, { value: 'disabled', label: 'disabled' }] },
247
+ ],
248
+ onChange: (vals) => {
249
+ query = String(vals['mod-search'] || '').trim().toLowerCase();
250
+ stateLens = String(vals['mod-state'] || '');
251
+ renderRows();
252
+ },
253
+ });
150
254
  }
151
255
 
152
- renderGrids();
153
-
154
- // Only this project's OWN modules can be submitted upstream: a module that
155
- // already ships with the core is, by definition, already in the catalog.
156
- const submitStatus = $('#submit-status');
157
- if (submitStatus) {
158
- submitStatus.innerHTML = own.length
159
- ? own.map((m) => `
160
- <span style="display:inline-flex;align-items:center;gap:8px;margin:4px 12px 4px 0;">
161
- <button type="button" class="mod-submit" data-submit-key="${escapeHtml(m.key)}"
162
- ${data.submit_pipeline_live ? '' : 'disabled title="Submit pipeline not live yet — tracked by task 1770"'}>
163
- Submit "${escapeHtml(m.key)}" for review
164
- </button>
165
- </span>`).join('')
166
- : '<span class="mod-note">Nothing to submit — every module here already ships with the core.</span>';
167
- document.querySelectorAll('[data-submit-key]').forEach((btn) => btn.addEventListener('click', onSubmit));
256
+ const lede = $('#submit-lede');
257
+ if (lede && !allModules.some((m) => sourceOf(m) === 'instance')) {
258
+ lede.textContent = 'Nothing to submit — every module here ships with the core.';
168
259
  }
169
260
 
170
- const search = $('#mod-search');
171
- if (search) search.addEventListener('input', renderGrids);
261
+ wire();
262
+ renderRows();
172
263
  }
173
264
 
174
- async function onToggle(ev) {
175
- const btn = ev.currentTarget;
176
- const card = btn.closest('.mod-card');
177
- const key = card && card.dataset.key;
265
+ async function onToggle(btn) {
266
+ const key = btn.dataset.key;
178
267
  if (!key) return;
179
- const next = btn.dataset.on !== '1';
268
+ const next = btn.getAttribute('aria-pressed') !== 'true';
180
269
  btn.disabled = true;
181
270
  try {
182
271
  const res = await api.request('POST', `${API}/modules/${encodeURIComponent(key)}/enable`, { body: { enabled: next } });
183
272
  const body = res.data || {};
184
273
  if (!res.ok) throw new Error(body.message || `enable → ${res.status}`);
185
- btn.dataset.on = next ? '1' : '0';
186
- btn.textContent = next ? 'Enabled' : 'Disabled';
274
+ const m = allModules.find((x) => x.key === key);
275
+ if (m) m.enabled = next;
276
+ btn.setAttribute('aria-pressed', next ? 'true' : 'false');
277
+ btn.textContent = next ? 'enabled' : 'disabled';
278
+ const figs = $('#mod-figs');
279
+ if (figs) figs.innerHTML = figsHTML();
280
+ renderRows();
187
281
  toast(body.note || (next ? `${key} enabled.` : `${key} disabled.`), { duration: 3200 });
188
282
  } catch (err) {
189
283
  console.error('[modules] toggle failed', err);
@@ -193,8 +287,7 @@
193
287
  }
194
288
  }
195
289
 
196
- async function onSubmit(ev) {
197
- const btn = ev.currentTarget;
290
+ async function onSubmit(btn) {
198
291
  const key = btn.dataset.submitKey;
199
292
  try {
200
293
  const res = await api.request('POST', `${API}/modules/${encodeURIComponent(key)}/submit`);
@@ -217,12 +310,11 @@
217
310
  const res = await api.request('GET', `${API}/modules`);
218
311
  if (res.status === 401) { window.location.assign(`${API}/auth/web/start`); return; }
219
312
  if (!res.ok) throw new Error(`modules → ${res.status}`);
220
- const data = res.data;
221
- render(data);
313
+ render(res.data);
222
314
  } catch (err) {
223
315
  console.error('[modules] failed to load', err);
224
- const sub = $('#modules-sub');
225
- if (sub) sub.textContent = 'Could not load modules. Return to the hall and try again.';
316
+ const list = $('#core-grid');
317
+ if (list) list.innerHTML = `<li>${kit.emptyStateHtml('Could not load modules. Return to the hall and try again.')}</li>`;
226
318
  toast('Could not load modules.', { duration: 3200 });
227
319
  }
228
320
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "_": "Modules — the core + instance module grids under one search. The empty state matters more than the populated one: a filter that matches nothing is the state a browse page spends real time in. Written with the re-skin (task 1003307) so this page is MEASURED at three widths in both modes rather than looked at once.",
2
+ "_": "Modules — the catalog as ONE panel of oversight rows (task 1003472): the kit filter bar, the figure band, two groups under heads. The empty state matters more than the populated one: a filter that matches nothing is the state a browse page spends real time in. Written with the re-skin (task 1003307) so this page is MEASURED at three widths in both modes rather than looked at once; the redesign added the `lens` and `details` states. Populated look: the hall-preview harness (scripts/hall-preview/fixtures/modules.json carries eighteen modules, one built by the project).",
3
3
  "page": "/builders/modules",
4
4
  "surface": "hall-ui",
5
5
  "stub": {
@@ -46,6 +46,10 @@
46
46
  "search-empty": {
47
47
  "auth": true,
48
48
  "actions": [
49
+ [
50
+ "wait",
51
+ 600
52
+ ],
49
53
  [
50
54
  "fill",
51
55
  "#mod-search",
@@ -57,6 +61,49 @@
57
61
  ]
58
62
  ]
59
63
  },
64
+ "lens": {
65
+ "auth": true,
66
+ "actions": [
67
+ [
68
+ "wait",
69
+ 600
70
+ ],
71
+ [
72
+ "focus",
73
+ "#mod-state"
74
+ ],
75
+ [
76
+ "press",
77
+ "ArrowDown"
78
+ ],
79
+ [
80
+ "wait",
81
+ 300
82
+ ]
83
+ ],
84
+ "expect": {
85
+ "visible": [
86
+ "#mod-state"
87
+ ]
88
+ }
89
+ },
90
+ "details": {
91
+ "auth": true,
92
+ "actions": [
93
+ [
94
+ "wait",
95
+ 600
96
+ ],
97
+ [
98
+ "clickIfVisible",
99
+ "#core-grid .mod-row:first-child .mod-more > summary"
100
+ ],
101
+ [
102
+ "wait",
103
+ 300
104
+ ]
105
+ ]
106
+ },
60
107
  "out": {
61
108
  "auth": false
62
109
  }
@@ -1,6 +1,7 @@
1
1
  /* people.css — page-local styles for the cross-project builder directory
2
- (task 1003470, goal 1000081), served at /people and at its legacy door
3
- /scouting, which loads the same page. Only the identity cell lives here: the
2
+ (task 1003470, goal 1000081), served at /people — its legacy door /scouting
3
+ was deleted in task 1003440 and now 301s here. Only the identity cell lives
4
+ here: the
4
5
  ledger, the filter bar, the pager, the chips and the empty state are the
5
6
  shell's and the kit's. Tokens are the world's real names — no compat alias. */
6
7
 
@@ -30,8 +30,8 @@
30
30
  </div>
31
31
 
32
32
  <main id="hall-main">
33
- <!-- task 1003470: ONE directory. /people and its legacy door /scouting
34
- render this same page from the same script — the same
33
+ <!-- task 1003470: ONE directory. Its legacy door /scouting rendered this
34
+ same page from the same script, and task 1003440 deleted it — the same
35
35
  /api/gds/scouting rollup used to be drawn three different ways. -->
36
36
  <section class="scroll" id="people-scroll" aria-labelledby="people-h">
37
37
  <h2 id="people-h" class="scroll__h">Builder directory<span class="count-pill" id="people-count" hidden></span></h2>
@@ -1,10 +1,11 @@
1
1
  // people.js — the People page: the cross-project builder directory (task
2
- // 1002577; ADR 0141 §4 is the data's design of record), served at /people and
3
- // at its legacy door /scouting — task 1003470 made the two ONE page, this
4
- // script, because the same /api/gds/scouting rollup was being drawn three
2
+ // 1002577; ADR 0141 §4 is the data's design of record), served at /people —
3
+ // task 1003470 first made its legacy door /scouting render this same script,
4
+ // and task 1003440 deleted that door (the URL 301s here), because the
5
+ // same /api/gds/scouting rollup was being drawn three
5
6
  // different ways (a kit ledger here, a hand-built table there, and the same
6
7
  // columns in a different order). The endpoint is gated server-side on
7
- // 'project.curate' OR being a project owner (R14, ADR 0210); both pages are
8
+ // 'project.curate' OR being a project owner (R14, ADR 0210); this page is
8
9
  // gated in serve-internal.js on that same pair, so a visitor who is neither
9
10
  // never sees this shell.
10
11
  //
@@ -1,5 +1,5 @@
1
1
  {
2
- "_": "People — the cross-project directory behind requireProjectCuratePage (metic+ via project.curate, or a project owner). Descriptive only; it grants no authority, and the page says so above the fold. Written with the re-skin (task 1003307); the redesign (task 1003470) added the identity cell, the sort and craft lenses, and the `search` state. /scouting is the legacy door to this same page (scouting.states.json measures it separately). Populated look: the hall-preview harness (scripts/hall-preview/fixtures/scouting.json carries five builders, three with avatars).",
2
+ "_": "People — the cross-project directory behind requireProjectCuratePage (metic+ via project.curate, or a project owner). Descriptive only; it grants no authority, and the page says so above the fold. Written with the re-skin (task 1003307); the redesign (task 1003470) added the identity cell, the sort and craft lenses, and the `search` state. Its legacy door /scouting was deleted in task 1003440; the URL 301s here. Populated look: the hall-preview harness (scripts/hall-preview/fixtures/scouting.json carries five builders, three with avatars).",
3
3
  "page": "/builders/people",
4
4
  "surface": "hall-ui",
5
5
  "stub": {
@@ -555,7 +555,7 @@
555
555
  // GET/PATCH /api/bongos/me/cross-project (platform-identity module — hub only).
556
556
  // One on/off toggle: "show my cross-project record on my public profile".
557
557
  // Enforcement is server-side in the module's read functions, so flipping it
558
- // takes effect immediately on the profile, /people, and /scouting. The section
558
+ // takes effect immediately on the profile and /people. The section
559
559
  // reveals ONLY when the module's endpoint answers (404 on a vanilla instance →
560
560
  // stays hidden, no error state). P04 later re-homes this in the Profile rail
561
561
  // group; storage is account-global on the hub (not behavior_prefs).
@@ -2859,9 +2859,10 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
2859
2859
  does not exist, so "Someone helped" rendered full-size on every row. */
2860
2860
  .btn--sm { min-height: 26px; padding: 0 11px; font-size: 12.5px; }
2861
2861
 
2862
- /* people.js:45 — the builder's name cell. The only definition in the hall lives
2863
- in scouting.html's inline <style>, so the same data read bold on Scouting and
2864
- plain on People. One definition, both pages. */
2862
+ /* people.js:45 — the builder's name cell. The hall's only definition used to
2863
+ live in scouting.html's inline <style>, so the same data read bold on
2864
+ Scouting and plain on People. It lives here now, and scouting.html itself
2865
+ went with task 1003440. */
2865
2866
  .who { font-weight: 600; }
2866
2867
 
2867
2868
  /* people.js:105 — notice() sets body.className='note'. .note lives in
@@ -3056,18 +3057,18 @@ html.palette-open { overflow: hidden; }
3056
3057
  The state colours are the hall's fixed verification vocabulary, never the
3057
3058
  world's warm accent states (ADR 0196 §2): a status must not re-tint when an
3058
3059
  instance re-brands. */
3059
- /* The five names below are the SAME pill, found by the task 1003307 audit:
3060
- government, harbor, gate, watch and modules each invented one because the
3061
- shell had none to take. They are aliased rather than renamed because renaming
3062
- would mean editing five JS row builders, and a class name is not what was
3063
- wrong — the duplicated, hand-rolled label voice was. Their own state
3064
- modifiers stay in their own sheets. */
3060
+ /* The names below are the SAME pill, found by the task 1003307 audit:
3061
+ government, harbor, gate and watch each invented one because the shell had
3062
+ none to take (modules had one too; its rows take .fact-pill by name since
3063
+ task 1003472). They are aliased rather than renamed because renaming would
3064
+ mean editing the JS row builders, and a class name is not what was wrong —
3065
+ the duplicated, hand-rolled label voice was. Their own state modifiers stay
3066
+ in their own sheets. */
3065
3067
  .fact-pill,
3066
3068
  .gov-pill,
3067
3069
  .harbor-pill,
3068
3070
  .gate-pill,
3069
- .badge,
3070
- .mod-chip {
3071
+ .badge {
3071
3072
  display: inline-flex;
3072
3073
  align-items: center;
3073
3074
  min-height: 24px;
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.468",
3
+ "version": "1.19.470",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.468",
9
+ "version": "1.19.470",
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.19.468",
3
+ "version": "1.19.470",
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",