@bongos/core 1.19.985 → 1.19.986
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 +18 -13
- package/docs/module-api-changelog.md +2 -0
- package/modules/lifecycle/db-ship.js +18 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/task_merge.mjs +24 -2
- package/tests/task_merge_route.mjs +93 -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.986",
|
|
6
|
+
"core_contract": "1.19.986",
|
|
7
|
+
"source_commit": "df54cfbda9edb7e3e5b7f953174e31e6a5b65bc4",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-23T18:
|
|
9
|
+
"built_at": "2026-09-23T18:36:30.871Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 518,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2390,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2934,
|
|
20
|
+
"tree_sha256": "8b898c04436c76259878da047c512541c0d0a03e9de13419b31e0a0d41ffa182",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2712,7 +2712,7 @@
|
|
|
2712
2712
|
{
|
|
2713
2713
|
"path": "docs/module-api-changelog.md",
|
|
2714
2714
|
"mode": "0000644",
|
|
2715
|
-
"sha256": "
|
|
2715
|
+
"sha256": "e779420483a2e19ea834a5995d1960296dad434fa158c1e46db3ee4e12b6569e"
|
|
2716
2716
|
},
|
|
2717
2717
|
{
|
|
2718
2718
|
"path": "docs/modules-contract.md",
|
|
@@ -5802,7 +5802,7 @@
|
|
|
5802
5802
|
{
|
|
5803
5803
|
"path": "modules/lifecycle/db-ship.js",
|
|
5804
5804
|
"mode": "0000644",
|
|
5805
|
-
"sha256": "
|
|
5805
|
+
"sha256": "c239b8cb88087f7bdb860c876e3e924bc1efef3cdba0ddec0374f71f7f2082d5"
|
|
5806
5806
|
},
|
|
5807
5807
|
{
|
|
5808
5808
|
"path": "modules/lifecycle/db-tasks.js",
|
|
@@ -8157,12 +8157,12 @@
|
|
|
8157
8157
|
{
|
|
8158
8158
|
"path": "package-lock.json",
|
|
8159
8159
|
"mode": "0000644",
|
|
8160
|
-
"sha256": "
|
|
8160
|
+
"sha256": "2fcebda4add1d874aee7ce5f756b37bb55485b6c001aaccde5b0f9624d4c0192"
|
|
8161
8161
|
},
|
|
8162
8162
|
{
|
|
8163
8163
|
"path": "package.json",
|
|
8164
8164
|
"mode": "0000644",
|
|
8165
|
-
"sha256": "
|
|
8165
|
+
"sha256": "80d5c9a1504e44bfccb0e3bfb0fd38e99eed359118d9d48f12bf9cab895f0533"
|
|
8166
8166
|
},
|
|
8167
8167
|
{
|
|
8168
8168
|
"path": "public-docs/index.html",
|
|
@@ -10187,7 +10187,7 @@
|
|
|
10187
10187
|
{
|
|
10188
10188
|
"path": "src/module-api.js",
|
|
10189
10189
|
"mode": "0000644",
|
|
10190
|
-
"sha256": "
|
|
10190
|
+
"sha256": "17399ab568a9343c5c5cead3e1e467af941ec891ee117b424ad0e2ed10151731"
|
|
10191
10191
|
},
|
|
10192
10192
|
{
|
|
10193
10193
|
"path": "src/module-loader/catalog.js",
|
|
@@ -14382,7 +14382,12 @@
|
|
|
14382
14382
|
{
|
|
14383
14383
|
"path": "tests/task_merge.mjs",
|
|
14384
14384
|
"mode": "0000644",
|
|
14385
|
-
"sha256": "
|
|
14385
|
+
"sha256": "716c421719fbf14a8024b7b34090ac50a8384fa3de9d35d1336f302c960c16c6"
|
|
14386
|
+
},
|
|
14387
|
+
{
|
|
14388
|
+
"path": "tests/task_merge_route.mjs",
|
|
14389
|
+
"mode": "0000644",
|
|
14390
|
+
"sha256": "24d411385ae7f8f21a0ba0e785257888052b3539c234a04f5bde797628e05654"
|
|
14386
14391
|
},
|
|
14387
14392
|
{
|
|
14388
14393
|
"path": "tests/task_ownership_gate.mjs",
|
|
@@ -2444,5 +2444,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2444
2444
|
landed since 1.19.983 with no explicit bump. run 35900593334. (task 1002620)
|
|
2445
2445
|
1.19.985 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2446
2446
|
landed since 1.19.984 with no explicit bump. run 35902000024. (task 1002620)
|
|
2447
|
+
1.19.986 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2448
|
+
landed since 1.19.985 with no explicit bump. run 35903379073. (task 1002620)
|
|
2447
2449
|
---------------------------------------------------------------------------
|
|
2448
2450
|
```
|
|
@@ -17,6 +17,7 @@ const seams = api;
|
|
|
17
17
|
const doneWhen = require('./done-when.js');
|
|
18
18
|
const { THETES_GRADUATION_THRESHOLD, maybeAutoGraduateToThetes, foldWorktreeName } = require('./db-rank-authz.js');
|
|
19
19
|
const { mostRecentClaimHolderId, sealSessionLog, staleClaimHourPredicate } = require('./db-shared.js');
|
|
20
|
+
const { LIVE_STATUSES } = require('./dead-deps.js');
|
|
20
21
|
|
|
21
22
|
// shouldRefuseClaimResolve — may THIS caller resolve THIS claim? (task 1003767)
|
|
22
23
|
//
|
|
@@ -1374,6 +1375,7 @@ async function mergeTask({ taskId, intoTaskId, reason }, deps = {}) {
|
|
|
1374
1375
|
return {
|
|
1375
1376
|
taskId, taskTitle: task.title, taskStatus: 'abandoned',
|
|
1376
1377
|
mergedIntoTaskId: Number(task.merged_into_task_id), alreadyMerged: true,
|
|
1378
|
+
strandedDependents: [],
|
|
1377
1379
|
};
|
|
1378
1380
|
}
|
|
1379
1381
|
throw Object.assign(
|
|
@@ -1391,7 +1393,7 @@ async function mergeTask({ taskId, intoTaskId, reason }, deps = {}) {
|
|
|
1391
1393
|
}
|
|
1392
1394
|
|
|
1393
1395
|
const { rows: targetRows } = await client.query(
|
|
1394
|
-
`SELECT id, merged_into_task_id FROM tasks WHERE id = $1`,
|
|
1396
|
+
`SELECT id, merged_into_task_id FROM tasks WHERE id = $1 FOR UPDATE`,
|
|
1395
1397
|
[intoTaskId]
|
|
1396
1398
|
);
|
|
1397
1399
|
if (targetRows.length === 0) {
|
|
@@ -1424,6 +1426,20 @@ async function mergeTask({ taskId, intoTaskId, reason }, deps = {}) {
|
|
|
1424
1426
|
[taskId, intoTaskId, noteText]
|
|
1425
1427
|
);
|
|
1426
1428
|
|
|
1429
|
+
// Abandoned blocks its dependents forever (owner ruling 2026-08-21, see this
|
|
1430
|
+
// module's CLAUDE.md), so the merge must say who it just stranded — the same
|
|
1431
|
+
// report scripts/gds/abandon.js prints. Live dependents only; read inside the
|
|
1432
|
+
// txn so it agrees with the row we just flipped.
|
|
1433
|
+
const { rows: stranded } = await client.query(
|
|
1434
|
+
`SELECT t.id, t.title, t.status
|
|
1435
|
+
FROM dependencies d
|
|
1436
|
+
JOIN tasks t ON t.id = d.from_id
|
|
1437
|
+
WHERE d.from_kind = 'task' AND d.to_kind = 'task' AND d.to_id = $1
|
|
1438
|
+
AND t.status = ANY($2::text[])
|
|
1439
|
+
ORDER BY t.id`,
|
|
1440
|
+
[taskId, [...LIVE_STATUSES]]
|
|
1441
|
+
);
|
|
1442
|
+
|
|
1427
1443
|
await client.query('COMMIT');
|
|
1428
1444
|
return {
|
|
1429
1445
|
taskId,
|
|
@@ -1432,6 +1448,7 @@ async function mergeTask({ taskId, intoTaskId, reason }, deps = {}) {
|
|
|
1432
1448
|
previousStatus: task.status,
|
|
1433
1449
|
mergedIntoTaskId: Number(intoTaskId),
|
|
1434
1450
|
alreadyMerged: false,
|
|
1451
|
+
strandedDependents: stranded.map((r) => ({ id: String(r.id), title: r.title, status: r.status })),
|
|
1435
1452
|
};
|
|
1436
1453
|
} catch (err) {
|
|
1437
1454
|
await client.query('ROLLBACK').catch(() => {});
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.986",
|
|
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.986",
|
|
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.986",
|
|
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
|
@@ -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.986'; // 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');
|
package/tests/task_merge.mjs
CHANGED
|
@@ -48,7 +48,7 @@ const { test, summary } = makeRunner();
|
|
|
48
48
|
function fakePool({
|
|
49
49
|
status = 'ready', notFound = false, mergedInto = null,
|
|
50
50
|
hasActiveClaim = false, targetNotFound = false, targetMergedInto = null,
|
|
51
|
-
updatedStatus = 'abandoned',
|
|
51
|
+
updatedStatus = 'abandoned', dependents = [],
|
|
52
52
|
} = {}) {
|
|
53
53
|
return makeSqlAwareClient((s, params) => {
|
|
54
54
|
if (/^SELECT id, title, status, merged_into_task_id FROM tasks WHERE id = \$1 FOR UPDATE/.test(s)) {
|
|
@@ -57,9 +57,12 @@ function fakePool({
|
|
|
57
57
|
if (/^SELECT id FROM claims WHERE task_id = \$1 AND released_at IS NULL/.test(s)) {
|
|
58
58
|
return { rows: hasActiveClaim ? [{ id: 999 }] : [] };
|
|
59
59
|
}
|
|
60
|
-
if (/^SELECT id, merged_into_task_id FROM tasks WHERE id = \$1/.test(s)) {
|
|
60
|
+
if (/^SELECT id, merged_into_task_id FROM tasks WHERE id = \$1 FOR UPDATE/.test(s)) {
|
|
61
61
|
return { rows: targetNotFound ? [] : [{ id: params[0], merged_into_task_id: targetMergedInto }] };
|
|
62
62
|
}
|
|
63
|
+
if (/^SELECT t\.id, t\.title, t\.status FROM dependencies d/.test(s)) {
|
|
64
|
+
return { rows: dependents };
|
|
65
|
+
}
|
|
63
66
|
if (/^UPDATE tasks/.test(s)) {
|
|
64
67
|
return { rows: [{ id: params[0], title: 'fixture bug', status: updatedStatus }] };
|
|
65
68
|
}
|
|
@@ -194,5 +197,24 @@ await test('merge: a target that is itself merged elsewhere is refused, naming w
|
|
|
194
197
|
assert.equal(all(pool._client.queries, /^UPDATE tasks/).length, 0);
|
|
195
198
|
});
|
|
196
199
|
|
|
200
|
+
await test('merge: the target row is read FOR UPDATE (no-chains guard is not a TOCTOU)', async () => {
|
|
201
|
+
const pool = fakePool({ status: 'ready' });
|
|
202
|
+
await mergeTask({ taskId: TASK, intoTaskId: TARGET }, { pool });
|
|
203
|
+
const q = find(pool._client.queries, /^SELECT id, merged_into_task_id FROM tasks WHERE id = \$1/);
|
|
204
|
+
assert.ok(q, 'target read must happen');
|
|
205
|
+
assert.match(String(q.sql).replace(/\s+/g, ' '), /FOR UPDATE/);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
await test('merge: reports the live dependents it just stranded', async () => {
|
|
209
|
+
const dependents = [{ id: 5, title: 'blocked work', status: 'backlog' }];
|
|
210
|
+
const pool = fakePool({ status: 'ready', dependents });
|
|
211
|
+
const out = await mergeTask({ taskId: TASK, intoTaskId: TARGET }, { pool });
|
|
212
|
+
assert.deepEqual(out.strandedDependents, [{ id: '5', title: 'blocked work', status: 'backlog' }]);
|
|
213
|
+
const none = await mergeTask({ taskId: TASK, intoTaskId: TARGET }, { pool: fakePool({ status: 'ready' }) });
|
|
214
|
+
assert.deepEqual(none.strandedDependents, []);
|
|
215
|
+
const again = await mergeTask({ taskId: TASK, intoTaskId: TARGET }, { pool: fakePool({ status: 'abandoned', mergedInto: TARGET }) });
|
|
216
|
+
assert.deepEqual(again.strandedDependents, [], 'idempotent re-merge keeps the field shape');
|
|
217
|
+
});
|
|
218
|
+
|
|
197
219
|
// -------------------------------------------------------------------------
|
|
198
220
|
summary();
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// tests/task_merge_route.mjs
|
|
2
|
+
//
|
|
3
|
+
// POST /tasks/:id/merge as an HTTP route (task 1003622). tests/task_merge.mjs
|
|
4
|
+
// drives db.mergeTask directly; nothing there proves the route validates input,
|
|
5
|
+
// maps each mergeTask error code to its status, or surfaces strandedDependents.
|
|
6
|
+
// DB-free: db.mergeTask is stubbed on the cached module object.
|
|
7
|
+
//
|
|
8
|
+
// Run: node --test tests/task_merge_route.mjs
|
|
9
|
+
|
|
10
|
+
import { test } from 'node:test';
|
|
11
|
+
import { strict as assert } from 'node:assert';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import http from 'node:http';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const db = require('../modules/lifecycle/db.js');
|
|
17
|
+
const api = require('../src/module-api.js');
|
|
18
|
+
|
|
19
|
+
let calls = [];
|
|
20
|
+
let impl = async () => ({});
|
|
21
|
+
db.mergeTask = async (args) => { calls.push(args); return impl(args); };
|
|
22
|
+
api.requireBuilder = (req, _res, next) => { req.builder = { id: 7, rank: 'metic' }; next(); };
|
|
23
|
+
api.requirePermission = () => (_req, _res, next) => next();
|
|
24
|
+
|
|
25
|
+
const express = require('express');
|
|
26
|
+
const buildRouter = require('../modules/lifecycle/routes/tasks.js');
|
|
27
|
+
const app = express();
|
|
28
|
+
app.use(express.json());
|
|
29
|
+
app.use((_req, res, next) => {
|
|
30
|
+
res.fail = (code, opts) => { const o = typeof opts === 'number' ? { status: opts } : (opts || {}); res.status(o.status || 400).json({ error: { code, ...o } }); };
|
|
31
|
+
next();
|
|
32
|
+
});
|
|
33
|
+
app.use('/api/bongos', buildRouter());
|
|
34
|
+
const server = app.listen(0);
|
|
35
|
+
await new Promise((r) => server.once('listening', r));
|
|
36
|
+
const port = server.address().port;
|
|
37
|
+
|
|
38
|
+
function post(path, body) {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const data = JSON.stringify(body);
|
|
41
|
+
const req = http.request({ host: '127.0.0.1', port, path: `/api/bongos${path}`, method: 'POST',
|
|
42
|
+
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(data) } }, (res) => {
|
|
43
|
+
let d = '';
|
|
44
|
+
res.on('data', (c) => { d += c; });
|
|
45
|
+
res.on('end', () => { let j = null; try { j = JSON.parse(d); } catch { /* */ } resolve({ status: res.statusCode, body: j, raw: d }); });
|
|
46
|
+
});
|
|
47
|
+
req.on('error', reject);
|
|
48
|
+
req.end(data);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const fail = (code, extra = {}) => async () => { throw Object.assign(new Error(code), { code, ...extra }); };
|
|
53
|
+
|
|
54
|
+
test('success returns mergeTask result including strandedDependents', async () => {
|
|
55
|
+
calls = [];
|
|
56
|
+
impl = async () => ({ taskId: 10, mergedIntoTaskId: 20, strandedDependents: [{ id: '5', title: 't', status: 'backlog' }] });
|
|
57
|
+
const r = await post('/tasks/10/merge', { into_task_id: 20, reason: 'dup' });
|
|
58
|
+
assert.equal(r.status, 200, r.raw);
|
|
59
|
+
assert.equal(r.body.ok, true);
|
|
60
|
+
assert.deepEqual(r.body.strandedDependents, [{ id: '5', title: 't', status: 'backlog' }]);
|
|
61
|
+
assert.deepEqual(calls, [{ taskId: 10, intoTaskId: 20, reason: 'dup' }]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('missing into_task_id is rejected before mergeTask runs', async () => {
|
|
65
|
+
calls = [];
|
|
66
|
+
const r = await post('/tasks/10/merge', {});
|
|
67
|
+
assert.equal(r.status, 400, r.raw);
|
|
68
|
+
assert.equal(calls.length, 0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
for (const [code, status, extra] of [
|
|
72
|
+
['MERGE_SELF', 400, {}],
|
|
73
|
+
['TASK_NOT_FOUND', 404, {}],
|
|
74
|
+
['CANNOT_MERGE_SHIPPED', 409, {}],
|
|
75
|
+
['ALREADY_MERGED', 409, { merged_into: '30' }],
|
|
76
|
+
['TASK_HAS_ACTIVE_CLAIM', 409, {}],
|
|
77
|
+
['MERGE_TARGET_NOT_FOUND', 404, {}],
|
|
78
|
+
['MERGE_TARGET_MERGED', 409, { merged_into: '30' }],
|
|
79
|
+
]) {
|
|
80
|
+
test(`${code} maps to HTTP ${status}`, async () => {
|
|
81
|
+
impl = fail(code, extra);
|
|
82
|
+
const r = await post('/tasks/10/merge', { into_task_id: 20 });
|
|
83
|
+
assert.equal(r.status, status, r.raw);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
test('an unexpected error is a 500 merge_failed', async () => {
|
|
88
|
+
impl = fail('BOOM');
|
|
89
|
+
const r = await post('/tasks/10/merge', { into_task_id: 20 });
|
|
90
|
+
assert.equal(r.status, 500, r.raw);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test.after(() => server.close());
|