@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.
- package/.bongos-core.json +88 -58
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +8 -0
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/adr/0319-a-removal-verdict-is-a-row-beside-the-question.md +69 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +134 -3
- package/docs/api-reference.md +8 -1
- package/docs/copy-inventory.md +15 -11
- package/docs/copy-registry.json +48 -12
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-inbox.js +102 -0
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/people.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +12 -0
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/lifecycle/cascade.js +9 -1
- package/modules/lifecycle/migrations/lifecycle_009_removal_verdicts.sql +111 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/removal-proposals.js +259 -0
- package/modules/lifecycle/routes/removal-proposals.js +140 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/hall-preview/refresh-fixtures.js +6 -0
- package/src/bongos/routes/me.js +9 -1
- package/src/module-api.js +1 -1
- package/tests/blockers_hall.mjs +16 -2
- package/tests/core_update_banner.mjs +28 -0
- package/tests/nav_permission_atoms.mjs +110 -0
- 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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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:
|
|
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;
|