@bongos/core 1.19.918 → 1.19.920

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 (57) hide show
  1. package/.bongos-core.json +88 -58
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +6 -0
  4. package/clients/bongos-client/index.cjs +6 -0
  5. package/clients/bongos-client/index.d.ts +8 -0
  6. package/clients/bongos-client/index.mjs +6 -0
  7. package/docs/adr/0319-a-removal-verdict-is-a-row-beside-the-question.md +69 -0
  8. package/docs/adr/README.md +1 -0
  9. package/docs/api/openapi.json +134 -3
  10. package/docs/api-reference.md +8 -1
  11. package/docs/copy-inventory.md +15 -11
  12. package/docs/copy-registry.json +48 -12
  13. package/docs/module-api-changelog.md +4 -0
  14. package/modules/hall-ui/public/atlas.html +1 -1
  15. package/modules/hall-ui/public/blockers.html +1 -1
  16. package/modules/hall-ui/public/board-room.html +1 -1
  17. package/modules/hall-ui/public/collab.html +1 -1
  18. package/modules/hall-ui/public/copy-desk.html +1 -1
  19. package/modules/hall-ui/public/deploy.html +1 -1
  20. package/modules/hall-ui/public/diagrams.html +1 -1
  21. package/modules/hall-ui/public/drachmae.html +1 -1
  22. package/modules/hall-ui/public/gate.html +1 -1
  23. package/modules/hall-ui/public/goal-inbox.js +102 -0
  24. package/modules/hall-ui/public/goals.html +1 -1
  25. package/modules/hall-ui/public/government.html +1 -1
  26. package/modules/hall-ui/public/idea.html +1 -1
  27. package/modules/hall-ui/public/ideas.html +1 -1
  28. package/modules/hall-ui/public/index.html +1 -1
  29. package/modules/hall-ui/public/modules.html +1 -1
  30. package/modules/hall-ui/public/people.html +1 -1
  31. package/modules/hall-ui/public/primer.html +1 -1
  32. package/modules/hall-ui/public/profile.html +1 -1
  33. package/modules/hall-ui/public/project-settings.html +1 -1
  34. package/modules/hall-ui/public/ranks.html +1 -1
  35. package/modules/hall-ui/public/roadmap.html +1 -1
  36. package/modules/hall-ui/public/roster.html +1 -1
  37. package/modules/hall-ui/public/sessions.html +1 -1
  38. package/modules/hall-ui/public/settings.html +1 -1
  39. package/modules/hall-ui/public/studio.html +1 -1
  40. package/modules/hall-ui/public/style.css +12 -0
  41. package/modules/hall-ui/public/task.html +1 -1
  42. package/modules/hall-ui/public/watch.html +1 -1
  43. package/modules/hall-ui/public/work.html +1 -1
  44. package/modules/lifecycle/cascade.js +9 -1
  45. package/modules/lifecycle/migrations/lifecycle_009_removal_verdicts.sql +111 -0
  46. package/modules/lifecycle/module.json +2 -1
  47. package/modules/lifecycle/removal-proposals.js +259 -0
  48. package/modules/lifecycle/routes/removal-proposals.js +140 -0
  49. package/package-lock.json +2 -2
  50. package/package.json +1 -1
  51. package/scripts/hall-preview/refresh-fixtures.js +6 -0
  52. package/src/bongos/routes/me.js +9 -1
  53. package/src/module-api.js +1 -1
  54. package/tests/blockers_hall.mjs +16 -2
  55. package/tests/core_update_banner.mjs +28 -0
  56. package/tests/nav_permission_atoms.mjs +110 -0
  57. package/tests/removal_verdict.mjs +311 -0
@@ -55,7 +55,7 @@
55
55
 
56
56
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
57
57
 
58
- <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
58
+ <script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
59
59
  <script src="/builders/palette.js" defer></script>
60
60
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
61
61
  <script src="/builders/hall-kit.js"></script>
@@ -97,7 +97,7 @@
97
97
 
98
98
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
99
99
  time so the chrome exists before the deferred page scripts execute. -->
100
- <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
100
+ <script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
101
101
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
102
102
  <script src="/builders/palette.js" defer></script>
103
103
  <script src="/builders/diagram-viewer.js?v=2026-06-14-diagviewer-1" defer></script>
@@ -98,7 +98,7 @@
98
98
 
99
99
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
100
100
 
101
- <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
101
+ <script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
102
102
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
103
103
  <script src="/builders/palette.js" defer></script>
104
104
  <script src="/builders/md-reader.js?v=2026-09-02-mdreader-2"></script>
@@ -76,7 +76,7 @@
76
76
  <div id="gate-modal-root"></div>
77
77
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
78
78
 
79
- <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
79
+ <script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
80
80
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
81
81
  <script src="/builders/palette.js" defer></script>
82
82
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -191,6 +191,20 @@
191
191
  return;
192
192
  }
193
193
 
194
+ // task 1004095 (BV2.R16): confirming or dismissing a removal proposal. Like
195
+ // the two branches above it refreshes ITS OWN slice only — repainting the
196
+ // goals list would leave the answered row sitting there still asking (the
197
+ // task-1003066 lesson). The verdict is server-declared on the button rather
198
+ // than inferred from which one was pressed, so this branch needs to know
199
+ // nothing about the vocabulary beyond what to say afterwards.
200
+ const removal = e.target.closest('[data-removal-task]');
201
+ if (removal) {
202
+ const verdict = removal.dataset.removalVerdict;
203
+ act(() => postJSON(`${API}/removal-proposals/${encodeURIComponent(removal.dataset.removalTask)}/decide`, { verdict }),
204
+ REMOVAL_TOASTS[verdict] || 'Answered.', loadRemovals);
205
+ return;
206
+ }
207
+
194
208
  const item = e.target.closest('.ginbox__item');
195
209
  const goalId = item && item.dataset.goal;
196
210
  if (!goalId) return;
@@ -275,6 +289,86 @@
275
289
  .catch(() => { /* a non-triager's 403, or a hiccup: never break the hall */ });
276
290
  }
277
291
 
292
+ // ---- removal proposals (task 1004095, BV2.R16) ----------------------------
293
+ //
294
+ // A ship that deleted files an earlier SHIPPED task put there files a QUESTION,
295
+ // never a verdict (task 1004090, ADR 0313): refactors, renames and file moves
296
+ // all look like deletions to a diff, so nothing is marked removed until a person
297
+ // says so. This slice is where they say so.
298
+ //
299
+ // WHY IT IS NOT A ROW IN THE NOD QUEUE, which is the obvious place. That queue is
300
+ // scoped to ROUTED FILINGS — `origin='route'` in ideas_task_lineage — and ADR 0299
301
+ // decided, on live numbers, that it stays that way and is honestly named for that
302
+ // population. A removal proposal has no idea lineage at all. Same CARD (the
303
+ // anti-accretion rule at the top of this file), same floor, separate slice.
304
+ //
305
+ // THE ANSWER IS NOT IN THIS ROW, and that is deliberate. Which files went and
306
+ // whose work they were is prose in the proposal's body, because the cascade that
307
+ // files it has no field for a target (ADR 0242). So the title LINKS to the task
308
+ // and the buttons sit beside it: a person who has not read the evidence should
309
+ // have to take one more step, not one fewer.
310
+ //
311
+ // No client-side rank gate, for the same reason the nod slice has none: GET
312
+ // /removal-proposals is Metic+ server-side, so a non-triager's fetch 403s and the
313
+ // slice renders nothing. One gate, on the server.
314
+ // What each answer says when it lands. Keyed by the SERVER's verdict name, the
315
+ // ROT_TOASTS convention, so a verdict added later degrades to a neutral
316
+ // confirmation instead of a blank toast. The wording states the CONSEQUENCE,
317
+ // because that is the half a person cannot see from the row they just cleared.
318
+ const REMOVAL_TOASTS = {
319
+ confirmed: 'Confirmed — the ideas behind that work will dim.',
320
+ dismissed: 'Dismissed — nothing dims.',
321
+ };
322
+
323
+ // THE LABELS ARE ONE WORD EACH BECAUSE THE ROW IS A FLEX LINE AT EVERY WIDTH.
324
+ // `.ginbox__actions` is `flex: 0 0 auto` and nothing in the card wraps, so two
325
+ // buttons take their width straight off the body’s. Measured at 375px:
326
+ // "Confirm removal" + "Dismiss" left the title 46px wide and 440px tall — a
327
+ // column of single letters — where every other row on this card keeps 138–218px.
328
+ // One word each brings it back into that band; the sentence each answer means
329
+ // rides the `title`, and the head already says what the question is.
330
+ function removalRowHtml(r) {
331
+ const title = escapeHtml(r.title || `task #${r.proposal_task_id}`);
332
+ const from = r.ship_task_id
333
+ ? `deleted by task #${escapeHtml(String(r.ship_task_id))}`
334
+ : 'from a ship whose id could not be read';
335
+ const goal = r.goal_title ? ` · ${escapeHtml(r.goal_title)}` : '';
336
+ return `<li class="ginbox__item ginbox__item--stacked">
337
+ <div class="ginbox__body">
338
+ <a class="ginbox__goal" href="/task/${encodeURIComponent(r.proposal_task_id)}">${title}</a>
339
+ <span class="ginbox__sub">${from}${goal} — open it to see what went</span>
340
+ </div>
341
+ <div class="ginbox__actions">
342
+ <button class="btn-ghost" type="button" data-removal-task="${escapeHtml(String(r.proposal_task_id))}"
343
+ data-removal-verdict="confirmed" title="That work is genuinely gone">Confirm</button>
344
+ <button class="btn-ghost" type="button" data-removal-task="${escapeHtml(String(r.proposal_task_id))}"
345
+ data-removal-verdict="dismissed" title="A refactor, a move, or work already superseded">Dismiss</button>
346
+ </div>
347
+ </li>`;
348
+ }
349
+
350
+ function renderRemovals(data) {
351
+ const box = host && host.querySelector('#attention-removals');
352
+ if (!box) return;
353
+ const rows = (data && Array.isArray(data.pending) ? data.pending : []);
354
+ const count = data && data.count != null ? data.count : rows.length;
355
+ if (!rows.length) {
356
+ box.innerHTML = '';
357
+ } else {
358
+ const more = count > 5 ? `<p class="ginbox__pending">…${count - 5} more awaiting an answer.</p>` : '';
359
+ box.innerHTML = '<h3 class="ginbox__sec-h">Was this removed? '
360
+ + `<span class="count-pill">${count}</span></h3>`
361
+ + `<ul class="ginbox__list">${rows.slice(0, 5).map(removalRowHtml).join('')}</ul>${more}`;
362
+ }
363
+ window.OTBAttention?.refresh();
364
+ }
365
+
366
+ function loadRemovals() {
367
+ return getJSON(`${API}/removal-proposals`)
368
+ .then((data) => renderRemovals(data))
369
+ .catch(() => { /* a non-triager's 403, or an older server: never break the hall */ });
370
+ }
371
+
278
372
  // ---- the rot queue (task 1003279, goal 1000072) ---------------------------
279
373
  //
280
374
  // "Untouched-for-X triggers a configured action rather than a silent decay."
@@ -484,6 +578,13 @@
484
578
  // triage. Its rows carry the shared .ginbox__item class, so refresh()
485
579
  // counts them with no change to the selector below.
486
580
  '<div id="attention-nod"></div>' +
581
+ // task 1004095 (BV2.R16): removal proposals — a ship deleted files an
582
+ // earlier shipped task put there, and somebody has to say whether that work
583
+ // is genuinely gone or it was a refactor. Straight after nod because it is
584
+ // the same act at the same floor: a Metic+ answering a question the system
585
+ // filed, inside a goal. Before blockers because a blocker is waiting on the
586
+ // owner specifically, while this is waiting on whoever looks first.
587
+ '<div id="attention-removals"></div>' +
487
588
  // task 1003455 (goal 1000089): open blockers — things stuck on the owner
488
589
  // (modules/ideas/blockers.js). AFTER nod, BEFORE rot: like every slice
489
590
  // above it, a blocker is genuinely waiting on a human, but unlike them it
@@ -499,6 +600,7 @@
499
600
  host.addEventListener('click', onClick);
500
601
  load();
501
602
  loadNodQueue();
603
+ loadRemovals();
502
604
  loadBlockers();
503
605
  loadRot();
504
606
  // task 1002802 (P06): the profile slice lives in its own file (builders.js is
@@ -220,7 +220,7 @@
220
220
 
221
221
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
222
222
 
223
- <script src="/builders/shell.js?v=2026-07-03-goals1"></script>
223
+ <script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
224
224
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
225
225
  <script src="/builders/palette.js" defer></script>
226
226
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
@@ -134,7 +134,7 @@
134
134
 
135
135
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
136
136
 
137
- <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
137
+ <script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
138
138
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
139
139
  <script src="/builders/palette.js" defer></script>
140
140
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -52,7 +52,7 @@
52
52
 
53
53
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
54
54
 
55
- <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
55
+ <script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
56
56
  <script src="/builders/palette.js" defer></script>
57
57
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib" defer></script>
58
58
  <!-- task-detail.js still owns the #/idea/N and #/blocker/N overlay, so a ref
@@ -192,7 +192,7 @@
192
192
 
193
193
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
194
194
 
195
- <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
195
+ <script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
196
196
  <script src="/builders/palette.js" defer></script>
197
197
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
198
198
  <!-- task 1003069 (R08) — the quick-filing form's routed result links a task
@@ -102,7 +102,7 @@
102
102
  <script src="/builders/brand-apply.js?v=2026-06-27-r75" defer></script>
103
103
  <!-- The app shell (sidebar + top bar + view router). Dependency-free; loaded
104
104
  BEFORE builders.js so the chrome ids exist when events are wired. -->
105
- <script src="/builders/shell.js?v=2026-07-02-shell1" defer></script>
105
+ <script src="/builders/shell.js?v=2026-09-20-coreupdate1" defer></script>
106
106
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
107
107
  <script src="/builders/palette.js" defer></script>
108
108
  <!-- task 2166: the shared component kit (window.OTBKit) — pager/ledger/rail.
@@ -80,7 +80,7 @@
80
80
 
81
81
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
82
82
 
83
- <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
83
+ <script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
84
84
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
85
85
  <script src="/builders/palette.js" defer></script>
86
86
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
@@ -55,7 +55,7 @@
55
55
 
56
56
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
57
57
  chrome exists before the page script runs. -->
58
- <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
58
+ <script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
59
59
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
60
60
  <script src="/builders/palette.js" defer></script>
61
61
  <!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
@@ -115,7 +115,7 @@
115
115
 
116
116
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
117
117
 
118
- <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
118
+ <script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
119
119
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
120
120
  <script src="/builders/palette.js" defer></script>
121
121
  <!-- the shared markdown reader (window.OTBMdReader) — primer.js renders through
@@ -187,7 +187,7 @@
187
187
 
188
188
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
189
189
  chrome exists before the page script runs. -->
190
- <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
190
+ <script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
191
191
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
192
192
  <script src="/builders/palette.js" defer></script>
193
193
  <!-- shared frontend toolkit (window.OTB) — loaded before profile.js, which pulls from it. -->
@@ -58,7 +58,7 @@
58
58
 
59
59
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
60
60
 
61
- <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
61
+ <script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
62
62
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
63
63
  <script src="/builders/palette.js" defer></script>
64
64
  <script src="/builders/dom-utils.js?v=2026-06-16-gate"></script>
@@ -68,7 +68,7 @@
68
68
 
69
69
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
70
70
  chrome exists before the page script runs. -->
71
- <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
71
+ <script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
72
72
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
73
73
  <script src="/builders/palette.js" defer></script>
74
74
  <!-- shared frontend toolkit (window.OTB) — loaded before ranks.js, which pulls from it. -->
@@ -41,7 +41,7 @@
41
41
 
42
42
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
43
43
 
44
- <script src="/builders/shell.js?v=2026-07-03-roadmap1"></script>
44
+ <script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
45
45
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
46
46
  <script src="/builders/palette.js" defer></script>
47
47
  <script src="/builders/dom-utils.js?v=2026-06-14-domutils"></script>
@@ -50,7 +50,7 @@
50
50
 
51
51
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
52
52
  chrome exists before the page script runs. -->
53
- <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
53
+ <script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
54
54
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
55
55
  <script src="/builders/palette.js" defer></script>
56
56
  <!-- shared frontend toolkit (window.OTB — rank labels, number formats) + the
@@ -99,7 +99,7 @@
99
99
 
100
100
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
101
101
  chrome exists before the page script runs. -->
102
- <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
102
+ <script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
103
103
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
104
104
  <script src="/builders/palette.js" defer></script>
105
105
  <!-- shared frontend toolkit (window.OTB) — loaded BEFORE sessions.js so it can pull from it. -->
@@ -416,7 +416,7 @@
416
416
 
417
417
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
418
418
  chrome exists before the page scripts run. -->
419
- <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
419
+ <script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
420
420
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
421
421
  <script src="/builders/palette.js" defer></script>
422
422
  <!-- shared frontend toolkit (window.OTB) — loaded before consuming IIFEs. -->
@@ -66,7 +66,7 @@
66
66
 
67
67
  <div id="toast" class="toast" role="status" aria-live="polite" data-visible="0"></div>
68
68
 
69
- <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
69
+ <script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
70
70
  <script src="/builders/palette.js" defer></script>
71
71
  <script src="/builders/dom-utils.js?v=2026-06-23-contrib"></script>
72
72
  <!-- The kit, for its ONE renderer: the full-size ship-time visual figure
@@ -3032,6 +3032,18 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
3032
3032
  .ginbox__note { margin: 3px 0 0; font-size: 12.5px; color: var(--ink-soft); font-style: italic; }
3033
3033
  .ginbox__actions { flex: 0 0 auto; display: flex; gap: 6px; }
3034
3034
  .ginbox__pending { font-size: 12.5px; margin: 10px 0 0; color: var(--ink-soft); }
3035
+ /* task 1004095: a row whose actions are TWO verbs, not one. `.ginbox__actions` is
3036
+ `flex: 0 0 auto` and the card never wraps, so a second button takes its width
3037
+ straight off the title’s — measured at 375px, the removal row’s title fell to
3038
+ 96px and 252px tall where every other row on this card keeps 138–218px. The
3039
+ modifier lets the actions drop under the words at phone width instead, which is
3040
+ the card’s own "a phone gets its own treatment, the document never widens"
3041
+ rule. Opt-in: an existing single-verb row is untouched. */
3042
+ @media (max-width: 640px) {
3043
+ .ginbox__item--stacked { flex-wrap: wrap; }
3044
+ .ginbox__item--stacked .ginbox__body { flex: 1 1 100%; }
3045
+ .ginbox__item--stacked .ginbox__actions { margin-top: 6px; }
3046
+ }
3035
3047
  /* task 1003464: the system's notices are inbox LINES on Home (rebase owed, an
3036
3048
  input the system needs, the monthly budget) — the placement rule, not a
3037
3049
  banner inside the Standing card. A row is a kind in the label voice over one
@@ -57,7 +57,7 @@
57
57
 
58
58
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
59
59
  time so the chrome exists before the deferred page scripts execute. -->
60
- <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
60
+ <script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
61
61
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
62
62
  <script src="/builders/palette.js" defer></script>
63
63
  <!-- shared frontend toolkit (window.OTB) — loaded FIRST so task.js can pull from it. -->
@@ -162,7 +162,7 @@
162
162
 
163
163
  <!-- The app shell (sidebar + top bar). Dependency-free; loaded FIRST so the
164
164
  chrome exists before the page script runs. -->
165
- <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
165
+ <script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
166
166
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
167
167
  <script src="/builders/palette.js" defer></script>
168
168
  <!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
@@ -151,7 +151,7 @@
151
151
 
152
152
  <!-- The app shell (sidebar + top bar + footer). Dependency-free; runs at parse
153
153
  time so the chrome exists before the deferred page scripts execute. -->
154
- <script src="/builders/shell.js?v=2026-07-02-shell1"></script>
154
+ <script src="/builders/shell.js?v=2026-09-20-coreupdate1"></script>
155
155
  <!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
156
156
  <script src="/builders/palette.js" defer></script>
157
157
  <!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
@@ -113,6 +113,13 @@
113
113
  // person. Kept as an explicit set rather than inferred, because "is this
114
114
  // copy/visual work" is a judgement the rule should state out loud and a test
115
115
  // should be able to pin — an inferred predicate is the kind that quietly widens.
116
+ // The removal rule's key, lifted out of its declaration so the SURFACE that
117
+ // answers a removal proposal (task 1004095 / BV2.R16) can find those proposals
118
+ // without hand-typing the string. The key is half of every generated row's
119
+ // `source_ref` (see cascadeSourceRef below), so a reader that re-types it is a
120
+ // reader that silently returns nothing the day the key changes.
121
+ const REMOVAL_RULE_KEY = 'removal-proposed-on-ship';
122
+
116
123
  const COPY_VISUAL_MODULES = Object.freeze([
117
124
  'copy-desk', // the copy registry + proposals (goal 1000074's own spine)
118
125
  'hall-ui', // the builders' hall — the most-seen surface
@@ -234,7 +241,7 @@ const CASCADES = Object.freeze([
234
241
  body: artistReviewBody,
235
242
  }),
236
243
  Object.freeze({
237
- key: 'removal-proposed-on-ship',
244
+ key: REMOVAL_RULE_KEY,
238
245
  // ENGINEER, not artist: confirming asks whether deleted code is genuinely
239
246
  // gone or was moved, which is a question about the diff.
240
247
  discipline: 'engineer',
@@ -567,6 +574,7 @@ async function runCascadesForShip(shipped, deps = {}) {
567
574
 
568
575
  module.exports = {
569
576
  CASCADES,
577
+ REMOVAL_RULE_KEY,
570
578
  removalProposalBody,
571
579
  IDEA_CASCADES,
572
580
  TRIGGERS,
@@ -0,0 +1,111 @@
1
+ -- lifecycle_009_removal_verdicts.sql — the ANSWER to a removal proposal
2
+ -- (task 1004095 / BV2.R16, goal 1000095; ADR 0313 + ADR 0319).
3
+ --
4
+ -- WHY A TABLE AT ALL, when the proposal is already a task row. Because the two
5
+ -- answers are otherwise indistinguishable. A removal proposal is a question filed
6
+ -- at `backlog` by the `removal-proposed-on-ship` cascade (task 1004090); answering
7
+ -- it either way leaves nothing to build, so either way the task ends `abandoned`.
8
+ -- "Yes, that work is gone" and "no, that was a refactor" would then be the SAME
9
+ -- row, and brightness — the live share of a descent, the next task in this chain —
10
+ -- would have no way to tell the star it must dim from the one it must not. The
11
+ -- verdict is the fact the whole feature exists to produce; it cannot live in a
12
+ -- status that has one spelling for both answers.
13
+ --
14
+ -- WHY NOT A NEW TASK STATUS. ADR 0313 rejected exactly that: `shipped` is terminal
15
+ -- by design and much of the ledger leans on it, so "a removal is a fact about
16
+ -- work's fate, recorded BESIDE it, not a new terminal state that ship rollup would
17
+ -- have to learn". This table is that beside.
18
+ --
19
+ -- WHAT IS DELIBERATELY NOT HERE: the list of tasks whose work was removed. The
20
+ -- cascade files one task per rule per ship with no field for a target (ADR 0242
21
+ -- forbids widening the runner to fan out), so that list travels as prose in the
22
+ -- proposal's body — the owner's decision of 2026-09-20, recorded in
23
+ -- cascade.js:removalProposalBody. Brightness designs its own storage for targets
24
+ -- when it needs one. What this table owes brightness is the VERDICT and a
25
+ -- structured handle on the ship it was about; both are here, and neither is prose.
26
+ --
27
+ -- MODULE-OWNED and `lifecycle_`-prefixed so fitness check 11 permits it (ADR 0083
28
+ -- §Decision #5) — the lifecycle_004..008 convention. It only REFERENCES the core
29
+ -- `tasks` / `builders` tables; a module migration may not ALTER them.
30
+ --
31
+ -- Module migrations are ADDITIVE and NAMESPACED: no down-migration, idempotent
32
+ -- (safe to re-run).
33
+
34
+ BEGIN;
35
+
36
+ CREATE TABLE IF NOT EXISTS lifecycle_removal_verdicts (
37
+ -- The PROPOSAL task — the question that was answered. PRIMARY KEY, not just an
38
+ -- index: one proposal has exactly one answer, ever. A second decide is refused
39
+ -- at the route rather than upserted, because silently overwriting a person's
40
+ -- verdict would lose the only record that they gave a different one.
41
+ proposal_task_id bigint PRIMARY KEY REFERENCES tasks(id) ON DELETE CASCADE,
42
+
43
+ -- The SHIP the proposal was about — the task whose merge deleted the files.
44
+ -- Denormalised out of the proposal's `source_ref`, which encodes it as
45
+ -- `cascade-removal-proposed-on-ship-<id>`: a string nothing can join on, the
46
+ -- same complaint ideas_007 makes about the same field. Nullable because a
47
+ -- proposal whose ref cannot be parsed must still be answerable — the verdict is
48
+ -- the point, and refusing to record it over a malformed ref would be the tail
49
+ -- wagging the dog.
50
+ ship_task_id bigint REFERENCES tasks(id) ON DELETE SET NULL,
51
+
52
+ -- The answer. An ALLOW-LIST, not a deny-list: a value added to the vocabulary
53
+ -- later has to be added here too, rather than being admitted by default.
54
+ -- 'confirmed' — the work is genuinely gone. This is what brightness reads.
55
+ -- 'dismissed' — a refactor, a move the rename detection missed, or work
56
+ -- already superseded. Nothing dims.
57
+ verdict text NOT NULL CHECK (verdict IN ('confirmed', 'dismissed')),
58
+
59
+ -- WHO answered, and WHEN. A verdict with no author is a rumour: this is the
60
+ -- record that a PERSON decided, which is the entire difference between this
61
+ -- design and the derived-only one ADR 0313 rejected. Nullable only so a
62
+ -- deleted builder does not take the verdict with them.
63
+ decided_by bigint REFERENCES builders(id) ON DELETE SET NULL,
64
+ decided_at timestamptz NOT NULL DEFAULT now(),
65
+
66
+ -- Optional free text. Not NOT NULL, unlike lifecycle_008's `reason`: there the
67
+ -- row exists only when somebody overrode a rule and the "why" is the whole
68
+ -- record, whereas here the common case is an obvious yes or no and demanding
69
+ -- prose for it would only breed empty ceremony.
70
+ note text
71
+ );
72
+
73
+ -- Brightness reads the CONFIRMED rows and only those, so the partial index is the
74
+ -- shape of the one hot query rather than a general-purpose one on a two-value
75
+ -- column, which would never be worth its write cost.
76
+ CREATE INDEX IF NOT EXISTS idx_lifecycle_removal_verdicts_confirmed
77
+ ON lifecycle_removal_verdicts (ship_task_id)
78
+ WHERE verdict = 'confirmed';
79
+
80
+ -- SAFETY NET — a migration that cannot prove its own effect must not report
81
+ -- success (the lifecycle_007 / ideas_007 precedent). Read-only, no DDL. If the
82
+ -- table came back without the single-column primary key, one proposal could carry
83
+ -- two contradictory verdicts with nothing to arbitrate; if the CHECK is missing,
84
+ -- a typo'd verdict would be stored and read as neither answer.
85
+ DO $$
86
+ DECLARE pk_cols text; has_verdict_check boolean;
87
+ BEGIN
88
+ SELECT string_agg(a.attname, ',' ORDER BY a.attname) INTO pk_cols
89
+ FROM pg_constraint c
90
+ JOIN unnest(c.conkey) AS k(attnum) ON true
91
+ JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum
92
+ WHERE c.conrelid = 'lifecycle_removal_verdicts'::regclass
93
+ AND c.contype = 'p';
94
+ IF pk_cols IS DISTINCT FROM 'proposal_task_id' THEN
95
+ RAISE EXCEPTION
96
+ 'lifecycle_009: lifecycle_removal_verdicts primary key is (%) — expected (proposal_task_id), so one proposal holds one verdict.', COALESCE(pk_cols, 'none');
97
+ END IF;
98
+
99
+ SELECT EXISTS (
100
+ SELECT 1 FROM pg_constraint
101
+ WHERE conrelid = 'lifecycle_removal_verdicts'::regclass
102
+ AND contype = 'c'
103
+ AND pg_get_constraintdef(oid) ILIKE '%verdict%'
104
+ ) INTO has_verdict_check;
105
+ IF NOT has_verdict_check THEN
106
+ RAISE EXCEPTION
107
+ 'lifecycle_009: the verdict CHECK is missing — an unrecognised verdict would store as neither answer.';
108
+ END IF;
109
+ END $$;
110
+
111
+ COMMIT;
@@ -26,7 +26,8 @@
26
26
  "kickoff-seed",
27
27
  "rot",
28
28
  "project-settings",
29
- "artist-gate"
29
+ "artist-gate",
30
+ "removal-proposals"
30
31
  ],
31
32
  "pollers": [
32
33
  "publish-reconciler"