@bongos/core 1.19.448 → 1.19.450

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 (42) hide show
  1. package/.bongos-core.json +96 -36
  2. package/docs/copy-inventory.md +121 -114
  3. package/docs/copy-registry.json +377 -331
  4. package/docs/file-map.md +1 -1
  5. package/docs/module-api-changelog.md +4 -0
  6. package/modules/hall-ui/public/diagram-viewer.css +51 -34
  7. package/modules/hall-ui/public/diagram-viewer.js +4 -1
  8. package/modules/hall-ui/public/diagrams.css +45 -0
  9. package/modules/hall-ui/public/diagrams.html +12 -20
  10. package/modules/hall-ui/public/diagrams.states.json +28 -0
  11. package/modules/hall-ui/public/idea.css +138 -71
  12. package/modules/hall-ui/public/idea.html +13 -3
  13. package/modules/hall-ui/public/idea.js +17 -4
  14. package/modules/hall-ui/public/idea.states.json +14 -0
  15. package/modules/hall-ui/public/ideas.css +148 -107
  16. package/modules/hall-ui/public/ideas.html +15 -5
  17. package/modules/hall-ui/public/ideas.states.json +15 -0
  18. package/modules/hall-ui/public/primer.css +92 -400
  19. package/modules/hall-ui/public/primer.html +68 -26
  20. package/modules/hall-ui/public/primer.js +107 -308
  21. package/modules/hall-ui/public/primer.states.json +38 -0
  22. package/modules/hall-ui/public/ranks.html +9 -1
  23. package/modules/hall-ui/public/ranks.states.json +1 -1
  24. package/modules/hall-ui/public/record.css +324 -0
  25. package/modules/hall-ui/public/roadmap.css +152 -86
  26. package/modules/hall-ui/public/roadmap.html +6 -1
  27. package/modules/hall-ui/public/roadmap.states.json +31 -0
  28. package/modules/hall-ui/public/room.css +441 -0
  29. package/modules/hall-ui/public/style.css +269 -63
  30. package/modules/hall-ui/public/task.css +71 -0
  31. package/modules/hall-ui/public/task.html +15 -32
  32. package/modules/hall-ui/public/task.js +142 -39
  33. package/modules/hall-ui/public/task.states.json +14 -0
  34. package/modules/hall-ui/public/work.css +4 -15
  35. package/modules/ui-design/kit/serve.js +7 -1
  36. package/package-lock.json +2 -2
  37. package/package.json +1 -1
  38. package/scripts/hall-preview/server.js +34 -0
  39. package/src/module-api.js +1 -1
  40. package/tests/hall_audit.mjs +18 -11
  41. package/tests/hall_reading_world.mjs +430 -0
  42. package/tests/hall_record_world.mjs +412 -0
@@ -141,12 +141,77 @@
141
141
  return `${Math.floor(sec / 86400)}d ago`;
142
142
  }
143
143
 
144
+ // The status vocabulary, which is the ONE sanctioned exception to the world's
145
+ // Fifteen Rule (DESIGN.md §Reuse) — a lifecycle state is verification state, so
146
+ // it keeps green/amber/red and never borrows the accent. The accent is for what
147
+ // is pressable or live, and a status pill is neither.
148
+ //
149
+ // `backlog` is DELIBERATELY absent, and that is a change: the retired inline
150
+ // block painted it amber, identically to `blocked`. Those are not the same
151
+ // fact. Amber says "something is wrong here", which is true of a task blocked
152
+ // behind an unshipped dependency and false of one simply not scheduled yet, and
153
+ // conflating them made every unscheduled task read as a problem. Backlog takes
154
+ // the neutral pill; the lifecycle rail's first beat carries the distinction in
155
+ // words ("not ready to be claimed yet" vs "waiting on another task").
156
+ const PILL_TONE = {
157
+ shipped: 'ok', confirmed: 'ok',
158
+ active: 'info', completed: 'info', ready: 'info',
159
+ blocked: 'warn',
160
+ };
161
+ function statusPill(status, extraClass) {
162
+ const s = String(status || 'unknown');
163
+ const tone = PILL_TONE[s];
164
+ return `<span class="rec-pill${tone ? ` rec-pill--${tone}` : ''}${extraClass ? ` ${extraClass}` : ''}">${escapeHtml(s)}</span>`;
165
+ }
166
+
144
167
  function depList(kindWord, items) {
145
168
  const rows = items.map((d) => {
146
- const status = d.status ? `<span class="td-dep-status">${escapeHtml(d.status)}</span>` : '';
147
- return `<li><a class="td-dep" href="${hallBase()}/task/${escapeHtml(String(d.id))}">#${escapeHtml(String(d.id))} — ${escapeHtml(d.title || 'task')}</a>${status}</li>`;
169
+ const status = d.status ? statusPill(d.status, 'rec-pill--sm') : '';
170
+ return `<li><a class="rec-dep" href="${hallBase()}/task/${escapeHtml(String(d.id))}">` +
171
+ `<b class="rec-dep__id">${escapeHtml(String(d.id))}</b>${escapeHtml(d.title || 'task')}</a>${status}</li>`;
148
172
  }).join('');
149
- return `<div class="td-deps"><p class="td-section-h">${escapeHtml(kindWord)}</p><ul class="td-dep-list">${rows}</ul></div>`;
173
+ return `<section><p class="rec-sec__h">${escapeHtml(kindWord)}</p><ul class="rec-deplist">${rows}</ul></section>`;
174
+ }
175
+
176
+ // The lifecycle rail: five beats, exactly one lit. Six GDS statuses fold into
177
+ // five because backlog / blocked / ready all mean "no builder holds it yet";
178
+ // the first beat's caption is what says WHICH of the three it is, so the rail
179
+ // never implies a claim that has not happened.
180
+ const RAIL_AT = { active: 1, completed: 2, confirmed: 3, shipped: 4 };
181
+ const CLAIM_CAPTION = {
182
+ backlog: 'not ready to be claimed yet',
183
+ blocked: 'waiting on another task',
184
+ ready: 'waiting for a builder',
185
+ };
186
+ function lifecycleRail(t, credits) {
187
+ const at = RAIL_AT[t.status] ?? 0;
188
+ const landed = t.status === 'shipped' && credits != null;
189
+ const beats = [
190
+ ['Claim', CLAIM_CAPTION[t.status] || 'one builder, one worktree'],
191
+ ['Build', 'the code and the docs'],
192
+ ['Grade', 'the panel reads the diff'],
193
+ ['Ship', 'merged, then deployed'],
194
+ ['Credit', landed ? `${credits} ${window.OTBBranding?.taskCreditLabel?.(t) ?? curLabel()} landed` : 'credits land on the merge'],
195
+ ];
196
+ const steps = beats.map(([name, caption], i) =>
197
+ `<li class="tk-step"${i === at ? ' aria-current="step"' : ''}>` +
198
+ `<div class="tk-step__dot"></div><span class="tk-step__n">${escapeHtml(name)}</span>` +
199
+ `<span class="tk-step__c">${escapeHtml(caption)}</span></li>`).join('');
200
+ return `<ol class="tk-rail" aria-label="Lifecycle">${steps}</ol>`;
201
+ }
202
+
203
+ // The figure band — v2's stat-tile grid, the reference for any figure row the
204
+ // hall renders. Only figures the record actually CARRIES are drawn: most tasks
205
+ // have no recorded actual, and a band of dashes reads as a broken record
206
+ // rather than an honest one. Under two figures there is no band at all.
207
+ function figureBand(figs) {
208
+ const present = figs.filter((f) => f && f[1] != null && f[1] !== '');
209
+ if (present.length < 2) return '';
210
+ const cells = present.map(([label, value, unit]) =>
211
+ `<div class="rec-fig"><span class="rec-fig__l">${escapeHtml(label)}</span>` +
212
+ `<span class="rec-fig__v">${escapeHtml(String(value))}` +
213
+ `${unit ? `<u class="rec-fig__u">${escapeHtml(unit)}</u>` : ''}</span></div>`).join('');
214
+ return `<div class="rec-figs">${cells}</div>`;
150
215
  }
151
216
 
152
217
  // task 1003109 (goal 1000072, "Human legibility") — the ship-time visual.
@@ -164,11 +229,11 @@
164
229
  if (!/^\/api\/(gds|bongos)\/task-visuals\/task-\d+-[0-9a-f]{16}\.(png|jpg|gif|webp)$/.test(url)) return '';
165
230
  const alt = String(t.visual_alt || `Visual for task ${t.id}`);
166
231
  const caption = t.visual_alt
167
- ? `<figcaption class="td-visual__caption">${escapeHtml(t.visual_alt)}</figcaption>`
232
+ ? `<figcaption class="tk-visual__caption">${escapeHtml(t.visual_alt)}</figcaption>`
168
233
  : '';
169
- return `<figure class="td-visual">` +
234
+ return `<figure class="tk-visual">` +
170
235
  `<a href="${escapeHtml(url)}" target="_blank" rel="noopener">` +
171
- `<img class="td-visual__img" src="${escapeHtml(url)}" alt="${escapeHtml(alt)}" loading="lazy">` +
236
+ `<img class="tk-visual__img" src="${escapeHtml(url)}" alt="${escapeHtml(alt)}" loading="lazy">` +
172
237
  `</a>${caption}</figure>`;
173
238
  }
174
239
 
@@ -196,29 +261,33 @@
196
261
  function renderTask(t, lastClaim, viewer, goal) {
197
262
  document.title = `#${t.id} — ${t.title || 'Task'} — ${document.title.split(' — ').pop()}`;
198
263
  const sub = $('#task-sub');
199
- if (sub) sub.textContent = `Task #${t.id}`;
264
+ if (sub) sub.textContent = `Task ${t.id}`;
200
265
 
201
266
  const parts = [];
202
- parts.push(`<p class="deeplink-card__kind">TASK <span class="task-status-pill task-status-pill--${escapeHtml(t.status || '')}">${escapeHtml(t.status || 'unknown')}</span></p>`);
203
- parts.push(`<h1 class="deeplink-card__title" id="task-h">#${t.id}${t.title ? ' — ' + escapeHtml(t.title) : ''}</h1>`);
267
+ parts.push(`<p class="rec-kind">Task ${statusPill(t.status)}</p>`);
268
+ parts.push(`<h1 class="rec-title" id="task-h"><span class="rec-title__id">${escapeHtml(String(t.id))}</span>${escapeHtml(t.title || 'Untitled task')}</h1>`);
204
269
 
270
+ // The facts that place the task, in one line: what it belongs to, what KIND
271
+ // of work it is, and who is allowed to take it. The figures moved out of this
272
+ // run into the band below — a credit total set as one more comma-separated
273
+ // word is the thing the round's stat-tile reference exists to fix.
205
274
  const meta = [];
206
- if (t.version_id) meta.push(escapeHtml(t.version_id));
275
+ if (t.version_id) meta.push(`<b>${escapeHtml(t.version_id)}</b>`);
207
276
  if (t.kind) meta.push(escapeHtml(t.kind));
208
277
  if (t.discipline && t.discipline !== 'unclassified') meta.push(escapeHtml(t.discipline));
209
- if (t.priority != null) meta.push('P' + escapeHtml(String(t.priority)));
210
- meta.push('min: ' + escapeHtml(rankText(t.requires_rank)));
211
- const creditVal = window.OTBBranding?.taskCreditValue?.(t) ?? t.credits_reward;
212
- if (creditVal != null) meta.push(`${creditVal} ${window.OTBBranding?.taskCreditLabel?.(t) ?? curLabel()}`);
213
- if (t.est_minutes) meta.push(`${t.est_minutes}m`);
214
- parts.push(`<p class="deeplink-card__meta">${meta.join(' · ')}</p>`);
278
+ // rankText() answers "any rank" for an open task, which read as
279
+ // "min rank any rank" once the label was prefixed. An open floor is not a
280
+ // minimum, so it is said as the absence of one.
281
+ const floor = rankText(t.requires_rank);
282
+ meta.push(escapeHtml(floor === 'any rank' ? 'open to any rank' : `min rank ${floor}`));
283
+ parts.push(`<p class="rec-meta">${meta.join(' &middot; ')}</p>`);
215
284
 
216
285
  // task 1002955: when was this written? The panel showed a shipped date but
217
286
  // never a created one, so an untouched task carried no age at all. Absolute
218
287
  // date first (the panel has the room the card chip doesn't), age after it.
219
288
  if (t.created_at) {
220
289
  const age = fmtRel(t.created_at);
221
- parts.push(`<p class="deeplink-card__note">Created ${escapeHtml(fmtDate(t.created_at))}${age ? ` — ${escapeHtml(age)}` : ''}.</p>`);
290
+ parts.push(`<p class="rec-note">Created ${escapeHtml(fmtDate(t.created_at))}${age ? `, ${escapeHtml(age)}` : ''}.</p>`);
222
291
  }
223
292
 
224
293
  if (t.goal_id) {
@@ -227,24 +296,35 @@
227
296
  // read "Goal #1000072", which tells a person nothing they didn't already
228
297
  // know, and the chip answers "what kind of work is this" without opening
229
298
  // the goal. Marked inherited because a task carries no category of its own.
230
- const goalName = goal && goal.title ? ` — ${escapeHtml(goal.title)}` : '';
299
+ const goalName = goal && goal.title ? `, ${escapeHtml(goal.title)}` : '';
231
300
  const catChip = window.OTB.categoryChip(goal && goal.category, { inherited: true });
232
- parts.push(`<p class="td-goal">Goal: <a href="/goals#/goal/${encodeURIComponent(t.goal_id)}">Goal #${escapeHtml(String(t.goal_id))}</a>${goalName}${catChip}</p>`);
301
+ parts.push(`<p class="rec-goal">Goal <a href="/goals#/goal/${encodeURIComponent(t.goal_id)}">${escapeHtml(String(t.goal_id))}${goalName}</a>${catChip}</p>`);
233
302
  }
234
303
 
304
+ const creditVal = window.OTBBranding?.taskCreditValue?.(t) ?? t.credits_reward;
305
+ parts.push(figureBand([
306
+ [window.OTBBranding?.taskCreditLabel?.(t) ?? curLabel(), creditVal],
307
+ ['Estimated', t.est_minutes, 'm'],
308
+ ['Actual', t.actual_minutes, 'm'],
309
+ ['Priority', t.priority == null ? null : 'P' + t.priority],
310
+ ]));
311
+ parts.push(lifecycleRail(t, creditVal));
312
+
235
313
  // Claim history (task 751 — best-effort: the API surfaces the most recent
236
314
  // claim holder, not the full ledger of every past claim).
237
315
  if (lastClaim) {
238
316
  const who = escapeHtml(lastClaim.github_login || 'a builder');
239
317
  const when = lastClaim.claimed_at ? fmtDate(lastClaim.claimed_at, { time: true }) : '';
240
318
  const state = lastClaim.released_at
241
- ? (lastClaim.outcome ? `released (${escapeHtml(lastClaim.outcome)})` : 'released')
319
+ ? (lastClaim.outcome ? `released, outcome ${escapeHtml(lastClaim.outcome)}` : 'released')
242
320
  : 'holding this claim';
243
- parts.push(`<p class="task-detail__claim">Last claimed by <strong>${who}</strong> ${when ? 'on ' + escapeHtml(when) : ''} — ${state}.</p>`);
321
+ parts.push(`<section class="rec-sec"><p class="rec-sec__h">Claim</p>` +
322
+ `<p class="rec-body">Last claimed by <b>${who}</b>${when ? ' on ' + escapeHtml(when) : ''}. ${state}.` +
323
+ ` The task feed carries the most recent claim only, never the full ledger.</p></section>`);
244
324
  }
245
325
 
246
326
  if (t.value_summary) {
247
- parts.push(`<p class="td-goal"><strong>Value:</strong> ${escapeHtml(t.value_summary)}</p>`);
327
+ parts.push(`<section class="rec-sec"><p class="rec-sec__h">Value</p><p class="rec-body">${escapeHtml(t.value_summary)}</p></section>`);
248
328
  }
249
329
 
250
330
  // task 1003109: the ship-time VISUAL — the same claim as the value summary in
@@ -252,23 +332,29 @@
252
332
  // isn't. Absent on almost every task, and that is not an omission: a task with
253
333
  // no visual must never look incomplete, so nothing is drawn at all when there
254
334
  // is none — no placeholder, no empty frame, no "no visual" line.
255
- if (t.visual_url) parts.push(taskVisualHtml(t));
335
+ if (t.visual_url) parts.push(`<section class="rec-sec">${taskVisualHtml(t)}</section>`);
256
336
 
257
337
  if (t.description) {
258
- parts.push(`<div class="td-desc">${escapeHtml(t.description)}</div>`);
338
+ parts.push(`<section class="rec-sec"><p class="rec-sec__h">Description</p><p class="rec-desc">${escapeHtml(t.description)}</p></section>`);
259
339
  }
260
340
 
261
341
  const touches = Array.isArray(t.touches) ? t.touches : [];
262
- if (touches.length) parts.push(`<p class="td-touches">✎ ${touches.map(escapeHtml).join(', ')}</p>`);
342
+ if (touches.length) {
343
+ const files = touches.map((f) => `<li>${escapeHtml(f)}</li>`).join('');
344
+ parts.push(`<section class="rec-sec"><p class="rec-sec__h">Touches <em>${touches.length} ${touches.length === 1 ? 'file' : 'files'}</em></p>` +
345
+ `<ul class="rec-files">${files}</ul></section>`);
346
+ }
263
347
 
264
348
  const deps = Array.isArray(t.dependencies) ? t.dependencies : [];
265
349
  const dependents = Array.isArray(t.dependents) ? t.dependents : [];
266
- if (deps.length) parts.push(depList('Blocked by', deps));
267
- if (dependents.length) parts.push(depList('Blocks', dependents));
350
+ if (deps.length || dependents.length) {
351
+ parts.push(`<div class="rec-sec rec-depcols">${deps.length ? depList('Blocked by', deps) : ''}${dependents.length ? depList('Blocks', dependents) : ''}</div>`);
352
+ }
268
353
 
269
354
  if (t.shipped_at) {
270
- const by = t.shipped_by ? ` by builder #${escapeHtml(String(t.shipped_by))}` : '';
271
- parts.push(`<p class="deeplink-card__note">Shipped ${escapeHtml(fmtRel(t.shipped_at))}${by}.</p>`);
355
+ const by = t.shipped_by ? ` by builder ${escapeHtml(String(t.shipped_by))}` : '';
356
+ parts.push(`<section class="rec-sec"><p class="rec-sec__h">Shipped</p>` +
357
+ `<p class="rec-body">Shipped ${escapeHtml(fmtDate(t.shipped_at))}${by}, ${escapeHtml(fmtRel(t.shipped_at))}.</p></section>`);
272
358
  }
273
359
 
274
360
  // task 759: lifecycle action from the browser (no /builder-* CLI). The server
@@ -287,17 +373,25 @@
287
373
  } else if (t.status === 'completed') {
288
374
  lifecycle = rank === 'archon'
289
375
  ? `<button class="td-btn td-btn--primary" type="button" id="confirm-btn">Confirm</button>`
290
- : `<p class="td-action-note">Awaiting the grader / a confirmation before it can ship.</p>`;
376
+ : `<p class="rec-actnote">Awaiting the grader, or a confirmation, before it can ship.</p>`;
377
+ } else if (t.status === 'shipped') {
378
+ // A shipped task has no lifecycle control left, so the row says so in words
379
+ // rather than leaving a gap a reader has to interpret. Nothing here is
380
+ // pressable-or-live, so nothing here is warm — the One Warm Thing Rule.
381
+ lifecycle = `<p class="rec-actnote">This task has shipped and its credits have landed. Neither Ship it nor Confirm is available.</p>`;
291
382
  }
292
383
 
293
384
  // Copy prompt (the "ship link") — the same claim/build/ship prompt the Work
294
385
  // Board hands an agent. Purely client-side (clipboard write, no server call).
295
386
  parts.push(`
296
- <div class="td-actions">
297
- ${lifecycle}
298
- <button class="td-btn" type="button" id="copy-prompt-btn">Copy prompt</button>
299
- <a class="td-btn" href="${hallBase()}/work">Open the Work Board</a>
300
- </div>
387
+ <section class="rec-sec">
388
+ <p class="rec-sec__h">Actions</p>
389
+ <div class="rec-actions">
390
+ ${lifecycle}
391
+ <button class="td-btn" type="button" id="copy-prompt-btn">Copy prompt</button>
392
+ <a class="td-btn" href="${hallBase()}/work">Open the work board</a>
393
+ </div>
394
+ </section>
301
395
  `);
302
396
 
303
397
  const body = $('#task-body');
@@ -331,11 +425,16 @@
331
425
  }
332
426
  }
333
427
 
428
+ // The two honest failures. Both keep the <h1> — the empty and error renders are
429
+ // renders like any other, and a page whose only heading lives on the happy path
430
+ // has none in the states a reader is most likely to hit.
334
431
  function renderNotFound(id) {
335
432
  const body = $('#task-body');
336
433
  if (body) body.innerHTML = `
337
- <p class="deeplink-card__title" id="task-h">#${escapeHtml(String(id))} — no such task</p>
338
- <p class="deeplink-card__note">Tasks are numbered separately from GitHub pull requests, so this may be a PR rather than a GDS task.</p>
434
+ <p class="rec-kind">Task ${statusPill('no such task')}</p>
435
+ <h1 class="rec-big" id="task-h">${escapeHtml(String(id))}, no such task</h1>
436
+ <p class="rec-body">Tasks in the ledger are numbered separately from GitHub pull requests, so this may be a PR number rather than a task. The work board lists everything that does exist.</p>
437
+ <div class="rec-actions rec-sec"><a class="td-btn" href="${hallBase()}/work">Open the work board</a></div>
339
438
  `;
340
439
  const sub = $('#task-sub');
341
440
  if (sub) sub.textContent = 'Not found';
@@ -344,9 +443,13 @@
344
443
  function renderError(id) {
345
444
  const body = $('#task-body');
346
445
  if (body) body.innerHTML = `
347
- <p class="deeplink-card__title" id="task-h">Could not load #${escapeHtml(String(id))}</p>
348
- <p class="deeplink-card__note">Try reloading in a moment.</p>
446
+ <p class="rec-kind">Task ${statusPill('feed unavailable')}</p>
447
+ <h1 class="rec-big" id="task-h">Could not load ${escapeHtml(String(id))}</h1>
448
+ <p class="rec-body">The task feed did not answer. Nothing on the record has changed, only the read failed. Try again in a moment, or open the work board.</p>
449
+ <div class="rec-actions rec-sec"><a class="td-btn" href="${hallBase()}/work">Open the work board</a></div>
349
450
  `;
451
+ const sub = $('#task-sub');
452
+ if (sub) sub.textContent = 'Could not load';
350
453
  }
351
454
 
352
455
  async function load() {
@@ -0,0 +1,14 @@
1
+ {
2
+ "_": "The hall's TASK RECORD through modules/ui-design/kit/render.js (task 1003305). The record's own controls — the dependency links, the lifecycle buttons, the figure band, the way back — only exist once a task has LOADED, so the floors here are run against the hall-preview harness rather than the kit's own stub, which carries the public-landing feeds and not the hall's:\n\n node scripts/hall-preview/server.js --port 4612 --fixture-me\n node modules/ui-design/kit/render.js --states modules/hall-ui/public/task.states.json --base-in http://builders.localhost:4612 --base-out http://builders.localhost:4612\n\nRun against the kit's own stubs instead and every state renders `no such task`, which is a real state and is the `missing` one below — just not the one the re-skin changed.",
3
+ "page": "/task/2166",
4
+ "surface": "hall-ui",
5
+ "stub": { "prefix": "/builders/" },
6
+ "modeQuery": false,
7
+ "ignoreRequests": ["/api/(gds|bongos)/"],
8
+ "ignoreConsole": ["/api/(gds|bongos)/", "\b401\b", "\b404\b", "Unauthorized", "Not Found", "load failed"],
9
+ "states": {
10
+ "record": { "auth": true, "actions": [["wait", 600]], "expect": { "visible": ["#task-body", "#copy-prompt-btn"] } },
11
+ "missing": { "auth": true, "url": "/task/1003999", "actions": [["wait", 600]], "expect": { "visible": ["#task-h"] } }
12
+ },
13
+ "reducedMotion": { "auth": true, "actions": [["wait", 600]] }
14
+ }
@@ -16,21 +16,10 @@
16
16
 
17
17
  goals.html LOADS THIS SHEET TOO — a change here lands on both pages. */
18
18
 
19
- /* The board's page head, in v3's LABEL voice: the display face is saved for
20
- section headings, so the page's own name is quiet (mock: .greeting). It
21
- overrides the pre-shell .page-head h1 compat rule in style.css, which is
22
- 22px/650 and belongs to the pages nobody has re-skinned yet. */
23
- .page-head--board .page-head__h {
24
- margin: 0 0 6px;
25
- font-family: var(--label-face);
26
- font-size: 14px;
27
- font-weight: 400;
28
- letter-spacing: var(--label-space);
29
- text-transform: var(--label-case);
30
- color: var(--ink);
31
- text-shadow: var(--type-halo);
32
- }
33
- .page-head--board .page-head__sub { font-size: 14px; max-width: var(--measure); }
19
+ /* `.page-head--board` MOVED to style.css (task 1003305) when the ideas composer
20
+ became its third consumer: a modifier on the shell's own .page-head is shell
21
+ chrome, and a page sheet that only work.html and goals.html load cannot serve
22
+ a page that loads neither. Same rule, one home. */
34
23
 
35
24
  .kbd {
36
25
  display: inline-block;
@@ -288,7 +288,13 @@ const server = http.createServer((req, res) => {
288
288
  const under = p.startsWith(PREFIX) ? p.slice(PREFIX.length) : (PREFIX !== '/' && (p === PREFIX.slice(0, -1)) ? '' : p.replace(/^\//, ''));
289
289
  const asset = fileUnder(DIR, under);
290
290
  if (asset && !asset.endsWith('.html')) return sendFile(asset, res);
291
- const name = under === '' ? 'index' : under.replace(/\/$/, '').replace(/\.html$/, '');
291
+ const under2 = under.replace(/\/$/, '').replace(/\.html$/, '');
292
+ // A record page is `<page>/<id>` — the hall serves /builders/task/2166 and
293
+ // /builders/idea/12 from task.html and idea.html (serve-internal.js's two
294
+ // TASK_DETAIL_PAGE_RE / IDEA_DETAIL_PAGE_RE routes). Without this the states
295
+ // for those pages could only be rendered at a URL production never serves, and
296
+ // a page script that reads its id out of location.pathname would see none.
297
+ const name = under2 === '' ? 'index' : (fs.existsSync(path.join(DIR, under2 + '.html')) ? under2 : under2.replace(/\/[^/]+$/, ''));
292
298
  if (!name.includes('/') && fs.existsSync(path.join(DIR, name + '.html'))) {
293
299
  try { res.writeHead(200, { 'content-type': MIME['.html'], 'cache-control': 'no-store' }); return res.end(page(name + '.html').html); }
294
300
  catch (e) { res.writeHead(500, { 'content-type': 'text/plain' }); return res.end('render failed: ' + e.stack); }
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.448",
3
+ "version": "1.19.450",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.448",
9
+ "version": "1.19.450",
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.448",
3
+ "version": "1.19.450",
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",
@@ -222,11 +222,45 @@ function sendPage(res, name) {
222
222
  }
223
223
 
224
224
  app.get('/', (req, res, next) => { if (!sendPage(res, 'index.html')) next(); });
225
+ // The two RECORD pages are the hall's only two-segment page routes
226
+ // (serve-internal.js TASK_DETAIL_PAGE_RE / IDEA_DETAIL_PAGE_RE). /idea/:id was
227
+ // missing here, so the idea record could not be previewed at all — the harness
228
+ // fell through to the real gate and 302'd to sign-in (task 1003305).
225
229
  app.get(/^\/task\/\d+$/, (req, res, next) => { if (!sendPage(res, 'task.html')) next(); });
230
+ app.get(/^\/idea\/\d+$/, (req, res, next) => { if (!sendPage(res, 'idea.html')) next(); });
226
231
  app.get(/^\/([a-z-]+)$/, (req, res, next) => {
227
232
  if (!sendPage(res, `${req.params[0]}.html`)) next();
228
233
  });
229
234
 
235
+ // What the READING ROOMS read (task 1003306): the two long-form documents and
236
+ // the generated diagrams. All three live outside modules/hall-ui/public and are
237
+ // served by serve-internal.js from docs/onboarding/ — but the hall's page gate
238
+ // sits in front of them, so in the harness every one of those fetches 302'd to
239
+ // sign-in and the primer could only ever be previewed in its FAILURE state and
240
+ // the diagrams index with five broken images. Same gap task 1003305 closed for
241
+ // the record pages' own routes. Served here, ahead of the gate, from the same
242
+ // canonical files prod reads.
243
+ // NEVER hand-edit a diagram: the .svg/.png pair and assertions.json are written
244
+ // in lockstep by scripts/gds/gen-diagrams.js.
245
+ // Both prefixes, and that is not belt-and-braces. The diagrams page references
246
+ // its figures RELATIVELY (src="diagrams/01-….png"), which prod resolves under
247
+ // /builders/diagrams because the page is served at /builders/diagrams; the
248
+ // harness serves its pages at the root, so the same markup resolves at
249
+ // /diagrams. Mount one and the populated page renders five broken images.
250
+ for (const prefix of ['/builders/diagrams', '/diagrams']) {
251
+ app.use(prefix, express.static(path.join(ROOT, 'docs/onboarding/diagrams')));
252
+ }
253
+ for (const [route, file] of [
254
+ ['/builders/primer.md', 'docs/onboarding/primer.md'],
255
+ ['/builders/drachmae-and-karma.md', 'docs/onboarding/drachmae-and-karma.md'],
256
+ ]) {
257
+ app.get(route, (req, res, next) => {
258
+ const abs = path.join(ROOT, file);
259
+ if (!fs.existsSync(abs)) return next();
260
+ res.type('text/markdown; charset=utf-8').send(fs.readFileSync(abs, 'utf8'));
261
+ });
262
+ }
263
+
230
264
  // Everything else — /builders/*.css|js assets, /lib/bongos-client.js, diagram
231
265
  // PNGs — falls through to the real internal-surface mounting. Our handlers
232
266
  // above registered first, so the hall's auth gates in there never fire for
package/src/module-api.js CHANGED
@@ -33,7 +33,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
33
33
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
34
34
  // the entry to that file. Look for a version's history there, not here.
35
35
  // ---------------------------------------------------------------------------
36
- const CORE_VERSION = '1.19.448'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
36
+ const CORE_VERSION = '1.19.450'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
37
37
 
38
38
  // A namespaced logger so a module's log lines are attributable + consistent.
39
39
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -29,13 +29,17 @@ const pages = () => fs.readdirSync(HALL).filter((f) => f.endsWith('.html'));
29
29
  // rather than "predates the program" — knowing WHY is what tells the next
30
30
  // reader whether promoting it would actually fix anything.
31
31
  const INLINE_STYLE_BUDGET = {
32
- 'task.html': 27, // the task-751 detail-page block, deliberate
32
+ // (task.html's 27-line task-751 block was the last substantive inline style in
33
+ // the hall. Task 1003305 promoted it to task.css + record.css when the re-skin
34
+ // needed room the ratchet would not give, so the exception is gone with it —
35
+ // task.html now carries none and passes on the default.)
33
36
  // (my-projects + scouting were carried here as standalone surfaces until task
34
37
  // 1002868 adopted them into the shell; their exceptions are gone with them.)
35
- // Genuinely page-local, and already token-derived. One line over the default;
36
- // a whole stylesheet for the legend strip is not worth the extra request, and
37
- // diagram-viewer.css is shared with primer.html so it is the wrong home.
38
- 'diagrams.html': 16,
38
+ // (diagrams.html's 16-line legend + figure block was the hall's last inline
39
+ // style. Task 1003306 promoted it: the figure frame became the reading-room
40
+ // archetype's, shared with the primer, and the legend got diagrams.css — so
41
+ // the "not worth an extra request" reasoning stopped holding the moment the
42
+ // page needed a sheet anyway. No page carries an exception now.)
39
43
  };
40
44
  const DEFAULT_INLINE_BUDGET = 15;
41
45
 
@@ -147,18 +151,21 @@ test('the phone-width overflow guards are all still in place', () => {
147
151
  // top level, so finding one means the nearest @media is not the enclosing
148
152
  // one and the rule is really live everywhere.
149
153
  assert.ok(!/\n\}/.test(between), `${re} sits AFTER its nearest @media closed — it is live at every width`);
150
- // Pin the EXACT breakpoint, not a band: style.css guards at 640, primer.css
151
- // at 760 (its only breakpoint, the one primer.js's matchMedia mirrors).
152
- // Narrowing primer's to 640 "for consistency" would silently reopen the
153
- // 641–760px band the rule exists to cover, so a tolerant regex would hide
154
- // the very drift this test is here to catch.
154
+ // Pin the EXACT breakpoint, not a band: style.css guards at 640, the
155
+ // reading room at 760 (its only breakpoint, the one primer.js's matchMedia
156
+ // mirrors). Narrowing the room's to 640 "for consistency" would silently
157
+ // reopen the 641–760px band the rule exists to cover, so a tolerant regex
158
+ // would hide the very drift this test is here to catch.
159
+ // The room's rule moved sheets in task 1003306 — primer.css → room.css,
160
+ // when the archetype was factored out for the diagrams index to share. The
161
+ // ASSERTION is unchanged; only the file it reads moved with the rule.
155
162
  assert.match(between, new RegExp(`max-width:\\s*${breakpoint}px`),
156
163
  `${re} must sit in the max-width: ${breakpoint}px media query`);
157
164
  };
158
165
  scoped(css, /:has\(>\s*\.ledger\)\s*\{[^}]*overflow-x:\s*auto/, 640);
159
166
  scoped(css, /\.ledger th\s*\{[^}]*position:\s*static/, 640);
160
167
  scoped(css, /\.rank-tip\s*\{\s*display:\s*none/, 640);
161
- scoped(read('primer.css'), /\.primer-article table\s*\{[^}]*overflow-x:\s*auto/, 760);
168
+ scoped(read('room.css'), /\.primer-article table\s*\{[^}]*overflow-x:\s*auto/, 760);
162
169
  });
163
170
 
164
171
  // The strip has to hold BOTH invariants, and the second one is the easy one to