@bongos/core 1.19.601 → 1.19.603
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 +20 -15
- package/docs/module-api-changelog.md +4 -0
- package/modules/lifecycle/db-versions.js +18 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/version-close.js +146 -81
- package/src/module-api.js +1 -1
- package/tests/version_close_cli.mjs +164 -0
- package/tests/version_promotion.mjs +5 -1
- package/tests/version_roll_forward.mjs +37 -1
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.603",
|
|
6
|
+
"core_contract": "1.19.603",
|
|
7
|
+
"source_commit": "54df6f5e5080d70795a267d435e1c3595e597cd1",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-08T05:
|
|
9
|
+
"built_at": "2026-09-08T05:34:52.291Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 457,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2077,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2558,
|
|
20
|
+
"tree_sha256": "6ac267a518248236669ff017af4b9c35d3835cfa1c679b45ea8b6aa65c6690ef",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -2727,7 +2727,7 @@
|
|
|
2727
2727
|
{
|
|
2728
2728
|
"path": "docs/module-api-changelog.md",
|
|
2729
2729
|
"mode": "0000644",
|
|
2730
|
-
"sha256": "
|
|
2730
|
+
"sha256": "998a472d5b9c5c3b54337d03979d07204d1f224a91d6251e6b578744b9ff0364"
|
|
2731
2731
|
},
|
|
2732
2732
|
{
|
|
2733
2733
|
"path": "docs/modules-contract.md",
|
|
@@ -5567,7 +5567,7 @@
|
|
|
5567
5567
|
{
|
|
5568
5568
|
"path": "modules/lifecycle/db-versions.js",
|
|
5569
5569
|
"mode": "0000644",
|
|
5570
|
-
"sha256": "
|
|
5570
|
+
"sha256": "51b170cdd34a90c49070ec79723b5250dffbc8c61d5ed114d6a560bcc93cadb7"
|
|
5571
5571
|
},
|
|
5572
5572
|
{
|
|
5573
5573
|
"path": "modules/lifecycle/db.js",
|
|
@@ -7627,12 +7627,12 @@
|
|
|
7627
7627
|
{
|
|
7628
7628
|
"path": "package-lock.json",
|
|
7629
7629
|
"mode": "0000644",
|
|
7630
|
-
"sha256": "
|
|
7630
|
+
"sha256": "34defd9d51ab981a40425d9faa27a0a3aa694580fd7624f307bd1bdaba3a13d4"
|
|
7631
7631
|
},
|
|
7632
7632
|
{
|
|
7633
7633
|
"path": "package.json",
|
|
7634
7634
|
"mode": "0000644",
|
|
7635
|
-
"sha256": "
|
|
7635
|
+
"sha256": "aaa775cf4d3e84c22beae120aa96d84b1c40fdaf85c0159c1eb38d957a12e26f"
|
|
7636
7636
|
},
|
|
7637
7637
|
{
|
|
7638
7638
|
"path": "public-docs/index.html",
|
|
@@ -8932,7 +8932,7 @@
|
|
|
8932
8932
|
{
|
|
8933
8933
|
"path": "scripts/gds/version-close.js",
|
|
8934
8934
|
"mode": "0000644",
|
|
8935
|
-
"sha256": "
|
|
8935
|
+
"sha256": "2db9a69b8a64873eee7cd473ce2915df51ccfffb79f289ec01e0419d2861ba3e"
|
|
8936
8936
|
},
|
|
8937
8937
|
{
|
|
8938
8938
|
"path": "scripts/gds/version-literal-guard.js",
|
|
@@ -9347,7 +9347,7 @@
|
|
|
9347
9347
|
{
|
|
9348
9348
|
"path": "src/module-api.js",
|
|
9349
9349
|
"mode": "0000644",
|
|
9350
|
-
"sha256": "
|
|
9350
|
+
"sha256": "48518bea650f7714a9ff00aa6c5de38a1fced6c8c04cf050143254371c73f854"
|
|
9351
9351
|
},
|
|
9352
9352
|
{
|
|
9353
9353
|
"path": "src/module-loader/catalog.js",
|
|
@@ -12704,6 +12704,11 @@
|
|
|
12704
12704
|
"mode": "0000644",
|
|
12705
12705
|
"sha256": "372c80d42e122a70a4fae8fcfbaaa47459575d3e0a2b80fff8876dfc512c5aeb"
|
|
12706
12706
|
},
|
|
12707
|
+
{
|
|
12708
|
+
"path": "tests/version_close_cli.mjs",
|
|
12709
|
+
"mode": "0000644",
|
|
12710
|
+
"sha256": "7f7f4cc397bea0d319d5549b50aea178507dfeb2bdaf680fd42f4c80f56509ac"
|
|
12711
|
+
},
|
|
12707
12712
|
{
|
|
12708
12713
|
"path": "tests/version_close_route.mjs",
|
|
12709
12714
|
"mode": "0000644",
|
|
@@ -12722,12 +12727,12 @@
|
|
|
12722
12727
|
{
|
|
12723
12728
|
"path": "tests/version_promotion.mjs",
|
|
12724
12729
|
"mode": "0000644",
|
|
12725
|
-
"sha256": "
|
|
12730
|
+
"sha256": "a2be1bb9ea506cac6f1d7e4c54fe4dbd07fbca5fbd64624c099c0faf8be528de"
|
|
12726
12731
|
},
|
|
12727
12732
|
{
|
|
12728
12733
|
"path": "tests/version_roll_forward.mjs",
|
|
12729
12734
|
"mode": "0000644",
|
|
12730
|
-
"sha256": "
|
|
12735
|
+
"sha256": "cb4b723c632f3ec5f3bdbf06057d252bfbe3052841548a8ac386984c52447fc9"
|
|
12731
12736
|
},
|
|
12732
12737
|
{
|
|
12733
12738
|
"path": "tests/versions_route.mjs",
|
|
@@ -1651,5 +1651,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1651
1651
|
landed since 1.19.599 with no explicit bump. run 34187881031. (task 1002620)
|
|
1652
1652
|
1.19.601 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1653
1653
|
landed since 1.19.600 with no explicit bump. run 34188958832. (task 1002620)
|
|
1654
|
+
1.19.602 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1655
|
+
landed since 1.19.601 with no explicit bump. run 34189930315. (task 1002620)
|
|
1656
|
+
1.19.603 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1657
|
+
landed since 1.19.602 with no explicit bump. run 34191126290. (task 1002620)
|
|
1654
1658
|
---------------------------------------------------------------------------
|
|
1655
1659
|
```
|
|
@@ -404,11 +404,27 @@ async function closeVersion({ versionId, plan = [], reason, planningVersionId =
|
|
|
404
404
|
// Smoke fixtures are excluded on the same terms as the abandon branch, so
|
|
405
405
|
// both doors agree about what "unfinished" means.
|
|
406
406
|
const { rows: moved } = await client.query(
|
|
407
|
-
|
|
407
|
+
// THE VERSION MOVES WITH THE GOAL (task 1003710). Setting only
|
|
408
|
+
// `goal_id` leaves the task in a goal on the SUCCESSOR while still
|
|
409
|
+
// stamped with the version that just closed, and that column is not
|
|
410
|
+
// decoration: `version_progress` joins `tasks t ON t.version_id = v.id`
|
|
411
|
+
// and `version-close.js` filters `WHERE version_id = $1` in three
|
|
412
|
+
// places. A half-moved task therefore counts against a SHIPPED version
|
|
413
|
+
// forever, never counts toward the successor, and leaves the closed
|
|
414
|
+
// version still reporting carry-over it no longer holds — surfacing as
|
|
415
|
+
// "V1 closed but still shows N carried", a long way from here.
|
|
416
|
+
//
|
|
417
|
+
// Identical to the fix task 1003709 made to `carryBugsForward`
|
|
418
|
+
// (db-goals.js). Both halves of the boundary move — the roll-forward
|
|
419
|
+
// here and the maintenance carry-over there — and I shipped both with
|
|
420
|
+
// the same defect while only one got fixed. tests/version_roll_forward.mjs
|
|
421
|
+
// now pins the two against each other so the next edit cannot drift one
|
|
422
|
+
// without the other.
|
|
423
|
+
`UPDATE tasks SET goal_id = $2, version_id = $3, updated_at = now()
|
|
408
424
|
WHERE goal_id = $1 AND ${nonTerminalSql('status')}
|
|
409
425
|
AND title NOT LIKE '\\_\\_smoke\\_\\_%' ESCAPE '\\'
|
|
410
426
|
RETURNING id`,
|
|
411
|
-
[step.goalId, successorId]
|
|
427
|
+
[step.goalId, successorId, planningVersionId]
|
|
412
428
|
);
|
|
413
429
|
|
|
414
430
|
await client.query(
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.603",
|
|
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.603",
|
|
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.603",
|
|
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",
|
|
@@ -11,12 +11,15 @@
|
|
|
11
11
|
// 3. Identifies carry-over candidates (backlog + ready) and drafts a
|
|
12
12
|
// next-version scope stub (e.g. limitations/pms-v3.md) with a
|
|
13
13
|
// "## Carried Over from <VID>" section.
|
|
14
|
-
// 4.
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
14
|
+
// 4. CLOSES the version through POST /versions/:id/close (BV1.R23, task
|
|
15
|
+
// 1003610). This step used to print a "MANUAL STEP REQUIRED" block of SQL
|
|
16
|
+
// to paste into psql on the droplet, because at the time the API genuinely
|
|
17
|
+
// exposed no version status transition. BV1.R12 (task 1003599) built the
|
|
18
|
+
// route, so that text became false — and by then the SQL was actively
|
|
19
|
+
// harmful, since it bypasses the disposition two-step, the closure
|
|
20
|
+
// invariant, the successor lineage, the promotion and the bug carry-forward.
|
|
21
|
+
// Closing a version by hand now silently strands everything the route
|
|
22
|
+
// carries.
|
|
20
23
|
// 5. Prints a suggested git commit message.
|
|
21
24
|
//
|
|
22
25
|
// Every destructive write (file create, INSERT, UPDATE) prompts y/N first.
|
|
@@ -38,7 +41,7 @@ const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
|
38
41
|
// -------------------------------------------------------------------------
|
|
39
42
|
|
|
40
43
|
function parseArgs(argv) {
|
|
41
|
-
const out = { versionId: null, yes: false, nextId: null, noNextStub: false };
|
|
44
|
+
const out = { versionId: null, yes: false, nextId: null, noNextStub: false, reason: null, dispositions: {} };
|
|
42
45
|
for (let i = 2; i < argv.length; i++) {
|
|
43
46
|
const a = argv[i];
|
|
44
47
|
if (a === '--yes' || a === '-y') {
|
|
@@ -49,6 +52,16 @@ function parseArgs(argv) {
|
|
|
49
52
|
out.nextId = argv[++i];
|
|
50
53
|
} else if (a.startsWith('--next=')) {
|
|
51
54
|
out.nextId = a.slice('--next='.length);
|
|
55
|
+
} else if (a === '--reason') {
|
|
56
|
+
out.reason = argv[++i];
|
|
57
|
+
} else if (a.startsWith('--reason=')) {
|
|
58
|
+
out.reason = a.slice('--reason='.length);
|
|
59
|
+
} else if (a === '--disposition' || a.startsWith('--disposition=')) {
|
|
60
|
+
// `<goalId>=<verb>`, repeatable. Parsed into the exact map shape
|
|
61
|
+
// POST /versions/:id/close expects, so the operator never hand-writes JSON.
|
|
62
|
+
const raw = a.startsWith('--disposition=') ? a.slice('--disposition='.length) : argv[++i];
|
|
63
|
+
const eq = String(raw || '').indexOf('=');
|
|
64
|
+
if (eq > 0) out.dispositions[raw.slice(0, eq)] = { verb: raw.slice(eq + 1) };
|
|
52
65
|
} else if (a === '-h' || a === '--help') {
|
|
53
66
|
out.help = true;
|
|
54
67
|
} else if (!out.versionId && !a.startsWith('-')) {
|
|
@@ -62,8 +75,15 @@ function usage() {
|
|
|
62
75
|
return [
|
|
63
76
|
'Usage: node scripts/gds/version-close.js <VERSION_ID> [--yes|-y] [--next <ID>] [--no-next-stub]',
|
|
64
77
|
'',
|
|
65
|
-
'Drafts <scope>-shipped.md + next-version stub,
|
|
66
|
-
'
|
|
78
|
+
'Drafts <scope>-shipped.md + next-version stub, then CLOSES the version',
|
|
79
|
+
'through POST /versions/:id/close. Always interactive without --yes.',
|
|
80
|
+
'',
|
|
81
|
+
' --reason <text> recorded on the close; stamped on any task a',
|
|
82
|
+
' disposition abandons. Required by the route',
|
|
83
|
+
' whenever a goal is dispositioned.',
|
|
84
|
+
' --disposition <id>=<verb> roll_forward | abandon, repeatable — one per',
|
|
85
|
+
' open goal. Without them the close REFUSES and',
|
|
86
|
+
' lists the goals, so nothing is cut by omission.',
|
|
67
87
|
'',
|
|
68
88
|
' --no-next-stub Skip drafting the next-version scope stub entirely',
|
|
69
89
|
' (enforces the guided-review-pass memory rule). The',
|
|
@@ -74,6 +94,8 @@ function usage() {
|
|
|
74
94
|
' node scripts/gds/version-close.js PMS-V2 --next GDS-V3',
|
|
75
95
|
' node scripts/gds/version-close.js PMS-V2 --yes',
|
|
76
96
|
' node scripts/gds/version-close.js PMS-V2 --no-next-stub',
|
|
97
|
+
' node scripts/gds/version-close.js PMS-V2 --disposition 44=roll_forward \\',
|
|
98
|
+
' --disposition 51=abandon --reason "V2 is done; 51 is not coming back"',
|
|
77
99
|
].join('\n');
|
|
78
100
|
}
|
|
79
101
|
|
|
@@ -277,74 +299,66 @@ function buildNextVersionStub({ nextVersionId, prevVersionId, carryOverTasks })
|
|
|
277
299
|
}
|
|
278
300
|
|
|
279
301
|
// -------------------------------------------------------------------------
|
|
280
|
-
//
|
|
302
|
+
// The close itself — a ROUTE call, not printed SQL (BV1.R23, task 1003610)
|
|
281
303
|
// -------------------------------------------------------------------------
|
|
304
|
+
//
|
|
305
|
+
// THIS FILE USED TO PRINT A `-- MANUAL STEP REQUIRED` BLOCK and tell the operator
|
|
306
|
+
// to run three UPDATEs on the production droplet, on the entirely true grounds
|
|
307
|
+
// that "the GDS API does not currently expose endpoints for version status
|
|
308
|
+
// transitions". BV1.R12 (task 1003599) built `POST /versions/:id/close`, so that
|
|
309
|
+
// sentence became false — and a script that keeps printing it is worse than one
|
|
310
|
+
// that never offered anything, because the SQL path BYPASSES every rule this goal
|
|
311
|
+
// exists to enforce: the disposition two-step (R12/R14), the closure invariant
|
|
312
|
+
// (R08/R09), the successor lineage (R17), the promotion (R20) and the bug
|
|
313
|
+
// carry-forward (R18). An operator pasting that SQL would close a version and
|
|
314
|
+
// silently strand everything the route would have carried.
|
|
315
|
+
//
|
|
316
|
+
// The advisory half of this script — `suggestClose`, the shipped archive, the
|
|
317
|
+
// next-version scope stub — is unchanged and still the reason to run it.
|
|
282
318
|
|
|
283
|
-
function
|
|
284
|
-
|
|
319
|
+
async function closeVersionViaApi({ versionId, reason, dispositions }) {
|
|
320
|
+
const api = await cliClient();
|
|
321
|
+
const body = { reason };
|
|
322
|
+
if (dispositions && Object.keys(dispositions).length > 0) body.dispositions = dispositions;
|
|
323
|
+
const { ok, status, data } = await api.versions.postVersionsIdClose({ path: { id: versionId }, body });
|
|
324
|
+
return { ok, status, data };
|
|
285
325
|
}
|
|
286
326
|
|
|
287
|
-
|
|
327
|
+
// The route answers the R12 two-step: called with open goals and no disposition
|
|
328
|
+
// it REFUSES and returns them, so the operator can decide per goal rather than
|
|
329
|
+
// being told only that something is in the way. Rendered here rather than dumped
|
|
330
|
+
// as JSON because the next thing the operator types is the re-run.
|
|
331
|
+
function renderOpenGoalsRefusal(versionId, details) {
|
|
332
|
+
const goals = (details && details.goals) || [];
|
|
288
333
|
const lines = [
|
|
289
|
-
|
|
290
|
-
'-- The GDS API does not currently expose endpoints for: version status',
|
|
291
|
-
'-- transitions, version creation from CLI, or task version_id rebinding.',
|
|
292
|
-
'-- Run the SQL below on the production droplet (psql amazonprimea).',
|
|
293
|
-
'-- ---------------------------------------------------------------------',
|
|
294
|
-
'BEGIN;',
|
|
334
|
+
`[refused] ${versionId} still holds ${goals.length} open goal(s). Nothing was changed.`,
|
|
295
335
|
'',
|
|
296
|
-
|
|
297
|
-
`UPDATE versions`,
|
|
298
|
-
` SET status = 'shipped'`,
|
|
299
|
-
` , shipped_at = now()`,
|
|
300
|
-
` WHERE id = '${sqlEscape(versionId)}'`,
|
|
301
|
-
` AND status = 'building';`,
|
|
336
|
+
'Decide what happens to each, then re-run with --disposition:',
|
|
302
337
|
'',
|
|
303
338
|
];
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
lines.push(
|
|
307
|
-
lines.push('');
|
|
308
|
-
} else {
|
|
309
|
-
lines.push(`-- 2. Insert ${nextVersionId} stub with status='building'.`);
|
|
310
|
-
lines.push(`INSERT INTO versions (id, name, track, status, scope_doc_path, started_at)`);
|
|
311
|
-
lines.push(`VALUES ('${sqlEscape(nextVersionId)}',`);
|
|
312
|
-
lines.push(` '${sqlEscape(nextVersionId)}',`);
|
|
313
|
-
lines.push(` (SELECT track FROM versions WHERE id = '${sqlEscape(versionId)}'),`);
|
|
314
|
-
lines.push(` 'building',`);
|
|
315
|
-
lines.push(` '${sqlEscape(nextScopeDocPath)}',`);
|
|
316
|
-
lines.push(` now())`);
|
|
317
|
-
lines.push(`ON CONFLICT (id) DO NOTHING;`);
|
|
318
|
-
lines.push('');
|
|
339
|
+
for (const g of goals) {
|
|
340
|
+
const open = g.open_tasks ? ` (${g.open_tasks} unfinished task${g.open_tasks === 1 ? '' : 's'})` : '';
|
|
341
|
+
lines.push(` #${g.id} ${g.title || ''}${open}`);
|
|
319
342
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
lines.push(` WHERE id IN (${ids})`);
|
|
328
|
-
lines.push(` AND version_id = '${sqlEscape(versionId)}'`);
|
|
329
|
-
lines.push(` AND status IN ('backlog', 'ready');`);
|
|
330
|
-
lines.push('');
|
|
331
|
-
} else {
|
|
332
|
-
lines.push(`-- 3. No carry-over tasks — skip rebind step.`);
|
|
333
|
-
lines.push('');
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
lines.push('COMMIT;');
|
|
337
|
-
lines.push('-- ---------------------------------------------------------------------');
|
|
343
|
+
lines.push('');
|
|
344
|
+
lines.push(' --disposition <goalId>=roll_forward carry it into the planning version');
|
|
345
|
+
lines.push(' --disposition <goalId>=abandon cut it, stamping the close reason on its tasks');
|
|
346
|
+
lines.push('');
|
|
347
|
+
lines.push(` e.g. node scripts/gds/version-close.js ${versionId} \\`);
|
|
348
|
+
const example = goals.slice(0, 2).map((g) => `--disposition ${g.id}=roll_forward`).join(' ');
|
|
349
|
+
lines.push(` ${example || '--disposition <goalId>=roll_forward'} --reason "…"`);
|
|
338
350
|
return lines.join('\n');
|
|
339
351
|
}
|
|
340
352
|
|
|
353
|
+
|
|
341
354
|
function buildCommitMessage({ versionId, nextVersionId, shippedCount, carryCount }) {
|
|
342
355
|
const subject = `pms: close ${versionId}, open ${nextVersionId}`;
|
|
343
356
|
const body = [
|
|
344
357
|
'',
|
|
345
358
|
`- Archive ${versionId} scope to <scope>-shipped.md (${shippedCount} shipped task(s)).`,
|
|
346
359
|
`- Open ${nextVersionId} scope stub${carryCount ? ` with ${carryCount} carry-over task(s)` : ' (clean close)'}.`,
|
|
347
|
-
'-
|
|
360
|
+
'- Version status, task rebinding and the successor promotion were done by',
|
|
361
|
+
' POST /versions/:id/close (BV1.R23) — this commit is the drafted docs only.',
|
|
348
362
|
].join('\n');
|
|
349
363
|
return `${subject}\n${body}\n`;
|
|
350
364
|
}
|
|
@@ -500,34 +514,85 @@ async function main() {
|
|
|
500
514
|
} else {
|
|
501
515
|
await writeFileWithConfirm(prompter, nextScopeDocPath, nextStubMd, `${nextVersionId} scope stub`);
|
|
502
516
|
}
|
|
517
|
+
|
|
518
|
+
// Step 8 runs INSIDE this try so the prompter is still open for its confirm
|
|
519
|
+
// and is closed on every exit — including the refusal paths, which return
|
|
520
|
+
// early. A leaked readline keeps the process alive forever, which on a CLI
|
|
521
|
+
// reads as a hang rather than a failure.
|
|
522
|
+
return await closeStep();
|
|
503
523
|
} finally {
|
|
504
524
|
prompter.close();
|
|
505
525
|
}
|
|
506
526
|
|
|
507
|
-
// Step 8:
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
527
|
+
// Step 8: CLOSE THE VERSION THROUGH THE ROUTE (BV1.R23, task 1003610).
|
|
528
|
+
//
|
|
529
|
+
// Everything above this point is advisory — it drafts documents and reports the
|
|
530
|
+
// gate. This is the one destructive act, and it now goes through
|
|
531
|
+
// `POST /versions/:id/close` so it obeys the disposition two-step, the closure
|
|
532
|
+
// invariant, the successor lineage, the promotion and the bug carry-forward.
|
|
533
|
+
// The `--yes` flag the drafting steps honour gates it too: without it the
|
|
534
|
+
// operator is asked, because closing a version cannot be undone through the API.
|
|
535
|
+
async function closeStep() {
|
|
516
536
|
console.log('');
|
|
517
|
-
|
|
537
|
+
if (!args.yes) {
|
|
538
|
+
const go = await prompter.ask(`Close ${versionId} now via POST /versions/${versionId}/close?`);
|
|
539
|
+
if (!go) {
|
|
540
|
+
console.log(`[skip] ${versionId} left building. Re-run with --yes, or close it from the hall.`);
|
|
541
|
+
printCommit();
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
518
545
|
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
546
|
+
const closeReason = args.reason || `Closing ${versionId} (${shipped.length} shipped, ${carryOver.length} carry-over).`;
|
|
547
|
+
const res = await closeVersionViaApi({ versionId, reason: closeReason, dispositions: args.dispositions });
|
|
548
|
+
|
|
549
|
+
if (!res.ok) {
|
|
550
|
+
const err = (res.data && res.data.error) || `http_${res.status}`;
|
|
551
|
+
if (err === 'version_holds_open_goals' || err === 'disposition_incomplete') {
|
|
552
|
+
console.log(renderOpenGoalsRefusal(versionId, res.data && res.data.details));
|
|
553
|
+
process.exitCode = 1;
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
// Every other refusal is already NAMED by the route (ADR 0250 §7), so the
|
|
557
|
+
// honest thing is to relay it rather than re-word it into something the
|
|
558
|
+
// operator cannot search for.
|
|
559
|
+
console.log(`[refused] ${err}: ${(res.data && res.data.message) || ''}`);
|
|
560
|
+
process.exitCode = 1;
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
console.log(`[closed] ${versionId} → ${res.data.version.status}`);
|
|
565
|
+
for (const a of (res.data.applied || [])) {
|
|
566
|
+
const extra = a.successor_goal_id ? ` → successor goal #${a.successor_goal_id} (${a.tasks_moved} task(s) moved)`
|
|
567
|
+
: (a.tasks_abandoned != null ? ` (${a.tasks_abandoned} task(s) abandoned)` : '');
|
|
568
|
+
console.log(` goal #${a.goal_id}: ${a.verb} — ${a.result}${extra}`);
|
|
569
|
+
}
|
|
570
|
+
if (res.data.promoted) {
|
|
571
|
+
console.log(`[promoted] ${res.data.promoted.id} is now building.`);
|
|
572
|
+
if (res.data.carried && res.data.carried.moved) {
|
|
573
|
+
console.log(`[carried] ${res.data.carried.moved} open defect(s) moved to its maintenance goal.`);
|
|
574
|
+
}
|
|
575
|
+
} else {
|
|
576
|
+
console.log('[note] no version was in planning — the project has no live train until one is cut.');
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
printCommit();
|
|
529
580
|
console.log('');
|
|
530
|
-
console.log(`[done] ${versionId}
|
|
581
|
+
console.log(`[done] ${versionId} closed. Commit the drafted docs with the message above.`);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function printCommit() {
|
|
585
|
+
const commit = buildCommitMessage({
|
|
586
|
+
versionId,
|
|
587
|
+
nextVersionId,
|
|
588
|
+
shippedCount: shipped.length,
|
|
589
|
+
carryCount: carryOver.length,
|
|
590
|
+
});
|
|
591
|
+
console.log('');
|
|
592
|
+
console.log('--- suggested git commit message ---');
|
|
593
|
+
console.log(commit);
|
|
594
|
+
console.log('--- end commit message ---');
|
|
595
|
+
}
|
|
531
596
|
}
|
|
532
597
|
|
|
533
598
|
if (require.main === module) {
|
|
@@ -539,11 +604,11 @@ if (require.main === module) {
|
|
|
539
604
|
|
|
540
605
|
module.exports = {
|
|
541
606
|
parseArgs,
|
|
607
|
+
usage,
|
|
542
608
|
deriveNextVersionId,
|
|
543
609
|
deriveNextScopePath,
|
|
544
610
|
deriveShippedPath,
|
|
545
611
|
buildShippedArchive,
|
|
546
612
|
buildNextVersionStub,
|
|
547
|
-
buildManualSql,
|
|
548
613
|
buildCommitMessage,
|
|
549
614
|
};
|
package/src/module-api.js
CHANGED
|
@@ -55,7 +55,7 @@ const { buildInfo } = require('./build-info');
|
|
|
55
55
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
56
56
|
// the entry to that file. Look for a version's history there, not here.
|
|
57
57
|
// ---------------------------------------------------------------------------
|
|
58
|
-
const CORE_VERSION = '1.19.
|
|
58
|
+
const CORE_VERSION = '1.19.603'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
59
59
|
|
|
60
60
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
61
61
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// tests/version_close_cli.mjs — version-close.js drives the ROUTE, and no longer
|
|
2
|
+
// prints SQL for a human to paste into the production droplet
|
|
3
|
+
// (BV1.R23, task 1003610, goal 1000086).
|
|
4
|
+
//
|
|
5
|
+
// WHAT THIS SCRIPT USED TO DO, AND WHY IT WAS RIGHT AT THE TIME. It ended with a
|
|
6
|
+
// `-- MANUAL STEP REQUIRED` block: three UPDATEs to flip the version, insert the
|
|
7
|
+
// successor and rebind carry-over tasks, with the instruction to run them on the
|
|
8
|
+
// droplet. Its own header explained why — "the GDS API does not currently expose
|
|
9
|
+
// endpoints for version status transitions" — and that was simply true.
|
|
10
|
+
//
|
|
11
|
+
// WHY IT IS NOW THE MOST DANGEROUS THING IN THE FILE. BV1.R12 (task 1003599)
|
|
12
|
+
// built `POST /versions/:id/close`, so the claim became false. Worse, by then the
|
|
13
|
+
// route had accumulated everything this goal exists to enforce: the disposition
|
|
14
|
+
// two-step (R12/R14), the closure invariant (R08/R09), the successor lineage
|
|
15
|
+
// (R17), the promotion (R20) and the bug carry-forward (R18). An operator pasting
|
|
16
|
+
// that SQL would close a version and silently strand every one of them — a goal
|
|
17
|
+
// left open on a shipped version, a defect queue entombed where nothing routes,
|
|
18
|
+
// no successor promoted. The bypass was printed by the tool meant to prevent it.
|
|
19
|
+
//
|
|
20
|
+
// Run: node --test tests/version_close_cli.mjs
|
|
21
|
+
|
|
22
|
+
import assert from 'node:assert/strict';
|
|
23
|
+
import { test } from 'node:test';
|
|
24
|
+
import { readFileSync } from 'node:fs';
|
|
25
|
+
import { createRequire } from 'node:module';
|
|
26
|
+
|
|
27
|
+
process.env.NODE_ENV = 'test';
|
|
28
|
+
const require = createRequire(import.meta.url);
|
|
29
|
+
const { parseArgs, usage } = require('../scripts/gds/version-close.js');
|
|
30
|
+
|
|
31
|
+
const SRC = readFileSync(new URL('../scripts/gds/version-close.js', import.meta.url), 'utf8');
|
|
32
|
+
// Comments explain the history on purpose, so the prohibitions below read CODE.
|
|
33
|
+
const CODE = SRC.replace(/\/\/[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// The bypass is gone
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
test('the script no longer BUILDS any version-mutating SQL', () => {
|
|
40
|
+
assert.equal(/buildManualSql/.test(CODE), false, 'the SQL builder must be deleted, not merely unused');
|
|
41
|
+
for (const bad of [/UPDATE\s+versions/i, /INSERT INTO versions/i, /UPDATE\s+tasks/i]) {
|
|
42
|
+
assert.equal(bad.test(CODE), false, `the script must not emit ${bad} — that is the path around every rule this goal built`);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('it does not tell the operator to run anything on the droplet', () => {
|
|
47
|
+
assert.equal(/MANUAL STEP REQUIRED/.test(CODE), false);
|
|
48
|
+
assert.equal(/psql/i.test(CODE), false);
|
|
49
|
+
assert.equal(/droplet/i.test(CODE), false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('the commit message no longer promises SQL that will not be run', () => {
|
|
53
|
+
// It said "Run the printed SQL on the droplet to flip version status" — a
|
|
54
|
+
// commit body describing a step that no longer exists is a instruction someone
|
|
55
|
+
// eventually follows.
|
|
56
|
+
assert.equal(/Run the printed SQL/.test(CODE), false);
|
|
57
|
+
assert.match(SRC, /this commit is the drafted docs only/);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// It calls the route
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
test('the close goes through POST /versions/:id/close', () => {
|
|
65
|
+
assert.match(CODE, /postVersionsIdClose\(\{ path: \{ id: versionId \}, body \}\)/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('a disposition map is only sent when there is one', () => {
|
|
69
|
+
// The route treats an EMPTY map as "the caller dispositioned nothing", which is
|
|
70
|
+
// a different call from "the caller sent no map at all" — the first can satisfy
|
|
71
|
+
// a close of a version holding no open goals, the second is the two-step's
|
|
72
|
+
// first leg. Sending `{}` unconditionally would blur them.
|
|
73
|
+
assert.match(CODE, /if \(dispositions && Object\.keys\(dispositions\)\.length > 0\) body\.dispositions = dispositions;/);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('the advisory half is kept — this is still the script that drafts the archive', () => {
|
|
77
|
+
// The brief is explicit that suggestClose stays. Only the destructive tail moved.
|
|
78
|
+
assert.match(CODE, /fetchCloseSuggestions/);
|
|
79
|
+
assert.match(CODE, /buildShippedArchive|shippedDocPath/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// The two-step, rendered for a human
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
test('an open-goals refusal lists the goals and the exact re-run', () => {
|
|
87
|
+
// R12 refuses and RETURNS the goals precisely so the caller can decide per
|
|
88
|
+
// goal. Dumping that JSON would make the operator translate it themselves.
|
|
89
|
+
assert.match(CODE, /renderOpenGoalsRefusal/);
|
|
90
|
+
assert.match(SRC, /--disposition <goalId>=roll_forward/);
|
|
91
|
+
assert.match(SRC, /--disposition <goalId>=abandon/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('both incomplete-disposition refusals are handled, not just one', () => {
|
|
95
|
+
// The route answers `version_holds_open_goals` when nothing was sent and
|
|
96
|
+
// `disposition_incomplete` when a partial map was. Handling only the first
|
|
97
|
+
// leaves the commoner second attempt printing a bare error code.
|
|
98
|
+
assert.match(CODE, /version_holds_open_goals/);
|
|
99
|
+
assert.match(CODE, /disposition_incomplete/);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('any other refusal is RELAYED by name, not re-worded', () => {
|
|
103
|
+
// ADR 0250 §7: the codes are named so an agent can act on them. Re-phrasing one
|
|
104
|
+
// into prose makes it unsearchable.
|
|
105
|
+
assert.match(CODE, /\[refused\] \$\{err\}/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('a refusal exits non-zero', () => {
|
|
109
|
+
assert.match(CODE, /process\.exitCode = 1;/);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// The flags
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
test('--disposition parses into the exact map shape the route expects', () => {
|
|
117
|
+
const a = parseArgs(['node', 'x', 'BONGOS-V1', '--disposition', '44=roll_forward', '--disposition=51=abandon']);
|
|
118
|
+
assert.deepEqual(a.dispositions, { 44: { verb: 'roll_forward' }, 51: { verb: 'abandon' } },
|
|
119
|
+
'so the operator never hand-writes JSON');
|
|
120
|
+
assert.equal(a.versionId, 'BONGOS-V1');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('--reason survives both spellings', () => {
|
|
124
|
+
assert.equal(parseArgs(['n', 'x', 'V', '--reason', 'because']).reason, 'because');
|
|
125
|
+
assert.equal(parseArgs(['n', 'x', 'V', '--reason=because']).reason, 'because');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('a malformed --disposition is dropped rather than sent as nonsense', () => {
|
|
129
|
+
// `--disposition 44` with no verb would otherwise reach the route as
|
|
130
|
+
// {44: {verb: undefined}} and come back a bad_disposition the operator has to
|
|
131
|
+
// decode. Dropping it means the close refuses with the goal still listed.
|
|
132
|
+
const a = parseArgs(['n', 'x', 'V', '--disposition', '44', '--disposition', '=abandon']);
|
|
133
|
+
assert.deepEqual(a.dispositions, {});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('no flags means no dispositions — nothing is cut by omission', () => {
|
|
137
|
+
assert.deepEqual(parseArgs(['n', 'x', 'V']).dispositions, {});
|
|
138
|
+
assert.equal(parseArgs(['n', 'x', 'V']).reason, null);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('usage documents both new flags', () => {
|
|
142
|
+
const u = usage();
|
|
143
|
+
assert.match(u, /--reason/);
|
|
144
|
+
assert.match(u, /--disposition/);
|
|
145
|
+
assert.match(u, /POST \/versions\/:id\/close/);
|
|
146
|
+
assert.equal(/SQL/.test(u), false, 'the usage must not still advertise printed SQL');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// The close is still a confirmed, destructive act
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
test('without --yes the operator is asked before the version closes', () => {
|
|
154
|
+
// Closing cannot be undone through the API (ADR 0263 §2: nothing un-closes a
|
|
155
|
+
// version), so this is the one prompt that must not be dropped.
|
|
156
|
+
assert.match(CODE, /if \(!args\.yes\) \{[\s\S]{0,200}prompter\.ask\(`Close \$\{versionId\}/);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('the prompter is closed on every exit, including the refusal returns', () => {
|
|
160
|
+
// Step 8 returns early on a refusal. A leaked readline keeps the process alive,
|
|
161
|
+
// which on a CLI reads as a hang rather than a failure — so the close step runs
|
|
162
|
+
// inside the same try whose finally closes it.
|
|
163
|
+
assert.match(CODE, /return await closeStep\(\);\s*\}\s*finally \{\s*prompter\.close\(\);/);
|
|
164
|
+
});
|
|
@@ -128,7 +128,11 @@ test('the hook is composed in the ROUTE, not inside closeVersion', () => {
|
|
|
128
128
|
// the module's require DAG — and the route is the one place already holding the
|
|
129
129
|
// whole facade. Reaching sideways between the units is how that DAG stops being
|
|
130
130
|
// one.
|
|
131
|
-
|
|
131
|
+
// Comments stripped: db-versions.js NAMES carryBugsForward in prose (task
|
|
132
|
+
// 1003710's fix note points at the identical fix made there), and a check that
|
|
133
|
+
// read the explanation would fail on the cross-reference rather than on a call.
|
|
134
|
+
const dbvCode = DBV.replace(/\/\/[^\n]*/g, '');
|
|
135
|
+
assert.equal(/carryBugsForward/.test(dbvCode), false,
|
|
132
136
|
'db-versions.js must not reach into db-goals.js');
|
|
133
137
|
assert.match(ROUTES, /db\.promotePlanningVersion\(client\)/);
|
|
134
138
|
assert.match(ROUTES, /db\.carryBugsForward\(/);
|
|
@@ -36,7 +36,16 @@ const ROUTES = src('modules/lifecycle/routes/versions.js');
|
|
|
36
36
|
|
|
37
37
|
// The roll_forward branch, sliced so assertions land on the right code.
|
|
38
38
|
const CLOSE = DBV.slice(DBV.indexOf('async function closeVersion'));
|
|
39
|
-
|
|
39
|
+
// The roll_forward branch, bounded by its own `else {` and the invariant check
|
|
40
|
+
// that follows the disposition loop. Anchored on real markers rather than a
|
|
41
|
+
// character offset: a `- 4000` window silently stopped reaching the branch's own
|
|
42
|
+
// guard the moment the branch grew (task 1003710 added a comment and broke it).
|
|
43
|
+
const ROLL = (() => {
|
|
44
|
+
const start = CLOSE.lastIndexOf('} else {', CLOSE.indexOf("verb: 'roll_forward', result: 'succeeded'"));
|
|
45
|
+
const end = CLOSE.indexOf('// The invariant');
|
|
46
|
+
if (start === -1 || end === -1) throw new Error('roll_forward branch markers moved — fix this slice, do not widen it');
|
|
47
|
+
return CLOSE.slice(start, end);
|
|
48
|
+
})();
|
|
40
49
|
|
|
41
50
|
// ---------------------------------------------------------------------------
|
|
42
51
|
// The lineage
|
|
@@ -95,6 +104,33 @@ test('only UNFINISHED tasks are re-pointed; shipped work stays with the version
|
|
|
95
104
|
assert.match(move, /smoke/, 'smoke fixtures are excluded on the same terms as the abandon branch');
|
|
96
105
|
});
|
|
97
106
|
|
|
107
|
+
test('the VERSION moves with the goal, not just the goal_id (task 1003710)', () => {
|
|
108
|
+
// THE BUG THIS PINS, and it shipped. Setting only `goal_id` leaves the task in
|
|
109
|
+
// a goal on the successor while still stamped with the version that closed.
|
|
110
|
+
// `version_progress` joins `tasks t ON t.version_id = v.id` and
|
|
111
|
+
// `version-close.js` filters `WHERE version_id = $1`, so the task counts
|
|
112
|
+
// against a SHIPPED version forever, never counts toward the successor, and
|
|
113
|
+
// leaves the closed version reporting carry-over it no longer holds.
|
|
114
|
+
const move = ROLL.slice(ROLL.indexOf('UPDATE tasks SET goal_id'), ROLL.indexOf('RETURNING id`,', ROLL.indexOf('UPDATE tasks SET goal_id')));
|
|
115
|
+
assert.match(move, /version_id = \$3/, 'the successor version must be written too');
|
|
116
|
+
assert.match(ROLL, /\[step\.goalId, successorId, planningVersionId\]/, 'and bound, not interpolated');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('BOTH boundary moves write both columns — they cannot drift apart', () => {
|
|
120
|
+
// The roll-forward here and `carryBugsForward` in db-goals.js are the two ways
|
|
121
|
+
// a task crosses a version boundary. They shipped with the same defect and only
|
|
122
|
+
// one was fixed (task 1003709 fixed the carry-over, 1003710 this one). Pinning
|
|
123
|
+
// them against each other is what stops the next edit repeating it.
|
|
124
|
+
const dbg = src('modules/lifecycle/db-goals.js');
|
|
125
|
+
const carry = dbg.slice(dbg.indexOf('async function carryBugsForward'));
|
|
126
|
+
const carryMove = carry.slice(carry.indexOf('UPDATE tasks SET goal_id'));
|
|
127
|
+
for (const [name, sql] of [['roll_forward (db-versions.js)', ROLL.slice(ROLL.indexOf('UPDATE tasks SET goal_id'))],
|
|
128
|
+
['carryBugsForward (db-goals.js)', carryMove]]) {
|
|
129
|
+
assert.match(sql.slice(0, 200), /goal_id = \$2/, `${name}: must move goal_id`);
|
|
130
|
+
assert.match(sql.slice(0, 200), /version_id = \$3/, `${name}: must move version_id`);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
98
134
|
test('the predecessor is archived LAST, after its work has left', () => {
|
|
99
135
|
// The brief's explicit ask: it archives holding zero unfinished tasks, so it
|
|
100
136
|
// passes R10's own closure check on the way out rather than being
|