@bongos/core 1.19.605 → 1.19.607

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 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.605",
6
- "core_contract": "1.19.605",
7
- "source_commit": "7d75ad596b8a0ebda56d47a18c68f8d7d0372a62",
5
+ "core_version": "1.19.607",
6
+ "core_contract": "1.19.607",
7
+ "source_commit": "a2068c523c1b4845be5d94b006e95f1d213717ed",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-08T23:30:23.409Z",
9
+ "built_at": "2026-09-08T23:51:21.550Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 458,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 2077,
14
+ "functional_verbatim": 2078,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2559,
20
- "tree_sha256": "54ebf8421271793cdf29f4b7b5cbf777661dfb39ec6bd1eaba4639b0245316c9",
19
+ "file_count": 2560,
20
+ "tree_sha256": "1e9d9d267269d69cf9e5cf933d04757d0d20961047ab7f9cd2bfed3cbc721bd7",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -2732,7 +2732,7 @@
2732
2732
  {
2733
2733
  "path": "docs/module-api-changelog.md",
2734
2734
  "mode": "0000644",
2735
- "sha256": "310d1c9b2d8b922f06253f9f93b4f6c4c43f1841c6c1dc8b65e3867371669dad"
2735
+ "sha256": "69fd2cbffee9e8396923dd5984eafe6b647809c1695b87f1d5d0543748187904"
2736
2736
  },
2737
2737
  {
2738
2738
  "path": "docs/modules-contract.md",
@@ -7632,12 +7632,12 @@
7632
7632
  {
7633
7633
  "path": "package-lock.json",
7634
7634
  "mode": "0000644",
7635
- "sha256": "e852049fb0b2971ebe8c8abb994d0f4239a97e81b56a355eafc055b057beaced"
7635
+ "sha256": "65e25f20c4196fe1064184914552bf3ffbd5fca85db7c6115084d5d5b1e9a210"
7636
7636
  },
7637
7637
  {
7638
7638
  "path": "package.json",
7639
7639
  "mode": "0000644",
7640
- "sha256": "d5eed87ff6d40d0f4abd5cd21db941c624240471beacd6dde3ce15b7d800221b"
7640
+ "sha256": "bfbaa26f7eb3571a6c520e890c2c55c8558643fc195d4a3e963896a22f8faef6"
7641
7641
  },
7642
7642
  {
7643
7643
  "path": "public-docs/index.html",
@@ -8399,6 +8399,11 @@
8399
8399
  "mode": "0000644",
8400
8400
  "sha256": "9898b6f8e60b9f5ee4aeb9a023d1b68bb5bfbdf589eed10923a8e492716340ed"
8401
8401
  },
8402
+ {
8403
+ "path": "scripts/gds/prove-strict-versioning.js",
8404
+ "mode": "0000644",
8405
+ "sha256": "dbaec451d7e67cfce811287ebb2289582ec47d73b1fbff92125e7991b734c807"
8406
+ },
8402
8407
  {
8403
8408
  "path": "scripts/gds/provision-config.js",
8404
8409
  "mode": "0000644",
@@ -9352,7 +9357,7 @@
9352
9357
  {
9353
9358
  "path": "src/module-api.js",
9354
9359
  "mode": "0000644",
9355
- "sha256": "2d74208cc55db7fd00751b651faf4389a8b345ccfb46bea127d678a49f56629a"
9360
+ "sha256": "504ef9943bde8940c3e32850ffb6206a3a2f0869cbd293e3482e7309ea0e3608"
9356
9361
  },
9357
9362
  {
9358
9363
  "path": "src/module-loader/catalog.js",
@@ -1659,5 +1659,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1659
1659
  landed since 1.19.603 with no explicit bump. run 34288910914. (task 1002620)
1660
1660
  1.19.605 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1661
1661
  landed since 1.19.604 with no explicit bump. run 34290892691. (task 1002620)
1662
+ 1.19.606 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1663
+ landed since 1.19.605 with no explicit bump. run 34291868670. (task 1002620)
1664
+ 1.19.607 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1665
+ landed since 1.19.606 with no explicit bump. run 34292436274. (task 1002620)
1662
1666
  ---------------------------------------------------------------------------
1663
1667
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.605",
3
+ "version": "1.19.607",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.605",
9
+ "version": "1.19.607",
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.605",
3
+ "version": "1.19.607",
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",
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/prove-strict-versioning.js — does strict versioning actually hold
3
+ // on THIS instance? (BV1.R30a, task 1003716, goal 1000086; the harness BV1.R30
4
+ // runs. ADR 0250, ADR 0263, ADR 0265.)
5
+ //
6
+ // ADR 0250 built five rules and ADR 0265 added a sixth, all enforced server-side.
7
+ // Nothing could tell you whether they HOLD on a given instance — R30's brief said
8
+ // "run each query against live and capture its output verbatim", which in practice
9
+ // meant hand-assembling a dozen API calls and reading the results by eye. That is
10
+ // how a proof ends up asserting what someone hoped rather than what the instance
11
+ // returned.
12
+ //
13
+ // THE CORE SHIPS TO ANY INSTANCE, so this is not a one-off for the BONGOS-V1
14
+ // cutover. "Is my scope actually closed?" is a question every instance will ask,
15
+ // and its answer should not depend on someone reproducing a call sequence.
16
+ //
17
+ // Two halves, and the split is deliberate:
18
+ //
19
+ // CHECKS are read-only and always run. They count the live rows and say what
20
+ // they found, whether or not it is what the rules promise.
21
+ //
22
+ // PROBES (--probe) attempt writes that MUST be refused. Each one is chosen so a
23
+ // refusal is the only legal outcome, and it is SKIPPED WITH A STATED REASON when
24
+ // the preconditions for a guaranteed refusal are absent — a probe that could
25
+ // succeed is a mutation, not a proof. Each also RE-READS afterwards: a 409 that
26
+ // nevertheless mutated is the failure mode worth catching, and no amount of
27
+ // reading the status code finds it.
28
+ //
29
+ // Exit code is non-zero if any check fails, so this can become a gate.
30
+ //
31
+ // Usage:
32
+ // node scripts/gds/prove-strict-versioning.js [--probe] [--json]
33
+ // CLOUDBONGOS_API_BASE=https://cloudbongos.com node scripts/gds/prove-strict-versioning.js --probe
34
+
35
+ // apiCall is the ONE HTTP client for this directory: it resolves the instance
36
+ // base and bearer, returns { status, ok, data } WITHOUT throwing on an HTTP
37
+ // status (exactly what a refusal probe needs), and routes through the
38
+ // no-keep-alive dispatcher — a bare fetch() leaves sockets armed and turns a
39
+ // natural exit into a hang.
40
+ const { apiCall, hasFlag, cliExit, API_BASE } = require('./cli-lib');
41
+
42
+ // The statuses that mean a task is FINISHED — one vocabulary, declared once
43
+ // (BV1.R01, task 1003588). Everything else holds its goal, and therefore its
44
+ // version, open. Read from the module rather than re-listed here so this file
45
+ // cannot drift from the rule it is checking.
46
+ const { TERMINAL_STATUSES } = require('../../modules/lifecycle/task-status');
47
+ const NON_TERMINAL = ['backlog', 'ready', 'active', 'blocked', 'completed', 'confirmed']
48
+ .filter((s) => !TERMINAL_STATUSES.includes(s));
49
+
50
+ // A goal that is no longer open must hold no unfinished work (ADR 0250 D2/D3).
51
+ const CLOSED_GOAL_STATUSES = ['achieved', 'archived'];
52
+
53
+ // A version's catch-all goal, by the two shapes the codebase has used. The
54
+ // em-dash form is what `generalGoalTitle` matches; "Work with no goal yet" is
55
+ // BONGOS-V1's, and the mismatch between them is exactly why 47 tasks went
56
+ // homeless unnoticed (R31, task 1003711). Match BOTH, or this proof inherits the
57
+ // blind spot it exists to close.
58
+ const CATCH_ALL = (title, versionId) =>
59
+ title === `${versionId} — general` || /^work with no goal yet$/i.test(String(title || '').trim());
60
+
61
+ // The API returns a refusal code in two shapes depending on which `res.fail`
62
+ // overload the route used — a bare string, or an object carrying `.code`/`.error`.
63
+ // Rendering the object straight gives `[object Object]`, which tells a reader
64
+ // nothing about WHICH rule fired. Normalise both to the code.
65
+ function errorCode(body) {
66
+ const e = body && body.error;
67
+ if (!e) return null;
68
+ if (typeof e === 'string') return e;
69
+ return e.code || e.error || e.message || null;
70
+ }
71
+
72
+ /** GET a Bongos API path, returning the parsed body. Throws on a non-2xx. */
73
+ async function get(path) {
74
+ const r = await apiCall('GET', `/api/gds${path}`);
75
+ if (!r.ok) throw new Error(`GET ${path} → ${r.status}`);
76
+ return r.data;
77
+ }
78
+
79
+ /** POST a probe. Returns the status WITHOUT throwing — the status IS the proof. */
80
+ async function probePost(path, body) {
81
+ const r = await apiCall('POST', `/api/gds${path}`, body || {});
82
+ return { status: r.status, body: r.data, why: errorCode(r.data) };
83
+ }
84
+
85
+ /** Every non-terminal task on the instance, one status at a time (the list route filters by one). */
86
+ async function allNonTerminalTasks() {
87
+ const out = [];
88
+ for (const status of NON_TERMINAL) {
89
+ const j = await get(`/tasks?status=${encodeURIComponent(status)}&limit=1000`);
90
+ out.push(...(j.tasks || []));
91
+ }
92
+ return out;
93
+ }
94
+
95
+ const results = [];
96
+ /** Record one check. `ok` false makes the whole run exit non-zero. */
97
+ function record(id, ok, headline, detail) {
98
+ results.push({ id, ok, headline, detail: detail || null });
99
+ }
100
+
101
+ async function runChecks() {
102
+ const [versionsRes, goalsRes, tasks] = await Promise.all([
103
+ get('/versions'), get('/goals'), allNonTerminalTasks(),
104
+ ]);
105
+ const versions = versionsRes.versions || [];
106
+ const goals = goalsRes.goals || [];
107
+ const byGoal = new Map(goals.map((g) => [String(g.id), g]));
108
+ const byVersion = new Map(versions.map((v) => [String(v.id), v]));
109
+
110
+ // C1 — a closed goal holding unfinished work (ADR 0250 D2/D3).
111
+ const c1 = tasks.filter((t) => {
112
+ const g = t.goal_id == null ? null : byGoal.get(String(t.goal_id));
113
+ return g && CLOSED_GOAL_STATUSES.includes(g.status);
114
+ });
115
+ const c1Goals = [...new Set(c1.map((t) => String(t.goal_id)))];
116
+ record('C1', c1.length === 0,
117
+ `${c1Goals.length} closed goal(s) holding ${c1.length} unfinished task(s)`,
118
+ c1Goals.map((id) => `goal ${id} (${byGoal.get(id).status}) holds ${c1.filter((t) => String(t.goal_id) === id).length}`));
119
+
120
+ // C2 — every task has a real goal (ADR 0250 D4). The two populations are
121
+ // reported apart because they fail for different reasons: a catch-all is a
122
+ // goal nobody chose, a null is a goal nobody assigned, and only the first is
123
+ // visible to anything that reasons per goal.
124
+ const nullGoal = tasks.filter((t) => t.goal_id == null);
125
+ const inCatchAll = tasks.filter((t) => {
126
+ const g = t.goal_id == null ? null : byGoal.get(String(t.goal_id));
127
+ return g && CATCH_ALL(g.title, g.version_id);
128
+ });
129
+ record('C2', nullGoal.length === 0 && inCatchAll.length === 0,
130
+ `${nullGoal.length} task(s) with no goal, ${inCatchAll.length} in a catch-all goal`,
131
+ [`null goal_id: ${nullGoal.length}`, `catch-all: ${inCatchAll.length}`,
132
+ ...[...new Set(inCatchAll.map((t) => String(t.goal_id)))].map((id) => ` catch-all goal ${id} — ${byGoal.get(id).title}`)]);
133
+
134
+ // C3 — one version builds, at most one plans (ADR 0250 D5; R04 + R19).
135
+ const building = versions.filter((v) => v.status === 'building');
136
+ const planning = versions.filter((v) => v.status === 'planning');
137
+ record('C3', building.length === 1 && planning.length <= 1,
138
+ `${building.length} building, ${planning.length} planning`,
139
+ [`building: ${building.map((v) => v.id).join(', ') || '(none)'}`,
140
+ `planning: ${planning.map((v) => v.id).join(', ') || '(none)'}`]);
141
+
142
+ // C4 — the next version is scoped: every goal on the planning version carries
143
+ // at least one done-when criterion (R29b). A criterion-less goal can never
144
+ // achieve, so it entombs its version the way the catch-alls entombed V1.
145
+ if (planning.length === 0) {
146
+ record('C4', true, 'no planning version — nothing to scope yet (not a failure)', null);
147
+ } else {
148
+ const pv = planning[0];
149
+ const pvGoals = goals.filter((g) => String(g.version_id) === String(pv.id) && g.status === 'open');
150
+ const detail = [];
151
+ let missing = 0;
152
+ for (const g of pvGoals) {
153
+ const full = await get(`/goals/${g.id}`);
154
+ const n = (full.criteria || []).length;
155
+ if (n === 0) { missing++; detail.push(`goal ${g.id} — NO criteria — ${g.title}`); }
156
+ }
157
+ record('C4', missing === 0,
158
+ `${pv.id}: ${pvGoals.length - missing}/${pvGoals.length} goal(s) carry a done-when criterion`, detail);
159
+ }
160
+
161
+ // C5 — beyond R30's original four. R32 (task 1003713) made a close refuse to
162
+ // leave work behind; this is the same rule read from the other end, and it
163
+ // catches what closed BEFORE that rule existed.
164
+ const onShipped = tasks.filter((t) => {
165
+ const v = byVersion.get(String(t.version_id));
166
+ return v && v.status === 'shipped';
167
+ });
168
+ record('C5', onShipped.length === 0,
169
+ `${onShipped.length} unfinished task(s) stranded on a shipped version`,
170
+ onShipped.slice(0, 20).map((t) => `task ${t.id} (${t.status}) on ${t.version_id}`));
171
+
172
+ // C6 — a task and its goal must be on the SAME version. `PATCH /tasks/:id`
173
+ // refuses a cross-version link with 409 `goal_version_mismatch`, but nothing
174
+ // has ever checked the rows that predate that rule — and the hierarchy is
175
+ // version → goal → task, so a split row is counted by one version's rollup
176
+ // while appearing under the other version's goal. It is also how C5 and a
177
+ // by-goal count can honestly disagree about the same task, which is exactly
178
+ // how this check got written.
179
+ const split = tasks.filter((t) => {
180
+ const g = t.goal_id == null ? null : byGoal.get(String(t.goal_id));
181
+ return g && String(g.version_id) !== String(t.version_id);
182
+ });
183
+ record('C6', split.length === 0,
184
+ `${split.length} task(s) whose goal lives on a different version`,
185
+ split.slice(0, 20).map((t) => `task ${t.id} on ${t.version_id}, but goal ${t.goal_id} is on ${byGoal.get(String(t.goal_id)).version_id}`));
186
+
187
+ return { versions, goals, tasks, building, planning, byGoal };
188
+ }
189
+
190
+ async function runProbes(ctx) {
191
+ const { goals, tasks, building, byGoal } = ctx;
192
+ const out = [];
193
+ /** A probe is only a proof when refusal is the sole legal outcome. */
194
+ const skip = (id, why) => out.push({ id, skipped: true, why });
195
+
196
+ // P1 — R03: a goal cannot join a version that is already building.
197
+ if (building.length === 0) {
198
+ skip('P1', 'no building version, so a refusal is not guaranteed');
199
+ } else {
200
+ const before = (await get('/goals')).goals.length;
201
+ const r = await probePost('/goals', { version_id: building[0].id, title: '__probe__ R03 refusal — must never be created' });
202
+ const after = (await get('/goals')).goals.length;
203
+ out.push({ absent: r.status === 404, id: 'P1', rule: 'R03 — goal onto a building version', status: r.status,
204
+ refused: r.status === 409, error: r.why, mutated: after !== before });
205
+ }
206
+
207
+ // P2 — R10: archive refuses without a disposition. Aimed at a goal VERIFIED to
208
+ // hold open tasks, because a goal holding none archives legitimately — that is
209
+ // the one shape of this probe that would destroy something.
210
+ const held = new Map();
211
+ for (const t of tasks) if (t.goal_id != null) held.set(String(t.goal_id), (held.get(String(t.goal_id)) || 0) + 1);
212
+ const target = goals.find((g) => g.status === 'open' && (held.get(String(g.id)) || 0) > 0);
213
+ if (!target) {
214
+ skip('P2', 'no open goal holds unfinished tasks, so a bare archive could succeed');
215
+ } else {
216
+ const r = await probePost(`/goals/${target.id}/archive`, {});
217
+ const after = (await get(`/goals/${target.id}`)).goal;
218
+ out.push({ absent: r.status === 404, id: 'P2', rule: `R10 — bare archive of goal ${target.id} (holds ${held.get(String(target.id))})`,
219
+ status: r.status, refused: r.status === 409, error: r.why, mutated: after.status !== 'open' });
220
+ }
221
+
222
+ // P3 — R12: a version holding open goals refuses a close with no dispositions.
223
+ if (building.length === 0) {
224
+ skip('P3', 'no building version to close');
225
+ } else {
226
+ const v = building[0];
227
+ const openGoals = goals.filter((g) => String(g.version_id) === String(v.id) && g.status === 'open');
228
+ if (openGoals.length === 0) {
229
+ skip('P3', `${v.id} holds no open goals, so a bare close could succeed`);
230
+ } else {
231
+ const r = await probePost(`/versions/${v.id}/close`, {});
232
+ const after = (await get('/versions')).versions.find((x) => String(x.id) === String(v.id));
233
+ out.push({ absent: r.status === 404, id: 'P3', rule: `R12 — bare close of ${v.id} (holds ${openGoals.length} open goals)`,
234
+ status: r.status, refused: r.status === 409, error: r.why, mutated: after.status !== 'building' });
235
+ }
236
+ }
237
+ return out;
238
+ }
239
+
240
+ (async () => {
241
+ const base = API_BASE;
242
+ const wantProbe = hasFlag('--probe');
243
+ const asJson = hasFlag('--json');
244
+
245
+ const ctx = await runChecks();
246
+ const probes = wantProbe ? await runProbes(ctx) : [];
247
+
248
+ if (asJson) {
249
+ console.log(JSON.stringify({ instance: base, at: new Date().toISOString(), checks: results, probes }, null, 2));
250
+ } else {
251
+ console.log(`\nStrict-versioning proof — ${base}`);
252
+ console.log(` ${new Date().toISOString()}\n`);
253
+ for (const r of results) {
254
+ console.log(` ${r.ok ? 'HOLDS ' : 'FAILS '} ${r.id} ${r.headline}`);
255
+ if (r.detail) for (const d of r.detail) console.log(` ${d}`);
256
+ }
257
+ if (wantProbe) {
258
+ console.log('\n Refusal probes (each write below MUST be refused):');
259
+ for (const p of probes) {
260
+ if (p.skipped) { console.log(` SKIP ${p.id} ${p.why}`); continue; }
261
+ const verdict = p.absent ? 'ABSENT ' : (p.refused && !p.mutated ? 'REFUSED' : 'LEAKED ');
262
+ console.log(` ${verdict} ${p.id} ${p.rule} → ${p.status}${p.error ? ` ${p.error}` : ''}`);
263
+ if (p.absent) console.log(' the route does not exist on this instance — the rule is built but NOT DEPLOYED here.');
264
+ if (p.mutated) console.log(' ⚠ THE STATE CHANGED ANYWAY — the refusal is cosmetic.');
265
+ }
266
+ } else {
267
+ console.log('\n (read-only; pass --probe to also attempt the three refusals)');
268
+ }
269
+ console.log('');
270
+ }
271
+
272
+ const failedChecks = results.filter((r) => !r.ok).length;
273
+ // A 404 is NOT a leak — the rule simply is not on this instance yet. Counting it
274
+ // as a failure would make the harness cry wolf on every instance running an
275
+ // older core, and the honest signal ("built, not deployed") is already printed.
276
+ const leakedProbes = probes.filter((p) => !p.skipped && !p.absent && (!p.refused || p.mutated)).length;
277
+ // cliExit, never process.exit — a hard exit while the request machinery is still
278
+ // tearing down aborts natively on Windows/Node 24 AFTER stdout has flushed, so
279
+ // the run looks fine and then exits 127 (this directory's CLAUDE.md; task 1002734).
280
+ cliExit(failedChecks + leakedProbes > 0 ? 1 : 0);
281
+ })().catch((err) => {
282
+ console.error('prove-strict-versioning failed:', err.message);
283
+ cliExit(2);
284
+ });
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.605'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
58
+ const CORE_VERSION = '1.19.607'; // 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');