@bongos/core 1.19.556 → 1.19.558
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 +17 -17
- package/clients/bongos-client/index.d.ts +1 -2
- package/docs/api/openapi.json +3 -23
- package/docs/architecture.md +2 -1
- package/docs/module-api-changelog.md +4 -0
- package/modules/lifecycle/db-goals.js +18 -1
- package/modules/lifecycle/done-when.js +31 -2
- package/modules/lifecycle/routes/done-when.js +10 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/auto_satisfy_criteria.mjs +53 -1
- package/tests/goal_achievement.mjs +64 -1
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
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.558",
|
|
6
|
+
"core_contract": "1.19.558",
|
|
7
|
+
"source_commit": "3f1c35f7e8595d597e7174a4b57801739138e35a",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-06T16:
|
|
9
|
+
"built_at": "2026-09-06T16:50:32.542Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 443,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 2487,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "cf1dadb6f0fb444dffe307f0e5b6d7ad76eb9e3fc7833a3e034f456f45ecf4c3",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -392,7 +392,7 @@
|
|
|
392
392
|
{
|
|
393
393
|
"path": "clients/bongos-client/index.d.ts",
|
|
394
394
|
"mode": "0000644",
|
|
395
|
-
"sha256": "
|
|
395
|
+
"sha256": "24cbe2964b71993d9d6e55daaf0dc26c938f820c7c4f6c01579d9dd2508e89ba"
|
|
396
396
|
},
|
|
397
397
|
{
|
|
398
398
|
"path": "clients/bongos-client/index.mjs",
|
|
@@ -1782,12 +1782,12 @@
|
|
|
1782
1782
|
{
|
|
1783
1783
|
"path": "docs/api/openapi.json",
|
|
1784
1784
|
"mode": "0000644",
|
|
1785
|
-
"sha256": "
|
|
1785
|
+
"sha256": "1ad70f7f1cb7f53735f17fbd8e05287c9191a1d249493c928efb91692b491941"
|
|
1786
1786
|
},
|
|
1787
1787
|
{
|
|
1788
1788
|
"path": "docs/architecture.md",
|
|
1789
1789
|
"mode": "0000644",
|
|
1790
|
-
"sha256": "
|
|
1790
|
+
"sha256": "1fe7548e94bd9b29642bdddc8dacb201072782f181b517be2bac5fed1b1cd356"
|
|
1791
1791
|
},
|
|
1792
1792
|
{
|
|
1793
1793
|
"path": "docs/branding-contract.md",
|
|
@@ -2657,7 +2657,7 @@
|
|
|
2657
2657
|
{
|
|
2658
2658
|
"path": "docs/module-api-changelog.md",
|
|
2659
2659
|
"mode": "0000644",
|
|
2660
|
-
"sha256": "
|
|
2660
|
+
"sha256": "7fd202776ba59c21897773679f9f07221de79ba577a6d5e6500df52698b85386"
|
|
2661
2661
|
},
|
|
2662
2662
|
{
|
|
2663
2663
|
"path": "docs/modules-contract.md",
|
|
@@ -5432,7 +5432,7 @@
|
|
|
5432
5432
|
{
|
|
5433
5433
|
"path": "modules/lifecycle/db-goals.js",
|
|
5434
5434
|
"mode": "0000644",
|
|
5435
|
-
"sha256": "
|
|
5435
|
+
"sha256": "abba32642e123860fee7b33fe429294783480afd2e25a78f63f9d4af3b1613e8"
|
|
5436
5436
|
},
|
|
5437
5437
|
{
|
|
5438
5438
|
"path": "modules/lifecycle/db-grade.js",
|
|
@@ -5482,7 +5482,7 @@
|
|
|
5482
5482
|
{
|
|
5483
5483
|
"path": "modules/lifecycle/done-when.js",
|
|
5484
5484
|
"mode": "0000644",
|
|
5485
|
-
"sha256": "
|
|
5485
|
+
"sha256": "4e798aff55de6481ea9da3f8a08e68d99902f3eac4749e30d727517ff61176b5"
|
|
5486
5486
|
},
|
|
5487
5487
|
{
|
|
5488
5488
|
"path": "modules/lifecycle/est-advisory.js",
|
|
@@ -5637,7 +5637,7 @@
|
|
|
5637
5637
|
{
|
|
5638
5638
|
"path": "modules/lifecycle/routes/done-when.js",
|
|
5639
5639
|
"mode": "0000644",
|
|
5640
|
-
"sha256": "
|
|
5640
|
+
"sha256": "25b18aa82f5b2951f3a9560fa5285dc2793556303ed68ae0aa9ea781ec2d45c9"
|
|
5641
5641
|
},
|
|
5642
5642
|
{
|
|
5643
5643
|
"path": "modules/lifecycle/routes/gate-approvals.js",
|
|
@@ -7477,12 +7477,12 @@
|
|
|
7477
7477
|
{
|
|
7478
7478
|
"path": "package-lock.json",
|
|
7479
7479
|
"mode": "0000644",
|
|
7480
|
-
"sha256": "
|
|
7480
|
+
"sha256": "3d98dfe15e37d556fbb8b69fe49aefaa10ffb350560704361a420803e5bd1169"
|
|
7481
7481
|
},
|
|
7482
7482
|
{
|
|
7483
7483
|
"path": "package.json",
|
|
7484
7484
|
"mode": "0000644",
|
|
7485
|
-
"sha256": "
|
|
7485
|
+
"sha256": "94d2145a407d981305d11aa4f7b6fcb2997d831856b978836ceece7f91f48de5"
|
|
7486
7486
|
},
|
|
7487
7487
|
{
|
|
7488
7488
|
"path": "public-docs/index.html",
|
|
@@ -9177,7 +9177,7 @@
|
|
|
9177
9177
|
{
|
|
9178
9178
|
"path": "src/module-api.js",
|
|
9179
9179
|
"mode": "0000644",
|
|
9180
|
-
"sha256": "
|
|
9180
|
+
"sha256": "8e9e2a140d38ec1307d8c062aeb950ee5cadc17a0a166d5e96ec41f3b202c375"
|
|
9181
9181
|
},
|
|
9182
9182
|
{
|
|
9183
9183
|
"path": "src/module-loader/catalog.js",
|
|
@@ -9437,7 +9437,7 @@
|
|
|
9437
9437
|
{
|
|
9438
9438
|
"path": "tests/auto_satisfy_criteria.mjs",
|
|
9439
9439
|
"mode": "0000644",
|
|
9440
|
-
"sha256": "
|
|
9440
|
+
"sha256": "d63beea7b680a194d163aa5ad11806c9aa860de93d614195a05a33082e9f8adb"
|
|
9441
9441
|
},
|
|
9442
9442
|
{
|
|
9443
9443
|
"path": "tests/autonomous_fit_protected_paths.mjs",
|
|
@@ -10247,7 +10247,7 @@
|
|
|
10247
10247
|
{
|
|
10248
10248
|
"path": "tests/goal_achievement.mjs",
|
|
10249
10249
|
"mode": "0000644",
|
|
10250
|
-
"sha256": "
|
|
10250
|
+
"sha256": "3e157d5bf44370e4935b70203fe59e40cf9120d12c472f24ed7bd85cb33cbe91"
|
|
10251
10251
|
},
|
|
10252
10252
|
{
|
|
10253
10253
|
"path": "tests/goal_advisory.mjs",
|
|
@@ -243,7 +243,6 @@ export interface PostCostResponse { skipped?: boolean; reason?: unknown; id?: un
|
|
|
243
243
|
export interface PostDependenciesResponse { ok: boolean; dependency: unknown }
|
|
244
244
|
export interface PostDiscordChannelsReconcileResponse { ok: boolean; applied: unknown; failed: unknown; total: unknown; warnings: unknown }
|
|
245
245
|
export interface PostDoneWhenCriterionIdSatisfyRequest { satisfied_by_task_id?: number }
|
|
246
|
-
export interface PostDoneWhenCriterionIdSatisfyResponse { criterion: unknown; goal_achieved: unknown; goal: unknown }
|
|
247
246
|
export interface PostDoneWhenCriterionIdUnsatisfyResponse { criterion: unknown }
|
|
248
247
|
export interface PostGateApprovalsPrApproveResponse { ok: boolean; pr: unknown; already_clear?: boolean; message: unknown; head_sha?: unknown; status_set?: boolean; recheck?: unknown }
|
|
249
248
|
export interface PostGithubRepoVisibilityRequest { repo: string; visibility: "public" | "private" }
|
|
@@ -638,7 +637,7 @@ export interface BongosClient {
|
|
|
638
637
|
/** PATCH /done-when/{criterionId} — rank: metic+archon */
|
|
639
638
|
patchDoneWhenCriterionId(args: RequestArgs & { body: PatchDoneWhenCriterionIdRequest }): Promise<PatchDoneWhenCriterionIdResponse>;
|
|
640
639
|
/** POST /done-when/{criterionId}/satisfy — rank: metic+archon */
|
|
641
|
-
postDoneWhenCriterionIdSatisfy(args?: RequestArgs & { body?: PostDoneWhenCriterionIdSatisfyRequest }): Promise<
|
|
640
|
+
postDoneWhenCriterionIdSatisfy(args?: RequestArgs & { body?: PostDoneWhenCriterionIdSatisfyRequest }): Promise<ApiResponse>;
|
|
642
641
|
/** POST /done-when/{criterionId}/unsatisfy — rank: metic+archon */
|
|
643
642
|
postDoneWhenCriterionIdUnsatisfy(args?: RequestArgs): Promise<PostDoneWhenCriterionIdUnsatisfyResponse>;
|
|
644
643
|
/** GET /done-when/pending-review — rank: metic+archon */
|
package/docs/api/openapi.json
CHANGED
|
@@ -4397,14 +4397,7 @@
|
|
|
4397
4397
|
},
|
|
4398
4398
|
"responses": {
|
|
4399
4399
|
"200": {
|
|
4400
|
-
"description": "Success."
|
|
4401
|
-
"content": {
|
|
4402
|
-
"application/json": {
|
|
4403
|
-
"schema": {
|
|
4404
|
-
"$ref": "#/components/schemas/PostDoneWhenCriterionIdSatisfyResponse"
|
|
4405
|
-
}
|
|
4406
|
-
}
|
|
4407
|
-
}
|
|
4400
|
+
"description": "Success."
|
|
4408
4401
|
},
|
|
4409
4402
|
"400": {
|
|
4410
4403
|
"$ref": "#/components/responses/ValidationFailed"
|
|
@@ -19629,19 +19622,6 @@
|
|
|
19629
19622
|
},
|
|
19630
19623
|
"additionalProperties": false
|
|
19631
19624
|
},
|
|
19632
|
-
"PostDoneWhenCriterionIdSatisfyResponse": {
|
|
19633
|
-
"type": "object",
|
|
19634
|
-
"properties": {
|
|
19635
|
-
"criterion": {},
|
|
19636
|
-
"goal_achieved": {},
|
|
19637
|
-
"goal": {}
|
|
19638
|
-
},
|
|
19639
|
-
"required": [
|
|
19640
|
-
"criterion",
|
|
19641
|
-
"goal_achieved",
|
|
19642
|
-
"goal"
|
|
19643
|
-
]
|
|
19644
|
-
},
|
|
19645
19625
|
"PostDoneWhenCriterionIdUnsatisfyResponse": {
|
|
19646
19626
|
"type": "object",
|
|
19647
19627
|
"properties": {
|
|
@@ -22460,8 +22440,8 @@
|
|
|
22460
22440
|
}
|
|
22461
22441
|
},
|
|
22462
22442
|
"x-endpoint-count": 356,
|
|
22463
|
-
"x-schema-count":
|
|
22443
|
+
"x-schema-count": 390,
|
|
22464
22444
|
"x-undocumented-bodies": 9,
|
|
22465
|
-
"x-response-schemas":
|
|
22445
|
+
"x-response-schemas": 264,
|
|
22466
22446
|
"x-generated-by": "scripts/gds/gen-api-docs.js"
|
|
22467
22447
|
}
|
package/docs/architecture.md
CHANGED
|
@@ -120,7 +120,8 @@ tasks(id, version_id, title, description, status, touches text[], est_minutes, e
|
|
|
120
120
|
shipped = merged to main + deployed; credits awarded here (credit_log.reason =
|
|
121
121
|
'task.shipped' — ADR 0120 pays on land, not on confirm; which streams
|
|
122
122
|
actually pay depends on the instance reward mode, ADR 0146)
|
|
123
|
-
indexes: (version_id, status), (priority), GIN(touches)
|
|
123
|
+
indexes: (version_id, status), (priority), GIN(touches), (goal_id), (parent_task_id),
|
|
124
|
+
(created_by), (shipped_by) WHERE needs_rebase
|
|
124
125
|
task_blockers(task_id, blocker_ref) -- markdown anchors are canonical
|
|
125
126
|
claims(id, task_id, builder_id, worktree_name, claimed_at, released_at, outcome)
|
|
126
127
|
UNIQUE INDEX uniq_active_claim_per_task ON (task_id) WHERE released_at IS NULL
|
|
@@ -1561,5 +1561,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1561
1561
|
landed since 1.19.554 with no explicit bump. run 34045075186. (task 1002620)
|
|
1562
1562
|
1.19.556 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1563
1563
|
landed since 1.19.555 with no explicit bump. run 34045323629. (task 1002620)
|
|
1564
|
+
1.19.557 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1565
|
+
landed since 1.19.556 with no explicit bump. run 34045753301. (task 1002620)
|
|
1566
|
+
1.19.558 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1567
|
+
landed since 1.19.557 with no explicit bump. run 34046699120. (task 1002620)
|
|
1564
1568
|
---------------------------------------------------------------------------
|
|
1565
1569
|
```
|
|
@@ -15,6 +15,7 @@ const { pool } = api;
|
|
|
15
15
|
const doneWhen = require('./done-when.js');
|
|
16
16
|
const { deriveRequiredRank, highestRank } = require('./db-rank-authz.js');
|
|
17
17
|
const { nonTerminalSql } = require('./task-status');
|
|
18
|
+
const { SMOKE_NOT_LIKE } = require('./db-shared.js');
|
|
18
19
|
|
|
19
20
|
// -------------------------------------------------------------------------
|
|
20
21
|
// Goals (ADR 0086 §2 / BV1.R56 — task 1510)
|
|
@@ -199,16 +200,32 @@ async function achieveGoalIfComplete({ goalId }, deps = {}) {
|
|
|
199
200
|
if (!goal || goal.status !== 'open') return { achieved: false, goal: goal ?? null };
|
|
200
201
|
const { rows } = await activePool.query(
|
|
201
202
|
`SELECT count(*)::int AS total,
|
|
202
|
-
count(*) FILTER (WHERE NOT satisfied)::int AS unsatisfied
|
|
203
|
+
count(*) FILTER (WHERE NOT satisfied)::int AS unsatisfied,
|
|
204
|
+
(SELECT count(*)::int FROM tasks t
|
|
205
|
+
WHERE t.goal_id = $1 AND ${nonTerminalSql()}
|
|
206
|
+
AND ${SMOKE_NOT_LIKE.replace(/title/, 't.title')}) AS unfinished_tasks
|
|
203
207
|
FROM done_when_criteria
|
|
204
208
|
WHERE goal_id = $1`,
|
|
205
209
|
[goalId]
|
|
206
210
|
);
|
|
207
211
|
const total = rows[0]?.total ?? 0;
|
|
208
212
|
const unsatisfied = rows[0]?.unsatisfied ?? 0;
|
|
213
|
+
const unfinishedTasks = rows[0]?.unfinished_tasks ?? 0;
|
|
209
214
|
// Need ≥1 criterion and zero unsatisfied — confirming the LAST one is the
|
|
210
215
|
// trigger. An empty goal stays open (nothing could complete it).
|
|
211
216
|
if (total === 0 || unsatisfied > 0) return { achieved: false, goal };
|
|
217
|
+
// BV1.R08 (task 1003595, goal 1000086, ADR 0250 D2): AND no task in the goal is
|
|
218
|
+
// still non-terminal. Criteria alone were never the whole question — a task
|
|
219
|
+
// linked to NO criterion held nothing open, so a goal auto-achieved while
|
|
220
|
+
// carrying any number of backlog or ready tasks. Terminal is `shipped` or
|
|
221
|
+
// deliberately `abandoned` (task-status.js); `completed` and `confirmed` are
|
|
222
|
+
// mid-ship states that can still fail a grade or strand, so they hold the goal
|
|
223
|
+
// open too. Smoke rows are excluded the way version_progress excludes them — a
|
|
224
|
+
// test fixture must not be able to wedge a real goal open forever.
|
|
225
|
+
//
|
|
226
|
+
// This never blocks a SHIP; it declines only the goal's flip to achieved, and
|
|
227
|
+
// the goal simply stays open until its last task lands or is abandoned.
|
|
228
|
+
if (unfinishedTasks > 0) return { achieved: false, goal, unfinishedTasks };
|
|
212
229
|
const updated = await setGoalStatus({ goalId, status: 'achieved' }, deps);
|
|
213
230
|
return { achieved: true, goal: updated };
|
|
214
231
|
}
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
const { pool } = require('../../src/module-api');
|
|
27
27
|
const { nonTerminalSql } = require('./task-status');
|
|
28
|
+
const { SMOKE_NOT_LIKE } = require('./db-shared.js');
|
|
28
29
|
|
|
29
30
|
// isPendingReview — the ONE derivation of the "met — pending review" auto-flag
|
|
30
31
|
// (ADR 0086 §6 / BV1.R63): a criterion with >=1 linked task, every one shipped,
|
|
@@ -445,8 +446,33 @@ async function autoSatisfyShippedCriteria(exec, { taskId = null } = {}) {
|
|
|
445
446
|
// Cascade. Deliberately re-derived from the table rather than from `criteria`:
|
|
446
447
|
// a goal completes when ALL of its criteria are satisfied, and the ones that
|
|
447
448
|
// closed in THIS call may not be all of them. Mirrors db.achieveGoalIfComplete
|
|
448
|
-
// exactly — open goals only, >=1 criterion, zero unsatisfied
|
|
449
|
-
// so it sees the flips above
|
|
449
|
+
// exactly — open goals only, >=1 criterion, zero unsatisfied, AND no unfinished
|
|
450
|
+
// task (BV1.R09, task 1003596) — but on `exec`, so it sees the flips above
|
|
451
|
+
// inside the same transaction.
|
|
452
|
+
//
|
|
453
|
+
// THE THIRD CLAUSE, and why it must run HERE and not only in db-goals.js. This
|
|
454
|
+
// is the path that actually fires on every ship (`db-ship.js`) and every
|
|
455
|
+
// reconciler tick (`publish-reconciler.js`); `achieveGoalIfComplete` covers the
|
|
456
|
+
// manual `POST /done-when/:id/satisfy`. The two are a DELIBERATE duplicate —
|
|
457
|
+
// collapsing them into one helper creates a require cycle (db-goals requires
|
|
458
|
+
// done-when) — so the rule is written twice on purpose and both copies must
|
|
459
|
+
// move together, or a goal closes correctly through one door and wrongly
|
|
460
|
+
// through the other. ADR 0250 D2.
|
|
461
|
+
//
|
|
462
|
+
// `exec` is load-bearing for THIS clause specifically. The shipping task's flip
|
|
463
|
+
// to `shipped` is uncommitted at this point in the ship transaction; on the
|
|
464
|
+
// module pool the subquery would still see it as `confirmed` — non-terminal —
|
|
465
|
+
// and refuse to close a goal whose last task just landed. Every goal would then
|
|
466
|
+
// need a second, later trigger to close at all.
|
|
467
|
+
//
|
|
468
|
+
// Shipping is NOT blocked by any of this: the ship's own writes are untouched
|
|
469
|
+
// and this UPDATE simply matches no row, leaving the goal open.
|
|
470
|
+
//
|
|
471
|
+
// The subquery is index-served: migration 228 added idx_tasks_goal ON tasks
|
|
472
|
+
// (goal_id), so this is a small index scan per candidate goal, and the candidate
|
|
473
|
+
// set is only the goals whose criteria just closed in this call. (Raised in
|
|
474
|
+
// review off docs/architecture.md, whose tasks index list predated 228 and has
|
|
475
|
+
// been corrected.)
|
|
450
476
|
const goalIds = [...new Set(criteria.map((c) => c.goal_id).filter((g) => g != null))].map(Number);
|
|
451
477
|
if (goalIds.length === 0) return { criteria, goals: [] };
|
|
452
478
|
const { rows: goals } = await exec.query(
|
|
@@ -456,6 +482,9 @@ async function autoSatisfyShippedCriteria(exec, { taskId = null } = {}) {
|
|
|
456
482
|
AND g.status = 'open'
|
|
457
483
|
AND EXISTS (SELECT 1 FROM done_when_criteria d WHERE d.goal_id = g.id)
|
|
458
484
|
AND NOT EXISTS (SELECT 1 FROM done_when_criteria d WHERE d.goal_id = g.id AND NOT d.satisfied)
|
|
485
|
+
AND NOT EXISTS (SELECT 1 FROM tasks t WHERE t.goal_id = g.id
|
|
486
|
+
AND ${nonTerminalSql()}
|
|
487
|
+
AND ${SMOKE_NOT_LIKE.replace(/title/, 't.title')})
|
|
459
488
|
RETURNING g.id, g.title, g.version_id, g.status`,
|
|
460
489
|
[goalIds]
|
|
461
490
|
);
|
|
@@ -212,7 +212,16 @@ module.exports = function buildDoneWhenRouter() {
|
|
|
212
212
|
if (updated?.goal_id != null) {
|
|
213
213
|
goalResult = await db.achieveGoalIfComplete({ goalId: updated.goal_id });
|
|
214
214
|
}
|
|
215
|
-
|
|
215
|
+
// BV1.R08 (task 1003595): when the goal did NOT flip because it still holds
|
|
216
|
+
// unfinished work, say so. Confirming the last criterion and getting a bare
|
|
217
|
+
// `goal_achieved: false` back reads as a bug to the agent that just did it —
|
|
218
|
+
// it would retry, or file one. The count is the whole explanation.
|
|
219
|
+
res.json({
|
|
220
|
+
criterion: updated,
|
|
221
|
+
goal_achieved: goalResult.achieved,
|
|
222
|
+
goal: goalResult.goal,
|
|
223
|
+
...(goalResult.unfinishedTasks ? { goal_held_open_by_tasks: goalResult.unfinishedTasks } : {}),
|
|
224
|
+
});
|
|
216
225
|
}, { errorCode: 'satisfy_failed' }));
|
|
217
226
|
|
|
218
227
|
// PATCH /done-when/:criterionId — correct a criterion's TEXT (task 1003102).
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.558",
|
|
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.558",
|
|
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.558",
|
|
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.
|
|
52
|
+
const CORE_VERSION = '1.19.558'; // 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');
|
|
@@ -213,7 +213,6 @@ await test('closeCompletedWorkForShip: silent on the common no-op, and logs what
|
|
|
213
213
|
assert.match(loud[0], /shipTask 9/);
|
|
214
214
|
});
|
|
215
215
|
|
|
216
|
-
summary();
|
|
217
216
|
|
|
218
217
|
// ---- 4. the terminal vocabulary has exactly one spelling (task 1003588) ----
|
|
219
218
|
|
|
@@ -233,3 +232,56 @@ await test('VOCABULARY: no consumer re-types the terminal set behind the constan
|
|
|
233
232
|
f + ' must read the terminal set from task-status.js, not spell it again');
|
|
234
233
|
}
|
|
235
234
|
});
|
|
235
|
+
|
|
236
|
+
// ---- 5. the cascade carries R08's rule too (BV1.R09, task 1003596) ---------
|
|
237
|
+
//
|
|
238
|
+
// The two achievement writers are a DELIBERATE duplicate — collapsing them makes
|
|
239
|
+
// a require cycle (db-goals requires done-when) — so the danger is that only one
|
|
240
|
+
// moves and a goal then closes correctly through the manual door and wrongly
|
|
241
|
+
// through the ship. These cases pin the third clause on the path that actually
|
|
242
|
+
// fires on every ship and every reconciler tick.
|
|
243
|
+
|
|
244
|
+
await test('CASCADE (R09): an unfinished task in the goal blocks the flip', async () => {
|
|
245
|
+
const exec = fakeExec({ rows: [CRIT] }, { rows: [] });
|
|
246
|
+
await autoSatisfyShippedCriteria(exec, { taskId: 5 });
|
|
247
|
+
const cascade = String(exec.calls[1].sql).replace(/\s+/g, ' ');
|
|
248
|
+
assert.match(cascade, /NOT EXISTS \(SELECT 1 FROM tasks t WHERE t\.goal_id = g\.id/,
|
|
249
|
+
'satisfied criteria are necessary, not sufficient — ADR 0250 D2');
|
|
250
|
+
assert.match(cascade, /t\.status NOT IN \('shipped', 'abandoned'\)/,
|
|
251
|
+
'the terminal set is R01\'s, so completed/confirmed hold the goal open');
|
|
252
|
+
assert.match(cascade, /t\.title NOT LIKE/,
|
|
253
|
+
'a smoke fixture must not wedge a real goal open forever');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
await test('CASCADE (R09): the rule matches db-goals.js — both writers or neither', () => {
|
|
257
|
+
// The duplicate is intentional; the DRIFT is the bug. Both copies must ask for
|
|
258
|
+
// the same terminal set and the same smoke exclusion.
|
|
259
|
+
const dbGoals = readFileSync(new URL('../modules/lifecycle/db-goals.js', import.meta.url), 'utf8');
|
|
260
|
+
const doneWhen = readFileSync(new URL('../modules/lifecycle/done-when.js', import.meta.url), 'utf8');
|
|
261
|
+
for (const [name, src] of [['db-goals.js', dbGoals], ['done-when.js', doneWhen]]) {
|
|
262
|
+
assert.match(src, /FROM tasks t/, name + ' must consult tasks at all');
|
|
263
|
+
assert.match(src, /\$\{nonTerminalSql\(\)\}/,
|
|
264
|
+
name + ' must read the terminal set from task-status.js, not re-type it');
|
|
265
|
+
assert.match(src, /SMOKE_NOT_LIKE\.replace/,
|
|
266
|
+
name + ' must exclude smoke rows the way version_progress does');
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
await test('CASCADE (R09): the check runs on the caller exec, never the module pool', async () => {
|
|
271
|
+
// Load-bearing for this clause specifically: the shipping task's flip to
|
|
272
|
+
// `shipped` is uncommitted here, so on the module pool the subquery would still
|
|
273
|
+
// read it as `confirmed` — non-terminal — and refuse to close a goal whose last
|
|
274
|
+
// task just landed.
|
|
275
|
+
const exec = fakeExec({ rows: [CRIT] }, { rows: [] });
|
|
276
|
+
await autoSatisfyShippedCriteria(exec, { taskId: 5 });
|
|
277
|
+
assert.equal(exec.calls.length, 2, 'both statements go through the passed exec');
|
|
278
|
+
assert.match(String(exec.calls[1].sql), /UPDATE goals g/,
|
|
279
|
+
'the cascade UPDATE is the second, and it is the one carrying the task check');
|
|
280
|
+
const src = readFileSync(new URL('../modules/lifecycle/done-when.js', import.meta.url), 'utf8');
|
|
281
|
+
const start = src.indexOf('async function autoSatisfyShippedCriteria');
|
|
282
|
+
const body = src.slice(start, src.indexOf('describeAutoClose', start));
|
|
283
|
+
assert.equal(/\bpool\.query\(/.test(body), false,
|
|
284
|
+
'nothing in this function may reach the module pool');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
summary();
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { strict as assert } from 'node:assert';
|
|
22
22
|
import { createRequire } from 'node:module';
|
|
23
|
+
import { readFileSync } from 'node:fs';
|
|
23
24
|
import { makeRunner, makeSqlAwareClient } from './helpers.mjs';
|
|
24
25
|
|
|
25
26
|
process.env.NODE_ENV = 'test';
|
|
@@ -47,7 +48,10 @@ const GOAL = (over = {}) => ({
|
|
|
47
48
|
function goalHandler({ goal, counts }) {
|
|
48
49
|
return (s) => {
|
|
49
50
|
if (/^SELECT .*FROM goals WHERE id = \$1/.test(s)) return { rows: goal ? [goal] : [] };
|
|
50
|
-
|
|
51
|
+
// BV1.R08: the criteria count and the unfinished-task count come back in ONE
|
|
52
|
+
// row. `unfinished_tasks` defaults to 0 so every pre-existing case below still
|
|
53
|
+
// means what it meant — criteria alone decide it when no task is open.
|
|
54
|
+
if (/FROM done_when_criteria WHERE goal_id = \$1/.test(s)) return { rows: [{ unfinished_tasks: 0, ...counts }] };
|
|
51
55
|
if (/^UPDATE goals/.test(s)) return { rows: [{ ...goal, status: 'achieved', updated_at: 'T1' }] };
|
|
52
56
|
return null;
|
|
53
57
|
};
|
|
@@ -145,4 +149,63 @@ await test('listPendingReviewCriteria: SQL encodes the auto-flag (all shipped) +
|
|
|
145
149
|
});
|
|
146
150
|
|
|
147
151
|
// -------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
// =========================================================================
|
|
154
|
+
// BV1.R08 (task 1003595, goal 1000086, ADR 0250 D2) — criteria are not the whole
|
|
155
|
+
// question: a goal cannot close while it still holds non-terminal work.
|
|
156
|
+
//
|
|
157
|
+
// The hole this shuts: achievement was computed from done_when_criteria alone, so
|
|
158
|
+
// a task linked to NO criterion held nothing open and a goal auto-achieved while
|
|
159
|
+
// carrying any number of backlog or ready tasks. Fourteen goals on the live
|
|
160
|
+
// instance were in exactly that state, holding thirty-five tasks between them.
|
|
161
|
+
// =========================================================================
|
|
162
|
+
|
|
163
|
+
await test('R08: all criteria satisfied but ONE unfinished task remains → no flip', async () => {
|
|
164
|
+
const pool = fakePool(goalHandler({ goal: GOAL(), counts: { total: 3, unsatisfied: 0, unfinished_tasks: 1 } }));
|
|
165
|
+
const out = await achieveGoalIfComplete({ goalId: 7 }, { pool });
|
|
166
|
+
assert.equal(out.achieved, false, 'satisfied criteria are necessary, not sufficient');
|
|
167
|
+
assert.equal(out.goal.status, 'open', 'the goal stays open until the work lands');
|
|
168
|
+
assert.equal(out.unfinishedTasks, 1, 'and it reports WHY, so a caller can say so');
|
|
169
|
+
assert.equal(find(pool.queries, /^UPDATE goals/), undefined, 'must NOT flip the status');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await test('R08: the count is asked in the SAME round trip as the criteria count', async () => {
|
|
173
|
+
const pool = fakePool(goalHandler({ goal: GOAL(), counts: { total: 1, unsatisfied: 0, unfinished_tasks: 0 } }));
|
|
174
|
+
await achieveGoalIfComplete({ goalId: 7 }, { pool });
|
|
175
|
+
const q = find(pool.queries, /FROM done_when_criteria WHERE goal_id = \$1/);
|
|
176
|
+
const sql = String(q.sql).replace(/\s+/g, ' ');
|
|
177
|
+
assert.match(sql, /FROM tasks t WHERE t\.goal_id = \$1/, 'one query, not a second trip');
|
|
178
|
+
assert.match(sql, /t\.status NOT IN \('shipped', 'abandoned'\)/,
|
|
179
|
+
"the terminal set comes from task-status.js — mid-ship states hold the goal open");
|
|
180
|
+
assert.match(sql, /t\.title NOT LIKE/, 'a smoke fixture must not wedge a real goal open forever');
|
|
181
|
+
assert.deepEqual(q.params, [7], 'the goal id is bound once and reused');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await test('R08: zero unfinished tasks and all criteria satisfied → still flips', async () => {
|
|
185
|
+
// The rule must not become "goals never close". This is the whole happy path.
|
|
186
|
+
const pool = fakePool(goalHandler({ goal: GOAL(), counts: { total: 2, unsatisfied: 0, unfinished_tasks: 0 } }));
|
|
187
|
+
const out = await achieveGoalIfComplete({ goalId: 7 }, { pool });
|
|
188
|
+
assert.equal(out.achieved, true, 'finished work + satisfied criteria = achieved');
|
|
189
|
+
assert.ok(find(pool.queries, /^UPDATE goals/), 'the flip is persisted');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
await test('R08: an unsatisfied criterion still short-circuits before the task check', async () => {
|
|
193
|
+
// Ordering matters for the message, not the outcome: a goal with open criteria
|
|
194
|
+
// AND open tasks is refused on the criteria, and reports no task count.
|
|
195
|
+
const pool = fakePool(goalHandler({ goal: GOAL(), counts: { total: 3, unsatisfied: 2, unfinished_tasks: 9 } }));
|
|
196
|
+
const out = await achieveGoalIfComplete({ goalId: 7 }, { pool });
|
|
197
|
+
assert.equal(out.achieved, false);
|
|
198
|
+
assert.equal(out.unfinishedTasks, undefined,
|
|
199
|
+
'the criteria refusal is the honest reason when both are true');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await test('R08: the satisfy route tells the caller a goal was held open by work', async () => {
|
|
203
|
+
// A bare goal_achieved:false after confirming the LAST criterion reads as a bug
|
|
204
|
+
// to the agent that just did it. The route names the reason when there is one.
|
|
205
|
+
const src = readFileSync(new URL('../modules/lifecycle/routes/done-when.js', import.meta.url), 'utf8');
|
|
206
|
+
assert.match(src, /goal_held_open_by_tasks: goalResult\.unfinishedTasks/);
|
|
207
|
+
assert.match(src, /\.\.\.\(goalResult\.unfinishedTasks \?/,
|
|
208
|
+
'and stays silent on the ordinary path, so the response shape is unchanged when nothing is held');
|
|
209
|
+
});
|
|
210
|
+
|
|
148
211
|
summary();
|