@bongos/core 1.19.879 → 1.19.881
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 +39 -34
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/api/openapi.json +0 -4
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +220 -220
- package/docs/copy-registry.json +263 -263
- package/docs/module-api-changelog.md +4 -0
- package/docs/project-context.template.md +2 -2
- package/modules/hall-ui/public/dom-utils.js +18 -2
- package/modules/hall-ui/public/goals-render.js +8 -3
- package/modules/hall-ui/public/primer.js +0 -1
- package/modules/hall-ui/public/roadmap.css +0 -11
- package/modules/hall-ui/public/roadmap.js +7 -15
- package/modules/hall-ui/public/task.js +6 -2
- package/modules/hall-ui/public/work.js +4 -2
- package/modules/lifecycle/db-versions.js +17 -13
- package/modules/lifecycle/routes/versions.js +1 -11
- package/modules/lifecycle/version-autoclose.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/init.js +0 -3
- package/scripts/gds/provision-repo.js +5 -4
- package/src/module-api.js +1 -1
- package/tests/hall_record_world.mjs +5 -0
- package/tests/init.mjs +15 -12
- package/tests/kind_axis_unset.mjs +195 -0
- package/tests/provision.mjs +8 -3
- package/tests/version_first_goal.mjs +4 -2
- package/tests/versions_route.mjs +37 -12
|
@@ -2232,5 +2232,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2232
2232
|
landed since 1.19.877 with no explicit bump. run 35439004814. (task 1002620)
|
|
2233
2233
|
1.19.879 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2234
2234
|
landed since 1.19.878 with no explicit bump. run 35439529685. (task 1002620)
|
|
2235
|
+
1.19.880 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2236
|
+
landed since 1.19.879 with no explicit bump. run 35453118533. (task 1002620)
|
|
2237
|
+
1.19.881 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2238
|
+
landed since 1.19.880 with no explicit bump. run 35455207976. (task 1002620)
|
|
2235
2239
|
---------------------------------------------------------------------------
|
|
2236
2240
|
```
|
|
@@ -21,8 +21,8 @@ Keep it durable framing, not live state (live state belongs in the build platfor
|
|
|
21
21
|
do not apply to this project. Delete this comment block when done.
|
|
22
22
|
-->
|
|
23
23
|
|
|
24
|
-
<One paragraph: what this product is, who it's for, and the single idea that makes it distinct.
|
|
25
|
-
|
|
24
|
+
<One paragraph: what this product is, who it's for, and the single idea that makes it distinct. How work is
|
|
25
|
+
sorted — versions, and the role each task carries — is framed in [`CLAUDE.md`](../CLAUDE.md) §8.>
|
|
26
26
|
|
|
27
27
|
---
|
|
28
28
|
|
|
@@ -146,7 +146,10 @@
|
|
|
146
146
|
for (const k of list) {
|
|
147
147
|
const opt = document.createElement('option');
|
|
148
148
|
opt.value = k;
|
|
149
|
-
|
|
149
|
+
// The unset bucket is named for a reader on EVERY surface, filters included
|
|
150
|
+
// — `label` is the caller's choice for the real kinds, never for this one
|
|
151
|
+
// (task 1004038). roleOptions() names the role twin the same way.
|
|
152
|
+
opt.textContent = kindIsUnset(k) ? kindLabel(k) : label(k);
|
|
150
153
|
sel.appendChild(opt);
|
|
151
154
|
}
|
|
152
155
|
// A refill must not drop the reader's pick; `selected` is the filing default.
|
|
@@ -154,10 +157,23 @@
|
|
|
154
157
|
if (want) sel.value = want;
|
|
155
158
|
}
|
|
156
159
|
|
|
160
|
+
// A task with no kind carries the DB default 'unclassified' — the twin of
|
|
161
|
+
// ROLE_UNSET one axis over (task 1004038, filed by task 1003936 which fixed the
|
|
162
|
+
// role axis and left this one). A word a column chose is not a word a person
|
|
163
|
+
// should read (docs/voice.md V2), so a picker shows "No kind yet" and a row tag
|
|
164
|
+
// renders NOTHING. kindIsUnset() is how a caller asks. The STORED value and the
|
|
165
|
+
// filter VALUE stay 'unclassified'; only what is read changes.
|
|
166
|
+
const KIND_UNSET = 'unclassified';
|
|
167
|
+
function kindIsUnset(k) { return !k || String(k) === KIND_UNSET; }
|
|
168
|
+
|
|
169
|
+
// NOT ESCAPE-SAFE: like roleLabel() below, this returns a transformed copy of
|
|
170
|
+
// its input, so a caller rendering it into markup must escapeHtml() the result.
|
|
171
|
+
// Every caller today assigns it to textContent, which escapes for them.
|
|
157
172
|
// How a kind is named in a picker — 'blocker-resolution' → 'Blocker resolution'.
|
|
158
173
|
// The same shape roleLabel takes below, and it reproduces every label the
|
|
159
174
|
// hand-written <option> list in ideas.html carried, so no copy changes.
|
|
160
175
|
function kindLabel(k) {
|
|
176
|
+
if (kindIsUnset(k)) return 'No kind yet';
|
|
161
177
|
const s = String(k || '').replace(/-/g, ' ');
|
|
162
178
|
return s ? s.charAt(0).toUpperCase() + s.slice(1) : '';
|
|
163
179
|
}
|
|
@@ -407,7 +423,7 @@
|
|
|
407
423
|
escapeHtml, $, fmtNum, num, fmtInt, fmtCompact, fmtDate, rankLabel, toast, moduleOn, disciplines,
|
|
408
424
|
// task 1003936 — ONE word for the role axis, all six surfaces
|
|
409
425
|
roleAxisLabel, roleLabel, roleOptions, roleIsUnset, ROLE_UNSET,
|
|
410
|
-
fillKindFilter, kindLabel, // task 1003453 — one Kind filler, all three surfaces
|
|
426
|
+
fillKindFilter, kindLabel, kindIsUnset, KIND_UNSET, // task 1003453 — one Kind filler, all three surfaces
|
|
411
427
|
errCode, errMessage, errDetails, // task 1002462 — ADR 0116 envelope readers
|
|
412
428
|
contributeSettingsPanel, settingsPanels,
|
|
413
429
|
contributeHallWidget, hallWidgets, hallWidgetIds,
|
|
@@ -321,6 +321,11 @@
|
|
|
321
321
|
const disciplineTag = window.OTB.roleIsUnset(t.discipline)
|
|
322
322
|
? ''
|
|
323
323
|
: `<span class="task__tag">${escapeHtml(window.OTB.roleLabel(t.discipline))}</span>`;
|
|
324
|
+
// A kind nobody set is no tag at all, not the column's default word — the
|
|
325
|
+
// role tag above is the precedent this follows (task 1004038).
|
|
326
|
+
const kindTag = window.OTB.kindIsUnset(t.kind)
|
|
327
|
+
? ''
|
|
328
|
+
: `<span class="task__tag">${escapeHtml(t.kind)}</span>`;
|
|
324
329
|
const newcomerTag = t.newcomer_friendly ? '<span class="task__tag task__tag--newcomer">newcomer</span>' : '';
|
|
325
330
|
const rankTag = rankTagHtml(t.requires_rank);
|
|
326
331
|
const touches = Array.isArray(t.touches) ? t.touches : [];
|
|
@@ -361,7 +366,7 @@
|
|
|
361
366
|
<p class="task__title"><a class="task__link" href="${escapeHtml(taskHref(id))}">${escapeHtml(t.title || '')}</a></p>
|
|
362
367
|
<div class="task__meta">
|
|
363
368
|
<span class="task__tag">${escapeHtml(t.version_id || '?')}</span>
|
|
364
|
-
|
|
369
|
+
${kindTag}
|
|
365
370
|
${disciplineTag}
|
|
366
371
|
${newcomerTag}
|
|
367
372
|
${rankTag}
|
|
@@ -403,7 +408,7 @@
|
|
|
403
408
|
<tr class="ledger__row--task" data-task-id="${escapeHtml(String(t.id))}" style="cursor:pointer" title="Open task #${escapeHtml(String(t.id))}">
|
|
404
409
|
<td class="ledger__num">#${escapeHtml(String(t.id))}</td>
|
|
405
410
|
<td>${escapeHtml(t.title || '')}</td>
|
|
406
|
-
<td>${
|
|
411
|
+
<td>${window.OTB.kindIsUnset(t.kind) ? '—' : escapeHtml(t.kind)}</td>
|
|
407
412
|
<td class="ledger__drachmae">${fmtNum(creditVal(t))}${taskSettled(t) ? '' : ' <span class="ledger__est" title="Estimate — credits settle when the task is confirmed">est.</span>'}</td>
|
|
408
413
|
<td class="ledger__date">${fmtDate(t.shipped_at)}</td>
|
|
409
414
|
</tr>`).join('');
|
|
@@ -502,7 +507,7 @@
|
|
|
502
507
|
const sortBy = $('#f-goal-sort')?.value || 'cpm';
|
|
503
508
|
let list = state.work.todo.filter((t) => {
|
|
504
509
|
if (!matchesTodoSearch(t, sQuery)) return false;
|
|
505
|
-
if (kFilter && (t.kind ||
|
|
510
|
+
if (kFilter && (t.kind || window.OTB.KIND_UNSET) !== kFilter) return false;
|
|
506
511
|
if (dFilter && (t.discipline || window.OTB.ROLE_UNSET) !== dFilter) return false;
|
|
507
512
|
if (nFilter === 'yes' && !t.newcomer_friendly) return false;
|
|
508
513
|
return true;
|
|
@@ -42,7 +42,6 @@
|
|
|
42
42
|
// doesn't pick up the wrong picture. The image paths resolve at
|
|
43
43
|
// /builders/diagrams/* (mounted in server.js).
|
|
44
44
|
const DIAGRAM_AT_HEADING = [
|
|
45
|
-
{ match: 'two tracks', src: '/builders/diagrams/05-two-track-versioning.png', alt: 'The two tracks: product (the game) and internal (the dev system)' },
|
|
46
45
|
{ match: 'claim → ship lifecycle', src: '/builders/diagrams/01-task-lifecycle.png', alt: 'The full task lifecycle from backlog through shipped' },
|
|
47
46
|
{ match: 'rank ladder', src: '/builders/diagrams/02-rank-ladder.png', alt: 'The rank ladder — the ranks a builder can hold and how to rise' },
|
|
48
47
|
{ match: 'drachmae and karma', src: '/builders/diagrams/03-drachmae-karma.png', alt: 'How the reward currency and karma differ — pay for shipped work vs. reputation signals' },
|
|
@@ -38,17 +38,6 @@ body[data-page="roadmap"] {
|
|
|
38
38
|
/* ---- shared atoms ----
|
|
39
39
|
The chips are the deck's label voice: they name a KIND, so they take the
|
|
40
40
|
label tokens rather than a hand-written uppercase of their own. */
|
|
41
|
-
.rm-chip { display:inline-flex; align-items:center; gap:5px;
|
|
42
|
-
font-family:var(--label-face); font-size:var(--label-size); font-weight:400;
|
|
43
|
-
letter-spacing:var(--label-space); text-transform:var(--label-case);
|
|
44
|
-
padding:4px 9px; border-radius:var(--radius-pill); white-space:nowrap; }
|
|
45
|
-
.rm-chip .rm-sq { width:7px; height:7px; border-radius:2px; }
|
|
46
|
-
.rm-chip--product { background:var(--accent-tint); color:var(--accent-ink); }
|
|
47
|
-
.rm-chip--product .rm-sq { background:var(--accent); }
|
|
48
|
-
.rm-chip--internal { background:var(--info-weak); color:var(--info); }
|
|
49
|
-
.rm-chip--internal .rm-sq { background:var(--sea); }
|
|
50
|
-
.rm-chip--dogfood { background:var(--rm-terra-weak); color:color-mix(in srgb, var(--terracotta) 82%, var(--ink)); }
|
|
51
|
-
.rm-chip--dogfood .rm-sq { background:var(--terracotta); }
|
|
52
41
|
|
|
53
42
|
.rm-phase { display:inline-flex; align-items:center; gap:6px; font-size:11.5px; font-weight:500;
|
|
54
43
|
padding:4px 10px; border-radius:var(--radius-pill); white-space:nowrap; border:var(--line) solid transparent; }
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// links to.
|
|
14
14
|
//
|
|
15
15
|
// Reads (all GET): /public/progress (lifecycle rollup + counts + %, public),
|
|
16
|
-
// /public/versions (
|
|
16
|
+
// /public/versions (started/shipped dates, public), and — for every
|
|
17
17
|
// non-shipped version only — /goals?version=X + /goals/:id (per-goal criteria to
|
|
18
18
|
// split in-progress from not-started). Shipped versions render as a pill strip
|
|
19
19
|
// with no goal fetch. No writes. Server page gate: SIGNEDIN_BUILDERS_PAGE_RE.
|
|
@@ -37,11 +37,6 @@
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
40
|
-
const TRACK = {
|
|
41
|
-
product: { label: 'Product', cls: 'product' },
|
|
42
|
-
internal: { label: 'Platform', cls: 'internal' },
|
|
43
|
-
default: { label: 'Dogfood', cls: 'dogfood' },
|
|
44
|
-
};
|
|
45
40
|
const PHASE = {
|
|
46
41
|
building: { label: 'In flight', cls: 'building' },
|
|
47
42
|
planning: { label: 'Staged', cls: 'planning' },
|
|
@@ -60,9 +55,7 @@
|
|
|
60
55
|
|
|
61
56
|
const pctOf = (n, d) => (d > 0 ? (n / d) * 100 : 0);
|
|
62
57
|
const ym = (s) => { if (!s) return ''; const d = new Date(s); return Number.isNaN(d.getTime()) ? '' : `${MON[d.getMonth()]} ${d.getFullYear()}`; };
|
|
63
|
-
const trackOf = (v) => TRACK[v.track] || TRACK.internal;
|
|
64
58
|
|
|
65
|
-
const chip = (v) => { const t = trackOf(v); return `<span class="rm-chip rm-chip--${t.cls}"><span class="rm-sq"></span>${t.label}</span>`; };
|
|
66
59
|
const phase = (v) => { const p = PHASE[v.status]; return p ? `<span class="rm-phase rm-phase--${p.cls}"><span class="rm-pd"></span>${p.label}</span>` : ''; };
|
|
67
60
|
|
|
68
61
|
function ring(v) {
|
|
@@ -162,7 +155,7 @@
|
|
|
162
155
|
<div class="rm-featgrid__l">
|
|
163
156
|
<div class="rm-feat-top">
|
|
164
157
|
<div class="rt">
|
|
165
|
-
<div class="rm-row">${
|
|
158
|
+
<div class="rm-row">${phase(v)}<span class="rm-id">${escapeHtml(v.id)}</span></div>
|
|
166
159
|
<h3 class="rm-name">${escapeHtml(v.name)}</h3>
|
|
167
160
|
<p class="rm-desc rm-desc--clamp">${escapeHtml(v.desc)}</p>
|
|
168
161
|
</div>
|
|
@@ -183,7 +176,7 @@
|
|
|
183
176
|
return `<article class="rm-card">
|
|
184
177
|
<div class="rm-feat-top">
|
|
185
178
|
<div class="rt">
|
|
186
|
-
<div class="rm-row">${
|
|
179
|
+
<div class="rm-row">${phase(v)}<span class="rm-id">${escapeHtml(v.id)}</span></div>
|
|
187
180
|
<h3 class="rm-name">${escapeHtml(v.name)}</h3>
|
|
188
181
|
<p class="rm-desc rm-desc--clamp">${escapeHtml(v.desc)}</p>
|
|
189
182
|
</div>
|
|
@@ -225,7 +218,7 @@
|
|
|
225
218
|
return `<article class="rm-card rm-card--staged" style="grid-column:1 / -1">
|
|
226
219
|
<div class="rm-featgrid">
|
|
227
220
|
<div class="rm-featgrid__l">
|
|
228
|
-
<div class="rm-row">${
|
|
221
|
+
<div class="rm-row">${phase(v)}<span class="rm-id">${escapeHtml(v.id)}</span></div>
|
|
229
222
|
<h3 class="rm-name">${escapeHtml(v.name)}</h3>
|
|
230
223
|
<p class="rm-desc rm-desc--clamp">${escapeHtml(v.desc)}</p>
|
|
231
224
|
<div class="rm-staged-state">
|
|
@@ -243,7 +236,7 @@
|
|
|
243
236
|
return `<article class="rm-card rm-card--parked" style="grid-column:1 / -1">
|
|
244
237
|
<div class="rm-feat-top">
|
|
245
238
|
<div class="rt">
|
|
246
|
-
<div class="rm-row">${
|
|
239
|
+
<div class="rm-row">${phase(v)}<span class="rm-id">${escapeHtml(v.id)}</span></div>
|
|
247
240
|
<h3 class="rm-name">${escapeHtml(v.name)}</h3>
|
|
248
241
|
<p class="rm-desc rm-desc--clamp">${escapeHtml(v.desc)}</p>
|
|
249
242
|
</div>
|
|
@@ -353,8 +346,8 @@
|
|
|
353
346
|
}
|
|
354
347
|
}
|
|
355
348
|
|
|
356
|
-
// Merge /public/progress (counts, %, lifecycle) with /public/versions (
|
|
357
|
-
//
|
|
349
|
+
// Merge /public/progress (counts, %, lifecycle) with /public/versions (dates)
|
|
350
|
+
// into one version model the renderers consume.
|
|
358
351
|
function mergeVersions(progress, versions) {
|
|
359
352
|
const meta = new Map();
|
|
360
353
|
(versions || []).forEach((v) => meta.set(v.id, v));
|
|
@@ -363,7 +356,6 @@
|
|
|
363
356
|
return {
|
|
364
357
|
id: p.version_id,
|
|
365
358
|
name: p.name || m.name || p.version_id,
|
|
366
|
-
track: m.track || 'internal',
|
|
367
359
|
status: p.status || m.status || 'building',
|
|
368
360
|
desc: (p.done_when || m.done_when || '').trim(),
|
|
369
361
|
shipped: Number(p.shipped_count) || 0,
|
|
@@ -536,8 +536,12 @@
|
|
|
536
536
|
// word is the thing the round's stat-tile reference exists to fix.
|
|
537
537
|
const meta = [];
|
|
538
538
|
if (t.version_id) meta.push(`<b>${escapeHtml(t.version_id)}</b>`);
|
|
539
|
-
|
|
540
|
-
|
|
539
|
+
// Neither axis says the word its column defaults to (task 1004038). `if
|
|
540
|
+
// (t.kind)` was never a guard for it: 'unclassified' is a truthy string, so
|
|
541
|
+
// the run read it out loud. The role line below asked the same question with
|
|
542
|
+
// a literal — it is right, but a copy of roleIsUnset() is free to drift.
|
|
543
|
+
if (!window.OTB.kindIsUnset(t.kind)) meta.push(escapeHtml(t.kind));
|
|
544
|
+
if (!window.OTB.roleIsUnset(t.discipline)) meta.push(escapeHtml(t.discipline));
|
|
541
545
|
// rankText() answers "any rank" for an open task, which read as
|
|
542
546
|
// "min rank any rank" once the label was prefixed. An open floor is not a
|
|
543
547
|
// minimum, so it is said as the absence of one.
|
|
@@ -429,6 +429,8 @@
|
|
|
429
429
|
const disciplineTag = window.OTB.roleIsUnset(t.discipline)
|
|
430
430
|
? ''
|
|
431
431
|
: `<span class="task__tag">${escapeHtml(window.OTB.roleLabel(t.discipline))}</span>`;
|
|
432
|
+
// A kind nobody set is no tag at all, like the role above (task 1004038).
|
|
433
|
+
const kindTag = window.OTB.kindIsUnset(t.kind) ? '' : `<span class="task__tag">${escapeHtml(t.kind)}</span>`;
|
|
432
434
|
const newcomerTag = t.newcomer_friendly
|
|
433
435
|
? '<span class="task__tag task__tag--newcomer">newcomer</span>'
|
|
434
436
|
: '';
|
|
@@ -459,7 +461,7 @@
|
|
|
459
461
|
<p class="task__title"><a class="task__link" href="${escapeHtml(taskHref(t.id))}">${escapeHtml(t.title)}</a></p>
|
|
460
462
|
<div class="task__meta">
|
|
461
463
|
<span class="task__tag">${escapeHtml(t.version_id || '?')}</span>
|
|
462
|
-
|
|
464
|
+
${kindTag}
|
|
463
465
|
${disciplineTag}
|
|
464
466
|
${newcomerTag}
|
|
465
467
|
${rankTag}
|
|
@@ -1142,7 +1144,7 @@
|
|
|
1142
1144
|
<div class="history__row" data-task-id="${escapeHtml(String(t.id))}" title="Open task #${escapeHtml(String(t.id))}">
|
|
1143
1145
|
<span class="history__id">#${escapeHtml(String(t.id))}</span>
|
|
1144
1146
|
<span class="history__title">${escapeHtml(t.title || '(untitled)')} ${ver} ${disciplineTag} ${rankTagHtml(t.requires_rank)}<br>${goalTag}</span>
|
|
1145
|
-
<span class="history__when">${
|
|
1147
|
+
<span class="history__when">${window.OTB.kindIsUnset(t.kind) ? '' : escapeHtml(t.kind)}</span>
|
|
1146
1148
|
</div>`;
|
|
1147
1149
|
}).join('');
|
|
1148
1150
|
}
|
|
@@ -23,7 +23,7 @@ const { createGoal } = require('./db-goals.js');
|
|
|
23
23
|
|
|
24
24
|
async function listVersions() {
|
|
25
25
|
const { rows } = await pool.query(
|
|
26
|
-
`SELECT id, name,
|
|
26
|
+
`SELECT id, name, status, scope_doc_path, started_at, shipped_at, done_when
|
|
27
27
|
FROM versions
|
|
28
28
|
ORDER BY started_at`
|
|
29
29
|
);
|
|
@@ -46,7 +46,7 @@ async function getVersion(id, deps = {}) {
|
|
|
46
46
|
throw new Error('getVersion({ forUpdate }) requires deps.client — a row lock outside a transaction holds nothing');
|
|
47
47
|
}
|
|
48
48
|
const { rows } = await executorFor(deps, pool).query(
|
|
49
|
-
`SELECT id, name,
|
|
49
|
+
`SELECT id, name, status, scope_doc_path, started_at, shipped_at, done_when
|
|
50
50
|
FROM versions WHERE id = $1${deps.forUpdate ? ' FOR UPDATE' : ''}`,
|
|
51
51
|
[id]
|
|
52
52
|
);
|
|
@@ -167,10 +167,14 @@ async function versionsWithStatus(status, deps = {}) {
|
|
|
167
167
|
// goal-less criteria, and ADR 0183 criteria satisfy themselves off LINKED tasks.
|
|
168
168
|
// Criteria nobody can link a task to can never close. Stamping goal_id here is
|
|
169
169
|
// what gives them something to hang from.
|
|
170
|
-
async function createVersion({ id, name, status = 'planning',
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
170
|
+
async function createVersion({ id, name, status = 'planning', scopeDocPath = null, doneWhen = null, criteria = [], firstGoal = null, createdBy = null }, deps = {}) {
|
|
171
|
+
// No `track`: the two-track (product/internal) model was retired by task
|
|
172
|
+
// 1002006 and its code unwound by task 1003665. versions.track survives as a
|
|
173
|
+
// column, NOT NULL but carrying DEFAULT 'product' since core_205, so an INSERT
|
|
174
|
+
// that omits it is legal — which is what lets this path stop naming it. The
|
|
175
|
+
// comment that used to sit here said the column had NO default; that was true
|
|
176
|
+
// when it was written and stale by core_205, and believing it is why five
|
|
177
|
+
// insert paths kept supplying a value nobody read.
|
|
174
178
|
//
|
|
175
179
|
// deps.pool is the module's existing test seam (listVersions, versionProgress):
|
|
176
180
|
// honoured ONLY under NODE_ENV=test, so production can never be pointed at an
|
|
@@ -179,10 +183,10 @@ async function createVersion({ id, name, status = 'planning', track = 'product',
|
|
|
179
183
|
const txPool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
|
|
180
184
|
return withTx(async (client) => {
|
|
181
185
|
const { rows } = await client.query(
|
|
182
|
-
`INSERT INTO versions (id, name, status,
|
|
183
|
-
VALUES ($1, $2, $3, $4, $5
|
|
184
|
-
RETURNING id, name, status,
|
|
185
|
-
[id, name, status,
|
|
186
|
+
`INSERT INTO versions (id, name, status, scope_doc_path, done_when)
|
|
187
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
188
|
+
RETURNING id, name, status, scope_doc_path, started_at, shipped_at, done_when`,
|
|
189
|
+
[id, name, status, scopeDocPath, doneWhen]
|
|
186
190
|
);
|
|
187
191
|
const version = rows[0];
|
|
188
192
|
// The first goal. Titled for the version it opens unless the caller names one —
|
|
@@ -340,7 +344,7 @@ async function promotePlanningVersion(exec) {
|
|
|
340
344
|
SET status = 'building', started_at = COALESCE(started_at, now())
|
|
341
345
|
WHERE id = (SELECT id FROM versions WHERE status = 'planning' ORDER BY id LIMIT 1)
|
|
342
346
|
AND NOT EXISTS (SELECT 1 FROM versions WHERE status = 'building')
|
|
343
|
-
RETURNING id, name,
|
|
347
|
+
RETURNING id, name, status, started_at`
|
|
344
348
|
);
|
|
345
349
|
return rows[0] ?? null;
|
|
346
350
|
}
|
|
@@ -352,7 +356,7 @@ async function closeVersion({ versionId, plan = [], reason, planningVersionId =
|
|
|
352
356
|
await client.query('BEGIN');
|
|
353
357
|
|
|
354
358
|
const { rows: vRows } = await client.query(
|
|
355
|
-
`SELECT id, name,
|
|
359
|
+
`SELECT id, name, status, scope_doc_path, started_at, shipped_at, done_when
|
|
356
360
|
FROM versions WHERE id = $1 FOR UPDATE`,
|
|
357
361
|
[versionId]
|
|
358
362
|
);
|
|
@@ -576,7 +580,7 @@ async function closeVersion({ versionId, plan = [], reason, planningVersionId =
|
|
|
576
580
|
const { rows: closed } = await client.query(
|
|
577
581
|
`UPDATE versions SET status = 'shipped', shipped_at = now()
|
|
578
582
|
WHERE id = $1
|
|
579
|
-
RETURNING id, name,
|
|
583
|
+
RETURNING id, name, status, scope_doc_path, started_at, shipped_at, done_when`,
|
|
580
584
|
[versionId]
|
|
581
585
|
);
|
|
582
586
|
|
|
@@ -30,10 +30,6 @@ const { asyncHandler, validateOrRespond } = api;
|
|
|
30
30
|
// 'building'; 'shipped'/'frozen' are end states reached via the close flow.
|
|
31
31
|
const VALID_VERSION_STATUSES = new Set(['planning', 'building', 'shipped', 'frozen']);
|
|
32
32
|
|
|
33
|
-
// versions.track CHECK (migration 003) — NOT NULL with no DB default, so the
|
|
34
|
-
// create path must always supply one. 'product' is the caller-facing default:
|
|
35
|
-
// an instance seeding its first version is seeding its product track.
|
|
36
|
-
const VALID_VERSION_TRACKS = new Set(['product', 'internal']);
|
|
37
33
|
|
|
38
34
|
module.exports = function buildVersionsRouter() {
|
|
39
35
|
const router = express.Router();
|
|
@@ -99,7 +95,6 @@ module.exports = function buildVersionsRouter() {
|
|
|
99
95
|
id: { required: true, type: 'string', maxLength: 64, minLength: 1 },
|
|
100
96
|
name: { required: true, type: 'string', maxLength: 200, minLength: 1 },
|
|
101
97
|
status: { type: 'string', maxLength: 32 },
|
|
102
|
-
track: { type: 'string', maxLength: 16 },
|
|
103
98
|
done_when: { type: 'string', maxLength: 8000 },
|
|
104
99
|
scope_doc_path: { type: 'string', maxLength: 256 },
|
|
105
100
|
criteria: { type: 'array', maxItems: 100 },
|
|
@@ -116,11 +111,6 @@ module.exports = function buildVersionsRouter() {
|
|
|
116
111
|
return res.fail('bad_status', 400, { valid: Array.from(VALID_VERSION_STATUSES) });
|
|
117
112
|
}
|
|
118
113
|
|
|
119
|
-
const track = body.track || 'product';
|
|
120
|
-
if (!VALID_VERSION_TRACKS.has(track)) {
|
|
121
|
-
return res.fail('bad_track', 400, { valid: Array.from(VALID_VERSION_TRACKS) });
|
|
122
|
-
}
|
|
123
|
-
|
|
124
114
|
if (await db.getVersion(id)) {
|
|
125
115
|
return res.fail('version_exists', { status: 409, message: `Version '${id}' already exists.` });
|
|
126
116
|
}
|
|
@@ -187,7 +177,7 @@ module.exports = function buildVersionsRouter() {
|
|
|
187
177
|
let version, goal, created;
|
|
188
178
|
try {
|
|
189
179
|
({ version, goal, criteria: created } = await db.createVersion({
|
|
190
|
-
id, name, status,
|
|
180
|
+
id, name, status,
|
|
191
181
|
doneWhen: body.done_when ?? null,
|
|
192
182
|
scopeDocPath: body.scope_doc_path ?? null,
|
|
193
183
|
criteria,
|
|
@@ -65,7 +65,7 @@ async function autoCloseVersionIfComplete(exec, versionId) {
|
|
|
65
65
|
AND g.status = 'open'
|
|
66
66
|
AND ${NOT_MAINTENANCE}
|
|
67
67
|
)
|
|
68
|
-
RETURNING v.id, v.name, v.
|
|
68
|
+
RETURNING v.id, v.name, v.status, v.shipped_at`,
|
|
69
69
|
[versionId]
|
|
70
70
|
);
|
|
71
71
|
return rows[0] ?? null;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.881",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.881",
|
|
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.
|
|
3
|
+
"version": "1.19.881",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/scripts/gds/init.js
CHANGED
|
@@ -391,9 +391,6 @@ function versionPayload(spec) {
|
|
|
391
391
|
id: v.id,
|
|
392
392
|
name: v.name,
|
|
393
393
|
status: v.status || 'building',
|
|
394
|
-
// versions.track is NOT NULL (no DB default) — always send it explicitly.
|
|
395
|
-
// An instance's first version is its product track unless the spec says otherwise.
|
|
396
|
-
track: v.track === 'internal' ? 'internal' : 'product',
|
|
397
394
|
done_when: v.done_when,
|
|
398
395
|
criteria: [{ criterion_id: v.criterion_id ? slugify(v.criterion_id) : 'first-goal', criterion_md: v.done_when }],
|
|
399
396
|
};
|
|
@@ -55,8 +55,9 @@ function migrateCmd(inst, { privileged = false } = {}) {
|
|
|
55
55
|
// ONLY IF the versions table is empty: idempotent, safe to re-run, and it never
|
|
56
56
|
// disturbs a project that already has versions. Mirrors seedViaApi's shape — V1
|
|
57
57
|
// (status 'building') + one done-when criterion so /status has a finish line from day
|
|
58
|
-
// one. `track`
|
|
59
|
-
//
|
|
58
|
+
// one. No `track` column: the two-track model was retired (task 1002006, unwound
|
|
59
|
+
// by task 1003665). The seed runs on a freshly-MIGRATED database, so core_205's
|
|
60
|
+
// DEFAULT 'product' is in place and an INSERT that omits the column is legal.
|
|
60
61
|
//
|
|
61
62
|
// The id/name/done_when come from instanceInitSpec so the starter version has ONE
|
|
62
63
|
// source of truth (the same spec the greenfield `bongos init` seed uses). SQL string
|
|
@@ -68,8 +69,8 @@ function seedFirstVersionCmd(inst, { privileged = false } = {}) {
|
|
|
68
69
|
const v = instanceInitSpec(inst).version;
|
|
69
70
|
const q = (s) => `$v$${s}$v$`; // dollar-quoted literal (no value contains a single quote or $)
|
|
70
71
|
const sql =
|
|
71
|
-
`INSERT INTO versions (id, name,
|
|
72
|
-
`SELECT ${q(v.id)}, ${q(v.name)}, $v$
|
|
72
|
+
`INSERT INTO versions (id, name, status, done_when) ` +
|
|
73
|
+
`SELECT ${q(v.id)}, ${q(v.name)}, $v$building$v$, ${q(v.done_when)} ` +
|
|
73
74
|
`WHERE NOT EXISTS (SELECT 1 FROM versions); ` +
|
|
74
75
|
`INSERT INTO done_when_criteria (version_id, criterion_id, criterion_md, sort_order) ` +
|
|
75
76
|
`SELECT ${q(v.id)}, $v$first-goal$v$, ${q(v.done_when)}, 10 ` +
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.881'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -354,6 +354,11 @@ function renderTask(task, { lastClaim = null, viewer = null, goal = null, goalsL
|
|
|
354
354
|
roleOptions: () => [{ value: '', label: '— any —' }, { value: 'engineer', label: 'Engineer' }, { value: 'artist', label: 'Artist' }],
|
|
355
355
|
roleIsUnset: (d) => !d || d === 'unclassified',
|
|
356
356
|
ROLE_UNSET: 'unclassified',
|
|
357
|
+
// task 1004038 — the kind axis asks the same question. Stubbed for the
|
|
358
|
+
// reason the comment above gives: an undefined helper throws mid-render,
|
|
359
|
+
// and the page then renders its "feed unavailable" card instead.
|
|
360
|
+
kindIsUnset: (k) => !k || k === 'unclassified',
|
|
361
|
+
KIND_UNSET: 'unclassified',
|
|
357
362
|
escapeHtml,
|
|
358
363
|
$: (sel) => documentObj.querySelector(sel),
|
|
359
364
|
fmtDate: (iso) => String(iso).slice(0, 10),
|
package/tests/init.mjs
CHANGED
|
@@ -204,16 +204,18 @@ test('versionPayload: explicit criterion_id is slugified', () => {
|
|
|
204
204
|
assert.equal(init.versionPayload(spec).criteria[0].criterion_id, 'prove-the-loop');
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
207
|
+
// The inverse of the case this replaces. That case pinned "always send an
|
|
208
|
+
// explicit track", guarding the 1.18.18 bug where omitting it 500ed every
|
|
209
|
+
// POST /versions against a NOT NULL column with no default. core_205 gave the
|
|
210
|
+
// column DEFAULT 'product' and task 1003665 retired the concept, so the payload
|
|
211
|
+
// must now NOT carry it — and a spec that still names one must not smuggle it
|
|
212
|
+
// through to a route that no longer accepts the field.
|
|
213
|
+
test('versionPayload: sends no track, and drops one a spec still names', () => {
|
|
214
|
+
assert.equal('track' in init.versionPayload(fullSpec()), false);
|
|
215
|
+
const legacy = fullSpec();
|
|
216
|
+
legacy.version.track = 'internal';
|
|
217
|
+
assert.equal('track' in init.versionPayload(legacy), false,
|
|
218
|
+
'an old spec file cannot reintroduce the field');
|
|
217
219
|
});
|
|
218
220
|
|
|
219
221
|
// ---- provisioning arm (ADR 0111 §3, task 1946) ----
|
|
@@ -661,12 +663,13 @@ async function runSeedTests() {
|
|
|
661
663
|
assert.ok(findTaskPost(api, /Production topology/), 'filed the manual fallback task on error');
|
|
662
664
|
});
|
|
663
665
|
|
|
664
|
-
await at('seedViaApi: version POST carries
|
|
666
|
+
await at('seedViaApi: the version POST carries NO track (task 1003665)', async () => {
|
|
665
667
|
const api = fakeApi();
|
|
666
668
|
await init.seedViaApi(provSpec({}), { log: () => {}, api });
|
|
667
669
|
const vpost = api.calls.find((c) => c.path.endsWith('/versions'));
|
|
668
670
|
assert.ok(vpost, 'POSTed the version');
|
|
669
|
-
assert.equal(vpost.body
|
|
671
|
+
assert.equal('track' in vpost.body, false,
|
|
672
|
+
'POST /versions no longer accepts the field; the DB default supplies the column');
|
|
670
673
|
});
|
|
671
674
|
|
|
672
675
|
await at('seedViaApi: the ideator kickoff task POSTs kind=decision + discipline=ideator', async () => {
|