@bongos/core 1.19.540 → 1.19.541

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.540",
6
- "core_contract": "1.19.540",
7
- "source_commit": "5fc647cb0f3cec892a9ecf58528bbd0158250e44",
5
+ "core_version": "1.19.541",
6
+ "core_contract": "1.19.541",
7
+ "source_commit": "0f2805ada935b696e72031342663a4193c5c802d",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-04T16:31:49.656Z",
9
+ "built_at": "2026-09-04T16:58:05.057Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 441,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 1999,
14
+ "functional_verbatim": 2000,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2464,
20
- "tree_sha256": "36d8ec4b4166ff3dedaed13accf062736759569e3ef0dbaf8655662c4b896a9b",
19
+ "file_count": 2465,
20
+ "tree_sha256": "281153bdf107ee76f439badeb13dc3cbd70393ce4b70d52084e542a9581684f8",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -27,7 +27,7 @@
27
27
  {
28
28
  "path": ".claude/skills/blocker-solve/SKILL.md",
29
29
  "mode": "0000644",
30
- "sha256": "f9a8cc82815777f88d8732cdf7d72a3076af4f4081943696de20bae12defb814"
30
+ "sha256": "90de8b32fb4dac69d6e5c781d78489c36c40f00c4b07f062c0650cff08052203"
31
31
  },
32
32
  {
33
33
  "path": ".claude/skills/builder-backup/SKILL.md",
@@ -2647,7 +2647,7 @@
2647
2647
  {
2648
2648
  "path": "docs/module-api-changelog.md",
2649
2649
  "mode": "0000644",
2650
- "sha256": "8e3f784b21310cbe1b35bbbed75b512d9a1818810cf90eb303cc60008ae68253"
2650
+ "sha256": "263a5d16f7cd22a3b6a193a4cff05994a8fcf7a0522b561ba802392c3328b4f2"
2651
2651
  },
2652
2652
  {
2653
2653
  "path": "docs/modules-contract.md",
@@ -5237,7 +5237,7 @@
5237
5237
  {
5238
5238
  "path": "modules/ideas/blockers.js",
5239
5239
  "mode": "0000644",
5240
- "sha256": "73349f0209190305a61e2f4d771fa466c12be4f9034b4134b60a9e4a560e1c5a"
5240
+ "sha256": "d3feb342ef22f8a3952c249c7d09a2fa1c9d01ac45f4306f4084e2572df5c203"
5241
5241
  },
5242
5242
  {
5243
5243
  "path": "modules/ideas/completeness.js",
@@ -7407,12 +7407,12 @@
7407
7407
  {
7408
7408
  "path": "package-lock.json",
7409
7409
  "mode": "0000644",
7410
- "sha256": "2b42ac2bcb878baf1aa06fafc797c6f9b778eb989f46f1e1d53b46cf35b2124c"
7410
+ "sha256": "d1edde947ae28592c6bb37d8a935cc2c2c9c9ca45c9064c1579c8128d313e5ae"
7411
7411
  },
7412
7412
  {
7413
7413
  "path": "package.json",
7414
7414
  "mode": "0000644",
7415
- "sha256": "a461dae944c1fc1c49b88250e96d2b6a4eaff4b60a12e96b730f15b8b43bef2d"
7415
+ "sha256": "1f416b6226afeec3bb285d65c4d2bede292e7ad63c04a5701c54d7c59324c634"
7416
7416
  },
7417
7417
  {
7418
7418
  "path": "public-docs/index.html",
@@ -9097,7 +9097,7 @@
9097
9097
  {
9098
9098
  "path": "src/module-api.js",
9099
9099
  "mode": "0000644",
9100
- "sha256": "e1b39ba5609d7066607c7a1d90ebbef58237deab27cbe74d1cacef7e55a76a33"
9100
+ "sha256": "c9da65f8915d9cc8bb519797fb741a9644421066fbb97b29bb1d270fa874c1b4"
9101
9101
  },
9102
9102
  {
9103
9103
  "path": "src/module-loader/catalog.js",
@@ -9404,6 +9404,11 @@
9404
9404
  "mode": "0000644",
9405
9405
  "sha256": "445231e7fea9163e83ec0a76fce981415ed141760e34b165016a8039bba4da51"
9406
9406
  },
9407
+ {
9408
+ "path": "tests/blocker_detail_hydration.mjs",
9409
+ "mode": "0000644",
9410
+ "sha256": "fac81eb7827820e9e9ad03d4024d8fd9372f6a346a6931ac0a77692805ae2d5c"
9411
+ },
9407
9412
  {
9408
9413
  "path": "tests/blocker_link.mjs",
9409
9414
  "mode": "0000644",
@@ -41,7 +41,7 @@ Read the blocker fully, then enrich it:
41
41
  node scripts/gds/api.js GET /api/gds/blockers/<id>
42
42
  ```
43
43
 
44
- Fields: `title`, `body_md` (often already contains a numbered "Steps (owner): …" plan — lean on it), `source`, `source_ref`, `status`, plus `blocking_task_ids` from the list view (`GET /api/gds/blockers`). These linked tasks are exactly what will auto-promote when you resolve.
44
+ Fields: `title`, `body_md` (often already contains a numbered "Steps (owner): …" plan — lean on it), `source`, `source_ref`, `status`, plus `blocking_task_ids` (task 1002459 — the detail route now hydrates the same array the list view returns). These linked tasks are exactly what will auto-promote when you resolve.
45
45
 
46
46
  - Read any linked task's notes (`GET /api/gds/tasks/<task-id>`) — the source task usually explains what "done" means.
47
47
  - Pull project knowledge so you're not re-deriving: `node scripts/gds/recall.js "<the blocker's topic>"` (add `--kind doc` for ADRs/recipes). This surfaces the ADR, recipe, or past session that already documents how to do this thing.
@@ -1529,5 +1529,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1529
1529
  landed since 1.19.538 with no explicit bump. run 33892948885. (task 1002620)
1530
1530
  1.19.540 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1531
1531
  landed since 1.19.539 with no explicit bump. run 33895602533. (task 1002620)
1532
+ 1.19.541 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1533
+ landed since 1.19.540 with no explicit bump. run 33897974683. (task 1002620)
1532
1534
  ---------------------------------------------------------------------------
1533
1535
  ```
@@ -9,15 +9,20 @@
9
9
 
10
10
  const { pool } = require('../../src/module-api');
11
11
 
12
+ // Shared by listOpenBlockers + getBlocker so the two reads can never drift
13
+ // (task 1002459 — getBlocker used to omit this column entirely, so a
14
+ // link-then-verify against the detail route read as a false negative).
15
+ const BLOCKING_TASK_IDS_SQL = `COALESCE(
16
+ (SELECT array_agg(tb.task_id ORDER BY tb.task_id)
17
+ FROM task_blockers tb WHERE tb.blocker_id = b.id),
18
+ ARRAY[]::bigint[]
19
+ ) AS blocking_task_ids`;
20
+
12
21
  async function listOpenBlockers() {
13
22
  const { rows } = await pool.query(
14
23
  `SELECT b.id, b.title, b.body_md, b.status, b.created_at,
15
24
  b.source, b.source_ref,
16
- COALESCE(
17
- (SELECT array_agg(tb.task_id ORDER BY tb.task_id)
18
- FROM task_blockers tb WHERE tb.blocker_id = b.id),
19
- ARRAY[]::bigint[]
20
- ) AS blocking_task_ids
25
+ ${BLOCKING_TASK_IDS_SQL}
21
26
  FROM blockers b
22
27
  WHERE b.status = 'open'
23
28
  ORDER BY b.id`
@@ -27,9 +32,10 @@ async function listOpenBlockers() {
27
32
 
28
33
  async function getBlocker(id) {
29
34
  const { rows } = await pool.query(
30
- `SELECT id, title, body_md, status, created_at, resolved_at, resolved_by, resolution_note,
31
- source, source_ref
32
- FROM blockers WHERE id = $1`,
35
+ `SELECT b.id, b.title, b.body_md, b.status, b.created_at, b.resolved_at, b.resolved_by,
36
+ b.resolution_note, b.source, b.source_ref,
37
+ ${BLOCKING_TASK_IDS_SQL}
38
+ FROM blockers b WHERE b.id = $1`,
33
39
  [id]
34
40
  );
35
41
  return rows[0] || null;
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.540",
3
+ "version": "1.19.541",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.540",
9
+ "version": "1.19.541",
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.540",
3
+ "version": "1.19.541",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
package/src/module-api.js CHANGED
@@ -49,7 +49,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
49
49
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
50
50
  // the entry to that file. Look for a version's history there, not here.
51
51
  // ---------------------------------------------------------------------------
52
- const CORE_VERSION = '1.19.540'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
52
+ const CORE_VERSION = '1.19.541'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
53
53
 
54
54
  // A namespaced logger so a module's log lines are attributable + consistent.
55
55
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,128 @@
1
+ // tests/blocker_detail_hydration.mjs
2
+ //
3
+ // task 1002459 — GET /blockers/:id was missing `blocking_task_ids`, the array
4
+ // the list route (GET /blockers) already returns for the same row. A caller
5
+ // doing the natural link-then-verify (link a task, then re-fetch the blocker
6
+ // by id to confirm) read that as a false negative — the ids were never gone,
7
+ // `getBlocker`'s SELECT just never asked for them.
8
+ //
9
+ // The fix hoists the array_agg fragment into one shared constant
10
+ // (BLOCKING_TASK_IDS_SQL) so `listOpenBlockers` and `getBlocker` cannot drift
11
+ // apart again the way they did here. This test pins that: both queries must
12
+ // carry the identical fragment, and the detail route's SELECT must not add a
13
+ // status filter the list route has (task_blockers rows persist after resolve,
14
+ // so a resolved blocker's field reads as "tasks it WAS blocking" — a
15
+ // deliberate widening, not a bug, per the task's decision note).
16
+ //
17
+ // DB-free: `modules/ideas/blockers.js` destructures `pool` off the doorway at
18
+ // require time, so patching `pool.query` on that same object — BEFORE
19
+ // requiring blockers.js — intercepts every query. No live Postgres needed.
20
+ //
21
+ // Run: node tests/blocker_detail_hydration.mjs
22
+
23
+ import { strict as assert } from 'node:assert';
24
+ import { createRequire } from 'node:module';
25
+
26
+ const require = createRequire(import.meta.url);
27
+
28
+ const api = require('../src/module-api.js');
29
+
30
+ let queries = [];
31
+
32
+ api.pool.query = async (sql, params = []) => {
33
+ queries.push({ sql: String(sql), params });
34
+ if (/FROM blockers b\s+WHERE b\.status = 'open'/.test(sql)) {
35
+ return {
36
+ rows: [
37
+ { id: '1000085', title: 'the open one', body_md: '', status: 'open',
38
+ created_at: new Date(), source: 'manual', source_ref: null,
39
+ blocking_task_ids: ['1001838', '1001839'] },
40
+ ],
41
+ };
42
+ }
43
+ if (/FROM blockers b WHERE b\.id = \$1/.test(sql)) {
44
+ const id = String(params[0]);
45
+ if (id === '1000085') {
46
+ return {
47
+ rows: [
48
+ { id: '1000085', title: 'the open one', body_md: '', status: 'open',
49
+ created_at: new Date(), resolved_at: null, resolved_by: null,
50
+ resolution_note: null, source: 'manual', source_ref: null,
51
+ blocking_task_ids: ['1001838', '1001839'] },
52
+ ],
53
+ };
54
+ }
55
+ if (id === '1000099') {
56
+ // A resolved blocker: task_blockers rows persist, so the field still
57
+ // reports the tasks it WAS blocking (see comment above).
58
+ return {
59
+ rows: [
60
+ { id: '1000099', title: 'the resolved one', body_md: '', status: 'resolved',
61
+ created_at: new Date(), resolved_at: new Date(), resolved_by: '41',
62
+ resolution_note: 'done', source: 'manual', source_ref: null,
63
+ blocking_task_ids: ['1002000'] },
64
+ ],
65
+ };
66
+ }
67
+ return { rows: [] };
68
+ }
69
+ throw new Error(`unexpected query: ${sql}`);
70
+ };
71
+
72
+ const blockers = require('../modules/ideas/blockers.js');
73
+
74
+ let passed = 0;
75
+ let failed = 0;
76
+
77
+ async function test(name, fn) {
78
+ queries = [];
79
+ try {
80
+ await fn();
81
+ passed += 1;
82
+ console.log(` ok ${name}`);
83
+ } catch (err) {
84
+ failed += 1;
85
+ console.log(` FAIL ${name}\n ${err.message}`);
86
+ }
87
+ }
88
+
89
+ function blockingTaskIdsFragment(sql) {
90
+ const match = sql.match(/COALESCE\(\s*\(SELECT array_agg\(tb\.task_id[\s\S]*?blocking_task_ids/);
91
+ assert.ok(match, `query does not carry a blocking_task_ids projection:\n${sql}`);
92
+ return match[0].replace(/\s+/g, ' ').trim();
93
+ }
94
+
95
+ await test('listOpenBlockers and getBlocker project blocking_task_ids from the identical fragment', async () => {
96
+ await blockers.listOpenBlockers();
97
+ const listSql = queries[queries.length - 1].sql;
98
+
99
+ queries = [];
100
+ await blockers.getBlocker(1000085);
101
+ const detailSql = queries[queries.length - 1].sql;
102
+
103
+ assert.equal(blockingTaskIdsFragment(detailSql), blockingTaskIdsFragment(listSql));
104
+ });
105
+
106
+ await test('getBlocker returns blocking_task_ids on the row — the gap this task closes', async () => {
107
+ const row = await blockers.getBlocker(1000085);
108
+ assert.deepEqual(row.blocking_task_ids, ['1001838', '1001839']);
109
+ });
110
+
111
+ await test('getBlocker does not restrict by status — the list route is open-only, the detail route is not', async () => {
112
+ const detailSql = (await (async () => { await blockers.getBlocker(1000085); return queries[queries.length - 1].sql; })());
113
+ assert.ok(!/WHERE b\.status = 'open'/.test(detailSql), 'detail query must not gain the list route\'s open-only filter');
114
+ });
115
+
116
+ await test('a resolved blocker still reports the tasks it WAS blocking (task_blockers rows persist)', async () => {
117
+ const row = await blockers.getBlocker(1000099);
118
+ assert.equal(row.status, 'resolved');
119
+ assert.deepEqual(row.blocking_task_ids, ['1002000']);
120
+ });
121
+
122
+ await test('getBlocker still returns null for an id with no row', async () => {
123
+ const row = await blockers.getBlocker(999999);
124
+ assert.equal(row, null);
125
+ });
126
+
127
+ console.log(`\n${passed} passed, ${failed} failed`);
128
+ process.exit(failed ? 1 : 0);