@bongos/core 1.19.890 → 1.19.891
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 +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/build-cli-package.js +4 -0
- package/scripts/gds/status.js +89 -13
- package/scripts/gds/version-literal-guard.js +22 -4
- package/src/module-api.js +1 -1
- package/tests/status_default_version.mjs +136 -0
- package/tests/version_literals.mjs +92 -0
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.891",
|
|
6
|
+
"core_contract": "1.19.891",
|
|
7
|
+
"source_commit": "e306e6ce6ad80a4e9259cea5c81326a60ff5e7ab",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-19T21:
|
|
9
|
+
"built_at": "2026-09-19T21:50:27.729Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 497,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2272,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2795,
|
|
20
|
+
"tree_sha256": "7d4974b5899a8d6ee86520b56479fb42f5256e4596ff21d29df8fd6925518fec",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2607,7 +2607,7 @@
|
|
|
2607
2607
|
{
|
|
2608
2608
|
"path": "docs/module-api-changelog.md",
|
|
2609
2609
|
"mode": "0000644",
|
|
2610
|
-
"sha256": "
|
|
2610
|
+
"sha256": "d6bb312d40e0a4532550ea1faa19b5ae80e09a4dab01035513f8967aecb88f2e"
|
|
2611
2611
|
},
|
|
2612
2612
|
{
|
|
2613
2613
|
"path": "docs/modules-contract.md",
|
|
@@ -7707,12 +7707,12 @@
|
|
|
7707
7707
|
{
|
|
7708
7708
|
"path": "package-lock.json",
|
|
7709
7709
|
"mode": "0000644",
|
|
7710
|
-
"sha256": "
|
|
7710
|
+
"sha256": "17d7b0573d81b759182725bbb1be90a3bf1da889cdef0ef97510e3bb0e263932"
|
|
7711
7711
|
},
|
|
7712
7712
|
{
|
|
7713
7713
|
"path": "package.json",
|
|
7714
7714
|
"mode": "0000644",
|
|
7715
|
-
"sha256": "
|
|
7715
|
+
"sha256": "90befa8ce40948a6ce667f2690343e2489b3ea33c08b4d6d1e2daee5227e9c9a"
|
|
7716
7716
|
},
|
|
7717
7717
|
{
|
|
7718
7718
|
"path": "public-docs/index.html",
|
|
@@ -8017,7 +8017,7 @@
|
|
|
8017
8017
|
{
|
|
8018
8018
|
"path": "scripts/gds/build-cli-package.js",
|
|
8019
8019
|
"mode": "0000644",
|
|
8020
|
-
"sha256": "
|
|
8020
|
+
"sha256": "ca0566c509fb1ea81e7c874e2676bbcb965803509c0289c577bc8f15791e6f5c"
|
|
8021
8021
|
},
|
|
8022
8022
|
{
|
|
8023
8023
|
"path": "scripts/gds/bump-version.js",
|
|
@@ -9207,7 +9207,7 @@
|
|
|
9207
9207
|
{
|
|
9208
9208
|
"path": "scripts/gds/status.js",
|
|
9209
9209
|
"mode": "0000644",
|
|
9210
|
-
"sha256": "
|
|
9210
|
+
"sha256": "191b49e9ccbcfc7b855d834535ad5e9de0970d6e91126c7a0591e7790d196a89"
|
|
9211
9211
|
},
|
|
9212
9212
|
{
|
|
9213
9213
|
"path": "scripts/gds/strand-watch.js",
|
|
@@ -9282,7 +9282,7 @@
|
|
|
9282
9282
|
{
|
|
9283
9283
|
"path": "scripts/gds/version-literal-guard.js",
|
|
9284
9284
|
"mode": "0000644",
|
|
9285
|
-
"sha256": "
|
|
9285
|
+
"sha256": "2c0e605876762f8c6607af49374f56bc0717a1aad08c3ffdbbae383ffaf1d9dd"
|
|
9286
9286
|
},
|
|
9287
9287
|
{
|
|
9288
9288
|
"path": "scripts/gds/version-select.js",
|
|
@@ -9717,7 +9717,7 @@
|
|
|
9717
9717
|
{
|
|
9718
9718
|
"path": "src/module-api.js",
|
|
9719
9719
|
"mode": "0000644",
|
|
9720
|
-
"sha256": "
|
|
9720
|
+
"sha256": "4dd8bdfcb69d7c1f7a1443175bea5379ed60daec5d19ec846a14732db59559fc"
|
|
9721
9721
|
},
|
|
9722
9722
|
{
|
|
9723
9723
|
"path": "src/module-loader/catalog.js",
|
|
@@ -13589,6 +13589,11 @@
|
|
|
13589
13589
|
"mode": "0000644",
|
|
13590
13590
|
"sha256": "4b92c96ae496736b0f44182e64d42a24f108cb51fbeb5cf7cd5814ad336b14f6"
|
|
13591
13591
|
},
|
|
13592
|
+
{
|
|
13593
|
+
"path": "tests/status_default_version.mjs",
|
|
13594
|
+
"mode": "0000644",
|
|
13595
|
+
"sha256": "9a80ed43b685ea537944886a1a4a02d35580c109bd777373e30b63afd9aa73df"
|
|
13596
|
+
},
|
|
13592
13597
|
{
|
|
13593
13598
|
"path": "tests/status_mirror_sync.mjs",
|
|
13594
13599
|
"mode": "0000644",
|
|
@@ -13877,7 +13882,7 @@
|
|
|
13877
13882
|
{
|
|
13878
13883
|
"path": "tests/version_literals.mjs",
|
|
13879
13884
|
"mode": "0000644",
|
|
13880
|
-
"sha256": "
|
|
13885
|
+
"sha256": "3b706518920bc48e1bcd5a0185a5829a179fa0e73a6ed491320327d2cb7c8c68"
|
|
13881
13886
|
},
|
|
13882
13887
|
{
|
|
13883
13888
|
"path": "tests/version_override_visibility.mjs",
|
|
@@ -2254,5 +2254,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2254
2254
|
landed since 1.19.888 with no explicit bump. run 35470239834. (task 1002620)
|
|
2255
2255
|
1.19.890 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2256
2256
|
landed since 1.19.889 with no explicit bump. run 35470624284. (task 1002620)
|
|
2257
|
+
1.19.891 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2258
|
+
landed since 1.19.890 with no explicit bump. run 35471575521. (task 1002620)
|
|
2257
2259
|
---------------------------------------------------------------------------
|
|
2258
2260
|
```
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.891",
|
|
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.891",
|
|
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.891",
|
|
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",
|
|
@@ -67,6 +67,10 @@ const FILES = [
|
|
|
67
67
|
'scripts/gds/start.js',
|
|
68
68
|
'scripts/gds/context-pack.js', // start.js renders through this
|
|
69
69
|
'scripts/gds/status.js',
|
|
70
|
+
// status.js resolves its default version at runtime instead of naming one
|
|
71
|
+
// (task 1004071) -- so the packaged CLI needs this too, or `bongos status`
|
|
72
|
+
// dies on a missing module. tests/cli_package.mjs caught exactly that.
|
|
73
|
+
'scripts/gds/version-select.js',
|
|
70
74
|
'scripts/gds/task.js',
|
|
71
75
|
'scripts/gds/recall.js',
|
|
72
76
|
'scripts/gds/cost.js',
|
package/scripts/gds/status.js
CHANGED
|
@@ -19,14 +19,24 @@
|
|
|
19
19
|
//
|
|
20
20
|
// Exit codes: 0 ok · 1 network/api error · 2 bad invocation · 3 not found.
|
|
21
21
|
|
|
22
|
-
const { API_BASE, cliClient, assertInstanceMatch } = require('./cli-lib');
|
|
22
|
+
const { API_BASE, cliClient, assertInstanceMatch, cliExit } = require('./cli-lib');
|
|
23
23
|
const { resolveEnv } = require('../../src/instance-config');
|
|
24
|
+
const { resolveDefaultVersion } = require('./version-select.js');
|
|
24
25
|
|
|
25
|
-
// The active internal version
|
|
26
|
-
// --version
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
const DEFAULT_VERSION = resolveEnv('STATUS_DEFAULT_VERSION') || 'BONGOS-V1';
|
|
26
|
+
// The active internal version is RESOLVED, never named here. Override with
|
|
27
|
+
// <PREFIX>_STATUS_DEFAULT_VERSION or --version.
|
|
28
|
+
//
|
|
29
|
+
// WHY THERE IS NO LITERAL (task 1004071). This line used to read
|
|
30
|
+
// const DEFAULT_VERSION = resolveEnv('STATUS_DEFAULT_VERSION') || 'BONGOS-V1';
|
|
31
|
+
// under a comment promising "the bare /status card should default to the live
|
|
32
|
+
// version, not a sealed one" — the same fix task 1709 had already applied once,
|
|
33
|
+
// for GDS-V4 → BONGOS-V1. BONGOS-V1 then sealed, nobody bumped the literal, and
|
|
34
|
+
// /status served a shipped version's closing scoreboard (140/203 satisfied) as
|
|
35
|
+
// the project's current status. It reads as a healthy project, which is why it
|
|
36
|
+
// sat: nothing in the output says `shipped`, and /status is what a session runs
|
|
37
|
+
// precisely when it does NOT already know. Bumping the literal again would only
|
|
38
|
+
// re-arm the same bomb for whoever seals BONGOS-V2, so the rule is derived from
|
|
39
|
+
// `versions.status` instead — the selection capture.js has always used.
|
|
30
40
|
|
|
31
41
|
// Compact, status-first ordering for the "remaining: …" summaries.
|
|
32
42
|
const STATUS_ORDER = ['shipped', 'confirmed', 'completed', 'claimed', 'ready', 'blocked', 'backlog'];
|
|
@@ -53,7 +63,36 @@ function parseArgs(argv) {
|
|
|
53
63
|
else if (!criterion) criterion = a.toUpperCase();
|
|
54
64
|
else return { error: `unexpected argument: ${a}` };
|
|
55
65
|
}
|
|
56
|
-
|
|
66
|
+
// No `versionExplicit` companion flag: `version !== null` already IS that
|
|
67
|
+
// fact, and resolveVersion returns `defaulted`/`source` for anyone who needs
|
|
68
|
+
// to act on it. A second field carrying the same truth is one that can drift.
|
|
69
|
+
return { version: version || null, criterion, json, widget, hook };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Resolve which version this invocation is about. Precedence: an explicit
|
|
73
|
+
// --version (or bare token) > the env override > the live BUILDING version.
|
|
74
|
+
// Returns { version, defaulted, source }; version is NULL when nothing is
|
|
75
|
+
// building, which the caller must treat as "stop and say so", never as a guess.
|
|
76
|
+
// Deps are injectable so the precedence is unit-tested without a session.
|
|
77
|
+
async function resolveVersion(opts, deps = {}) {
|
|
78
|
+
if (opts.version) return { version: opts.version, defaulted: false, source: 'flag' };
|
|
79
|
+
const env = (deps.resolveEnv || resolveEnv)('STATUS_DEFAULT_VERSION');
|
|
80
|
+
if (env) return { version: env, defaulted: true, source: 'env' };
|
|
81
|
+
const resolve = deps.resolveDefaultVersion || resolveDefaultVersion;
|
|
82
|
+
const building = await resolve();
|
|
83
|
+
if (building) return { version: building, defaulted: true, source: 'building' };
|
|
84
|
+
return { version: null, defaulted: true, source: 'none' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// One line naming a version the USER did not, so a cross-version answer cannot
|
|
88
|
+
// pass unnoticed. `Cn` is version-scoped and the versions number independently
|
|
89
|
+
// — BONGOS-V1 C8 is `money-one-unit`, BONGOS-V2 C8 is `wa6-kernel-and-packs` —
|
|
90
|
+
// so a silently-defaulted version answers a different question than the one
|
|
91
|
+
// asked. Empty when the user named the version: they already know.
|
|
92
|
+
function defaultedVersionNote(r) {
|
|
93
|
+
if (!r || !r.defaulted || !r.version) return '';
|
|
94
|
+
const why = r.source === 'env' ? 'from STATUS_DEFAULT_VERSION' : 'the version currently building';
|
|
95
|
+
return `(no version given — showing ${r.version}, ${why}. Pass --version to pick another.)`;
|
|
57
96
|
}
|
|
58
97
|
|
|
59
98
|
async function fetchProgress(version) {
|
|
@@ -325,7 +364,38 @@ async function main() {
|
|
|
325
364
|
// apiCall/cliClient guard, so without this `bongos status` would silently print another
|
|
326
365
|
// instance's progress. Skip only in --hook mode (the automated card path stays fail-open).
|
|
327
366
|
if (!opts.hook) await assertInstanceMatch();
|
|
328
|
-
|
|
367
|
+
|
|
368
|
+
// Resolve the version before anything fetches. --hook stays FAIL-OPEN like the
|
|
369
|
+
// instance check above: the automated card path must degrade, not throw, so a
|
|
370
|
+
// lookup that cannot answer emits an empty envelope rather than killing the hook.
|
|
371
|
+
//
|
|
372
|
+
// COST: one extra sequential GET /versions before the progress fetch, and it
|
|
373
|
+
// cannot be parallelised — the id IS the second call's path. It is paid only
|
|
374
|
+
// when no version was named: --version and STATUS_DEFAULT_VERSION both
|
|
375
|
+
// short-circuit it, so an instance that minds the round trip on the
|
|
376
|
+
// session-start card path sets the env var once and pays nothing. That is the
|
|
377
|
+
// deliberate trade against the alternative, which is the literal that spent
|
|
378
|
+
// months reporting a sealed version as current.
|
|
379
|
+
const picked = await resolveVersion(opts).catch(() => ({ version: null, defaulted: true, source: 'none' }));
|
|
380
|
+
if (!picked.version) {
|
|
381
|
+
if (opts.hook) {
|
|
382
|
+
process.stdout.write(JSON.stringify({
|
|
383
|
+
widgets_enabled: false, html: '', markdown: '',
|
|
384
|
+
warnings: ['status: no version is currently `building` — nothing to report.'],
|
|
385
|
+
}) + '\n');
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
console.error('status: no version is currently `building` — cannot resolve a default.');
|
|
389
|
+
console.error(' Open one, or pass --version explicitly (e.g. --version BONGOS-V2).');
|
|
390
|
+
// cliExit, NOT process.exit: resolveVersion above just made a network round
|
|
391
|
+
// trip, and a hard exit while the request machinery is tearing down aborts
|
|
392
|
+
// natively on Windows + Node 24 (UV_HANDLE_CLOSING) — exit 127 AFTER the
|
|
393
|
+
// message prints, so a caller reads "tooling broken" instead of "no building
|
|
394
|
+
// version". The other exits in this file are pre-request or have substantial
|
|
395
|
+
// rendering between; this branch has neither. See scripts/gds/CLAUDE.md.
|
|
396
|
+
return cliExit(3);
|
|
397
|
+
}
|
|
398
|
+
const data = await fetchProgress(picked.version);
|
|
329
399
|
|
|
330
400
|
// Resolve the requested criterion (if any) once — every output mode needs it.
|
|
331
401
|
let c = null;
|
|
@@ -333,7 +403,10 @@ async function main() {
|
|
|
333
403
|
const n = Number(opts.criterion.slice(1));
|
|
334
404
|
c = data.criteria.find((x) => x.cnum === n);
|
|
335
405
|
if (!c) {
|
|
336
|
-
|
|
406
|
+
// Name the version, not just the criterion: on a DEFAULTED version this is
|
|
407
|
+
// the reader's only clue that C13 was looked up somewhere they didn't pick.
|
|
408
|
+
console.error(`status: ${picked.version} has no criterion ${opts.criterion} (it has C1–C${data.criteria.length}).`);
|
|
409
|
+
if (picked.defaulted) console.error(` ${defaultedVersionNote(picked)}`);
|
|
337
410
|
process.exit(3);
|
|
338
411
|
}
|
|
339
412
|
}
|
|
@@ -356,9 +429,12 @@ async function main() {
|
|
|
356
429
|
// else the whole version rollup).
|
|
357
430
|
if (opts.json) { console.log(JSON.stringify(c || data, null, 2)); return; }
|
|
358
431
|
|
|
359
|
-
// Default human output — the existing plain-text rollup,
|
|
360
|
-
|
|
361
|
-
|
|
432
|
+
// Default human output — the existing plain-text rollup, plus one line when
|
|
433
|
+
// the version was resolved rather than chosen (--json/--widget/--hook are
|
|
434
|
+
// machine interfaces and stay byte-identical).
|
|
435
|
+
if (c) printOne(data, c); else printAll(data);
|
|
436
|
+
const note = defaultedVersionNote(picked);
|
|
437
|
+
if (note) console.log(` ${note}\n`);
|
|
362
438
|
}
|
|
363
439
|
|
|
364
440
|
if (require.main === module) {
|
|
@@ -368,4 +444,4 @@ if (require.main === module) {
|
|
|
368
444
|
});
|
|
369
445
|
}
|
|
370
446
|
|
|
371
|
-
module.exports = { parseArgs, renderStatusWidget, renderStatusMarkdown };
|
|
447
|
+
module.exports = { parseArgs, resolveVersion, defaultedVersionNote, renderStatusWidget, renderStatusMarkdown };
|
|
@@ -35,13 +35,31 @@ const VERSION_LITERAL = String.raw`['"][A-Z][A-Z0-9]*-V\d+['"]`;
|
|
|
35
35
|
const VERSION_FIELD = String.raw`version_id|versionId|suggested_version|target_version|DEFAULT_VERSION|VERSION_ID`;
|
|
36
36
|
const ASSIGNED = new RegExp(`(?:${VERSION_FIELD})\\s*['"]?\\s*[:=][^,;)]*${VERSION_LITERAL}`);
|
|
37
37
|
const DEFAULTED = new RegExp(`--version[^\\n]*?\\|\\|\\s*${VERSION_LITERAL}`);
|
|
38
|
+
// A version field FALLING BACK to a literal through `||`. ASSIGNED cannot see
|
|
39
|
+
// this shape: its `[^,;)]*` stops at the first `)`, so any call between the `=`
|
|
40
|
+
// and the literal hides it — `DEFAULT_VERSION = resolveEnv('X') || 'BONGOS-V1'`
|
|
41
|
+
// scanned CLEAN while `DEFAULT_VERSION = 'BONGOS-V1'` was caught (task 1004071).
|
|
42
|
+
// That is the exact line that served a sealed version as /status's default for
|
|
43
|
+
// months. Kept as its own narrow pattern rather than loosening ASSIGNED to
|
|
44
|
+
// `[^;]*`, which would fire on any line merely mentioning a field and a literal
|
|
45
|
+
// — the over-broad source assertion this repo keeps relearning (learning 1000196).
|
|
46
|
+
const FALLBACK = new RegExp(`(?:${VERSION_FIELD})\\s*['"]?\\s*[:=][^;]*?\\|\\|\\s*${VERSION_LITERAL}`);
|
|
38
47
|
|
|
39
48
|
// path::reason. A justified entry, one line, like every other guard here.
|
|
49
|
+
//
|
|
50
|
+
// A REASON MAY CITE THE CODE'S SHAPE, NEVER THE DATABASE'S CONTENTS (task
|
|
51
|
+
// 1004071). status.js was exempted because "TWO versions are currently building
|
|
52
|
+
// — so 'most recent building' is not a derivable answer here." True the day it
|
|
53
|
+
// was written; false once CB-V1 sealed. Nothing re-checks prose, so the entry
|
|
54
|
+
// outlived its justification in silence and /status went on serving a SHIPPED
|
|
55
|
+
// version's closing scoreboard as the project's current status. An entry whose
|
|
56
|
+
// truth depends on rows is a timer nobody set. `version_literals.mjs` now fails
|
|
57
|
+
// CI on an entry that is outright DEAD; it cannot police one that is merely
|
|
58
|
+
// stale, which is why that judgement has to be made here, when the entry is
|
|
59
|
+
// written.
|
|
40
60
|
const VERSION_LITERAL_ALLOW = new Map([
|
|
41
61
|
['scripts/gds/grade-smoke.js',
|
|
42
62
|
'GOOD_TASK / BAD_TASK are in-memory fixtures handed to grader.gradeSubagent; they are never POSTed, so no version_closed can refuse them and the literal cannot rot into a failure.'],
|
|
43
|
-
['scripts/gds/status.js',
|
|
44
|
-
'DEFAULT_VERSION is an owner-facing dashboard default with an env override, and TWO versions are currently building — so "most recent building" is not a derivable answer here. Retargeting what /status shows by default is an owner decision, not a refactor.'],
|
|
45
63
|
]);
|
|
46
64
|
|
|
47
65
|
function isExempt(rel) {
|
|
@@ -67,7 +85,7 @@ function scanScripts(files, read) {
|
|
|
67
85
|
scanned += 1;
|
|
68
86
|
if (VERSION_LITERAL_ALLOW.has(rel)) continue;
|
|
69
87
|
stripComments(src, rel).forEach((line, i) => {
|
|
70
|
-
if (ASSIGNED.test(line) || DEFAULTED.test(line)) {
|
|
88
|
+
if (ASSIGNED.test(line) || DEFAULTED.test(line) || FALLBACK.test(line)) {
|
|
71
89
|
violations.push(
|
|
72
90
|
`${rel}:${i + 1} names a version id where one should be RESOLVED: ${line.trim().slice(0, 90)}`
|
|
73
91
|
+ ' — a literal works until that version ships, then version_closed refuses every write and the'
|
|
@@ -119,4 +137,4 @@ function main() {
|
|
|
119
137
|
|
|
120
138
|
if (require.main === module) main();
|
|
121
139
|
|
|
122
|
-
module.exports = { checkNoVersionLiterals, scanScripts, stripComments, isExempt, VERSION_LITERAL_ALLOW, ASSIGNED, DEFAULTED };
|
|
140
|
+
module.exports = { checkNoVersionLiterals, scanScripts, stripComments, isExempt, VERSION_LITERAL_ALLOW, ASSIGNED, DEFAULTED, FALLBACK };
|
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.891'; // 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');
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// tests/status_default_version.mjs — /status resolves its default version at
|
|
2
|
+
// RUNTIME and never falls back to a sealed one (task 1004071).
|
|
3
|
+
//
|
|
4
|
+
// THE INCIDENT. `scripts/gds/status.js:29` read
|
|
5
|
+
// const DEFAULT_VERSION = resolveEnv('STATUS_DEFAULT_VERSION') || 'BONGOS-V1';
|
|
6
|
+
// while the comment three lines above stated the rule it was meant to keep:
|
|
7
|
+
// "the bare /status card should default to the live version, not a sealed one."
|
|
8
|
+
// BONGOS-V1 was later sealed and the literal was never bumped, so bare `/status`
|
|
9
|
+
// served a shipped version's closing scoreboard — 140/203 satisfied — as the
|
|
10
|
+
// project's CURRENT status. The output looks perfectly healthy, which is why it
|
|
11
|
+
// sat: nothing in the header says `shipped`, and /status is exactly what a
|
|
12
|
+
// session runs when it does NOT already know the answer and so cannot tell.
|
|
13
|
+
//
|
|
14
|
+
// WHY THE GUARD DID NOT CATCH IT. version-literal-guard.js already hunts this
|
|
15
|
+
// shape and already owns the remedy (version-select.js). status.js was on its
|
|
16
|
+
// allowlist, justified by "TWO versions are currently building — so 'most recent
|
|
17
|
+
// building' is not a derivable answer here." That was TRUE when written
|
|
18
|
+
// (BONGOS-V1 + CB-V1). It is false now: one version is building. The entry did
|
|
19
|
+
// not become wrong through anyone's error — the world moved under a justification
|
|
20
|
+
// that was a claim about live state, with nothing re-checking it. That staleness
|
|
21
|
+
// is the class defect, and `version_literals.mjs` now pins it.
|
|
22
|
+
//
|
|
23
|
+
// The rule these tests hold: an EXPLICIT choice always wins (flag, then env),
|
|
24
|
+
// a default is RESOLVED, and when nothing is building we stop rather than guess.
|
|
25
|
+
import { strict as assert } from 'node:assert';
|
|
26
|
+
import { test } from 'node:test';
|
|
27
|
+
import { createRequire } from 'node:module';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
|
|
31
|
+
const require = createRequire(import.meta.url);
|
|
32
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
33
|
+
const status = require(path.join(ROOT, 'scripts', 'gds', 'status.js'));
|
|
34
|
+
const { parseArgs, resolveVersion } = status;
|
|
35
|
+
|
|
36
|
+
// A deps bundle with nothing configured: no env override, nothing building.
|
|
37
|
+
const deps = (over = {}) => ({
|
|
38
|
+
resolveEnv: () => null,
|
|
39
|
+
resolveDefaultVersion: async () => null,
|
|
40
|
+
...over,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ---- parseArgs no longer bakes the default in -----------------------------
|
|
44
|
+
test('parseArgs leaves version NULL when none is given, so main can resolve it', () => {
|
|
45
|
+
const o = parseArgs(['node', 'status.js']);
|
|
46
|
+
assert.equal(o.version, null, 'a literal default here is the bug this task fixes');
|
|
47
|
+
assert.equal(o.criterion, null, 'and nothing was mistaken for a criterion');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('an explicit --version is still carried through', () => {
|
|
51
|
+
const o = parseArgs(['node', 'status.js', '--version', 'BONGOS-V2']);
|
|
52
|
+
assert.equal(o.version, 'BONGOS-V2');
|
|
53
|
+
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('a bare version token is carried through too', () => {
|
|
57
|
+
const o = parseArgs(['node', 'status.js', 'BONGOS-V2']);
|
|
58
|
+
assert.equal(o.version, 'BONGOS-V2');
|
|
59
|
+
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('a bare Cn token is a criterion, not a version', () => {
|
|
63
|
+
const o = parseArgs(['node', 'status.js', 'C8']);
|
|
64
|
+
assert.equal(o.criterion, 'C8');
|
|
65
|
+
assert.equal(o.version, null, 'C8 must not be mistaken for a version id');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ---- the resolution order -------------------------------------------------
|
|
69
|
+
test('an explicit flag beats both the env override and the building lookup', async () => {
|
|
70
|
+
const r = await resolveVersion({ version: 'GDS-V3' }, deps({
|
|
71
|
+
resolveEnv: () => 'ENV-V9',
|
|
72
|
+
resolveDefaultVersion: async () => 'BONGOS-V2',
|
|
73
|
+
}));
|
|
74
|
+
assert.equal(r.version, 'GDS-V3');
|
|
75
|
+
assert.equal(r.defaulted, false, 'the user chose it, so nothing was defaulted');
|
|
76
|
+
assert.equal(r.source, 'flag');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('the env override beats the building lookup, and still counts as defaulted', async () => {
|
|
80
|
+
const r = await resolveVersion({ version: null }, deps({
|
|
81
|
+
resolveEnv: () => 'ENV-V9',
|
|
82
|
+
resolveDefaultVersion: async () => 'BONGOS-V2',
|
|
83
|
+
}));
|
|
84
|
+
assert.equal(r.version, 'ENV-V9');
|
|
85
|
+
assert.equal(r.source, 'env');
|
|
86
|
+
assert.equal(r.defaulted, true, 'the user did not name it on this invocation');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('with no flag and no env, the BUILDING version is resolved at runtime', async () => {
|
|
90
|
+
const r = await resolveVersion({ version: null }, deps({
|
|
91
|
+
resolveDefaultVersion: async () => 'BONGOS-V2',
|
|
92
|
+
}));
|
|
93
|
+
assert.equal(r.version, 'BONGOS-V2');
|
|
94
|
+
assert.equal(r.source, 'building');
|
|
95
|
+
assert.equal(r.defaulted, true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('THE INCIDENT: when nothing is building we return null, never a sealed guess', async () => {
|
|
99
|
+
// version-select.js already refuses to guess here, and status must not
|
|
100
|
+
// reintroduce the guess downstream of it. A sealed version's final scoreboard
|
|
101
|
+
// presented as current status is worse than an error, because it looks fine.
|
|
102
|
+
const r = await resolveVersion({ version: null }, deps());
|
|
103
|
+
assert.equal(r.version, null);
|
|
104
|
+
assert.equal(r.source, 'none');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('a resolved default never returns a SHIPPED version', async () => {
|
|
108
|
+
// Wired through the real selection rule rather than a stub, so this test fails
|
|
109
|
+
// if pickDefaultVersion is ever loosened to fall back to the newest-any-status.
|
|
110
|
+
const { pickDefaultVersion } = require(path.join(ROOT, 'scripts', 'gds', 'version-select.js'));
|
|
111
|
+
const versions = [
|
|
112
|
+
{ id: 'BONGOS-V1', status: 'shipped', started_at: '2026-06-20' },
|
|
113
|
+
{ id: 'CB-V1', status: 'shipped', started_at: '2026-06-21' },
|
|
114
|
+
{ id: 'BONGOS-V2', status: 'building', started_at: '2026-06-22' },
|
|
115
|
+
];
|
|
116
|
+
const r = await resolveVersion({ version: null }, deps({
|
|
117
|
+
resolveDefaultVersion: async () => pickDefaultVersion(versions),
|
|
118
|
+
}));
|
|
119
|
+
assert.equal(r.version, 'BONGOS-V2', 'the only building row wins over two newer-looking sealed ones');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ---- the drill-in must say which version it answered about ----------------
|
|
123
|
+
test('a DEFAULTED version is announced, so a cross-version answer cannot pass unnoticed', () => {
|
|
124
|
+
// Cn is version-scoped and the versions number independently: BONGOS-V1 C8 is
|
|
125
|
+
// `money-one-unit`, BONGOS-V2 C8 is `wa6-kernel-and-packs`. Asking for "C8"
|
|
126
|
+
// against a silently-defaulted version answers a different question than the
|
|
127
|
+
// one asked, and the old output gave the reader nothing to notice it by.
|
|
128
|
+
const note = status.defaultedVersionNote({ version: 'BONGOS-V2', defaulted: true, source: 'building' });
|
|
129
|
+
assert.match(note, /BONGOS-V2/, 'it names the version it resolved');
|
|
130
|
+
assert.match(note, /building/, 'and why that one');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('an explicitly chosen version is NOT announced — no noise when the user said it', () => {
|
|
134
|
+
const note = status.defaultedVersionNote({ version: 'BONGOS-V2', defaulted: false, source: 'flag' });
|
|
135
|
+
assert.equal(note, '', 'the user already knows which version they asked for');
|
|
136
|
+
});
|
|
@@ -199,3 +199,95 @@ test('ship-flow.js imports every cross-module function it calls', () => {
|
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
});
|
|
202
|
+
|
|
203
|
+
// ---- an exemption must not outlive its justification ----------------------
|
|
204
|
+
// Task 1004071. scripts/gds/status.js sat on VERSION_LITERAL_ALLOW justified by
|
|
205
|
+
// "TWO versions are currently building — so 'most recent building' is not a
|
|
206
|
+
// derivable answer here." That was true the day it was written (BONGOS-V1 and
|
|
207
|
+
// CB-V1). It is false now: one version builds. Nothing noticed, because the
|
|
208
|
+
// reason is a claim about LIVE STATE and an allowlist holds prose, which no CI
|
|
209
|
+
// step can re-check. The entry did not become wrong through anyone's error —
|
|
210
|
+
// the world moved under it. Hence the rule now written beside the Map: a reason
|
|
211
|
+
// may cite the CODE's shape, never the database's contents.
|
|
212
|
+
//
|
|
213
|
+
// SCANNING AN ALLOWLISTED PATH PROVES NOTHING. scanScripts applies the allowlist
|
|
214
|
+
// itself (`if (VERSION_LITERAL_ALLOW.has(rel)) continue`), so handing it an
|
|
215
|
+
// exempt path returns zero violations whatever the file says — the first draft
|
|
216
|
+
// of this test read that vacuous [] as "no literal" and reported every entry as
|
|
217
|
+
// dead, including grade-smoke.js, which plainly still carries two. The source is
|
|
218
|
+
// therefore re-scanned under a synthetic path that is on neither the allowlist
|
|
219
|
+
// nor isExempt's patterns, which is the only way to see what the guard WOULD say.
|
|
220
|
+
const PROBE = 'scripts/gds/__allowlist_probe__.js';
|
|
221
|
+
const scanUnexempt = (src) => g.scanScripts([PROBE], () => src);
|
|
222
|
+
|
|
223
|
+
test('the probe path really is unexempt — otherwise every check below is vacuous', () => {
|
|
224
|
+
assert.ok(!g.VERSION_LITERAL_ALLOW.has(PROBE), 'probe must not be allowlisted');
|
|
225
|
+
assert.ok(!g.isExempt(PROBE), 'probe must not match an isExempt pattern');
|
|
226
|
+
// And it must actually fire on a known-bad line, or a silent-empty scanner
|
|
227
|
+
// would let every assertion below pass for the wrong reason (learning: a
|
|
228
|
+
// static scan breaks by finding nothing, and green looks identical to working).
|
|
229
|
+
assert.equal(scanUnexempt(" version_id: 'PMS-V1',").violations.length, 1);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('no allowlist entry is DEAD — each exempt file still contains the shape it is exempt for', () => {
|
|
233
|
+
for (const [rel] of g.VERSION_LITERAL_ALLOW) {
|
|
234
|
+
let src = null;
|
|
235
|
+
try { src = read(...rel.split('/')); } catch { /* handled below */ }
|
|
236
|
+
assert.ok(src !== null, `${rel} is allowlisted but does not exist — drop the entry`);
|
|
237
|
+
assert.ok(
|
|
238
|
+
scanUnexempt(src).violations.length > 0,
|
|
239
|
+
`${rel} is on VERSION_LITERAL_ALLOW but no longer names a version literal — `
|
|
240
|
+
+ 'the exemption is dead and must be removed, or it will silently cover a '
|
|
241
|
+
+ 'literal someone reintroduces later.'
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('THE INCIDENT: status.js is no longer exempt, and names no version literal', () => {
|
|
247
|
+
// Both halves must hold. Dropping the entry while leaving the literal reds the
|
|
248
|
+
// guard; fixing the literal while leaving the entry leaves status.js covered by
|
|
249
|
+
// nothing for good. Scanned under the probe path so this stays honest even if
|
|
250
|
+
// someone re-adds the allowlist entry.
|
|
251
|
+
assert.ok(
|
|
252
|
+
!g.VERSION_LITERAL_ALLOW.has('scripts/gds/status.js'),
|
|
253
|
+
'status.js resolves its default at runtime now — it must not stay exempt'
|
|
254
|
+
);
|
|
255
|
+
assert.deepEqual(scanUnexempt(read('scripts', 'gds', 'status.js')).violations, []);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ---- the fallback shape ASSIGNED could not see ----------------------------
|
|
259
|
+
test('THE BLIND SPOT: a version field falling back to a literal through || is caught', () => {
|
|
260
|
+
// The literal line that shipped, verbatim. ASSIGNED misses it because its
|
|
261
|
+
// `[^,;)]*` stops at the `)` of resolveEnv(...) — so wrapping a literal in any
|
|
262
|
+
// call defeated the guard, while the bare assignment was caught.
|
|
263
|
+
const bad = "const DEFAULT_VERSION = resolveEnv('STATUS_DEFAULT_VERSION') || 'BONGOS-V1';";
|
|
264
|
+
assert.equal(g.ASSIGNED.test(bad), false, 'documents WHY a third pattern was needed');
|
|
265
|
+
assert.equal(g.FALLBACK.test(bad), true);
|
|
266
|
+
assert.equal(scanUnexempt(bad).violations.length, 1, 'and the scanner reports it');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('the bare assignment ASSIGNED already caught still fails', () => {
|
|
270
|
+
assert.equal(scanUnexempt("const DEFAULT_VERSION = 'BONGOS-V1';").violations.length, 1);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('FALLBACK stays narrow — it does not fire on a version field alone', () => {
|
|
274
|
+
// The over-broad source assertion this repo keeps relearning (learning 1000196).
|
|
275
|
+
// Loosening ASSIGNED to `[^;]*` would have flagged all of these.
|
|
276
|
+
for (const ok of [
|
|
277
|
+
" const v = versions.find((x) => x.version_id === id) || null;",
|
|
278
|
+
" if (!version_id) throw new Error('need a version');",
|
|
279
|
+
" console.log(`resolved ${version_id}`);",
|
|
280
|
+
" const label = version_id || 'unknown';",
|
|
281
|
+
]) {
|
|
282
|
+
assert.deepEqual(scanUnexempt(ok).violations, [], `must not flag: ${ok.trim()}`);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('a comment showing the old bad line does not re-trip the guard', () => {
|
|
287
|
+
// status.js now quotes its own former literal in the explanatory comment. If
|
|
288
|
+
// comment-stripping regressed, the file it documents would red the guard.
|
|
289
|
+
assert.deepEqual(
|
|
290
|
+
scanUnexempt("// const DEFAULT_VERSION = resolveEnv('X') || 'BONGOS-V1';").violations,
|
|
291
|
+
[]
|
|
292
|
+
);
|
|
293
|
+
});
|