@bongos/core 1.19.798 → 1.19.799
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 +23 -13
- package/docs/module-api-changelog.md +2 -0
- package/migrations/core_223_blocker_resolve_respects_other_open_blockers.sql +10 -0
- package/migrations/core_245_restore_open_blocker_clause.sql +97 -0
- package/modules/lifecycle/db-claims.js +4 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/task_unblocked_open_blocker.mjs +123 -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.799",
|
|
6
|
+
"core_contract": "1.19.799",
|
|
7
|
+
"source_commit": "85b2cf74b5fa2c392c59c47b0b877e9b912503ac",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-17T01:
|
|
9
|
+
"built_at": "2026-09-17T01:33:26.348Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 487,
|
|
13
13
|
"agent_docs_stubbed": 25,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2187,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2699,
|
|
20
|
+
"tree_sha256": "6435ea324232356a39b076f0f83fad40beaa96485c1f9e9b3a38fea29feda830",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2532,7 +2532,7 @@
|
|
|
2532
2532
|
{
|
|
2533
2533
|
"path": "docs/module-api-changelog.md",
|
|
2534
2534
|
"mode": "0000644",
|
|
2535
|
-
"sha256": "
|
|
2535
|
+
"sha256": "4ad2853bc510a9ce3bfa2264734afb42eb41f75a28c0a51f639d48038e62e440"
|
|
2536
2536
|
},
|
|
2537
2537
|
{
|
|
2538
2538
|
"path": "docs/modules-contract.md",
|
|
@@ -3507,7 +3507,7 @@
|
|
|
3507
3507
|
{
|
|
3508
3508
|
"path": "migrations/core_223_blocker_resolve_respects_other_open_blockers.sql",
|
|
3509
3509
|
"mode": "0000644",
|
|
3510
|
-
"sha256": "
|
|
3510
|
+
"sha256": "54eab3a0de0a3471649bd18a1cf3208e4857ef2d819811944843e89db3456ddf"
|
|
3511
3511
|
},
|
|
3512
3512
|
{
|
|
3513
3513
|
"path": "migrations/core_223_task_visual.sql",
|
|
@@ -3624,6 +3624,11 @@
|
|
|
3624
3624
|
"mode": "0000644",
|
|
3625
3625
|
"sha256": "e298a5b2e9ffade95fae0e68910a25396fab560e6e54dd745cfc122c0dfdf403"
|
|
3626
3626
|
},
|
|
3627
|
+
{
|
|
3628
|
+
"path": "migrations/core_245_restore_open_blocker_clause.sql",
|
|
3629
|
+
"mode": "0000644",
|
|
3630
|
+
"sha256": "b51a03436ede7269f32f6b1e9400070dcbe3895ab64088ca27da5406cbcf5eea"
|
|
3631
|
+
},
|
|
3627
3632
|
{
|
|
3628
3633
|
"path": "modules/agents/CLAUDE.md",
|
|
3629
3634
|
"mode": "0000644",
|
|
@@ -5507,7 +5512,7 @@
|
|
|
5507
5512
|
{
|
|
5508
5513
|
"path": "modules/lifecycle/db-claims.js",
|
|
5509
5514
|
"mode": "0000644",
|
|
5510
|
-
"sha256": "
|
|
5515
|
+
"sha256": "e2b9e236090bc1936bed98a2487e0db9ab10e3c543597ababd68e664a396d8f3"
|
|
5511
5516
|
},
|
|
5512
5517
|
{
|
|
5513
5518
|
"path": "modules/lifecycle/db-deps-criteria.js",
|
|
@@ -7707,12 +7712,12 @@
|
|
|
7707
7712
|
{
|
|
7708
7713
|
"path": "package-lock.json",
|
|
7709
7714
|
"mode": "0000644",
|
|
7710
|
-
"sha256": "
|
|
7715
|
+
"sha256": "1b8fa8ffcdeac7490e95f849daa69ef5580ffce9da1eed6d3434990e29dc957d"
|
|
7711
7716
|
},
|
|
7712
7717
|
{
|
|
7713
7718
|
"path": "package.json",
|
|
7714
7719
|
"mode": "0000644",
|
|
7715
|
-
"sha256": "
|
|
7720
|
+
"sha256": "4dd1a1bfaad4b7ff8382bd13fdd0331f54536a84fdc928f7f9b97ad2b88e60ac"
|
|
7716
7721
|
},
|
|
7717
7722
|
{
|
|
7718
7723
|
"path": "public-docs/index.html",
|
|
@@ -9597,7 +9602,7 @@
|
|
|
9597
9602
|
{
|
|
9598
9603
|
"path": "src/module-api.js",
|
|
9599
9604
|
"mode": "0000644",
|
|
9600
|
-
"sha256": "
|
|
9605
|
+
"sha256": "4ad4e006b2f2916be9183776e9c33d49839b632b6a199180f23f99fb683f8e09"
|
|
9601
9606
|
},
|
|
9602
9607
|
{
|
|
9603
9608
|
"path": "src/module-loader/catalog.js",
|
|
@@ -13229,6 +13234,11 @@
|
|
|
13229
13234
|
"mode": "0000644",
|
|
13230
13235
|
"sha256": "1c5d35d58371cbde290a3e71e023febeebf350b54031d458c2ebe58f25962aa4"
|
|
13231
13236
|
},
|
|
13237
|
+
{
|
|
13238
|
+
"path": "tests/task_unblocked_open_blocker.mjs",
|
|
13239
|
+
"mode": "0000644",
|
|
13240
|
+
"sha256": "57a776dc56011bfe792a7a30ab29598bfc1b5b52f26ea0ef74faae5fbba26f6d"
|
|
13241
|
+
},
|
|
13232
13242
|
{
|
|
13233
13243
|
"path": "tests/task_visual_route_gate.mjs",
|
|
13234
13244
|
"mode": "0000644",
|
|
@@ -2070,5 +2070,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2070
2070
|
landed since 1.19.796 with no explicit bump. run 35169694391. (task 1002620)
|
|
2071
2071
|
1.19.798 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2072
2072
|
landed since 1.19.797 with no explicit bump. run 35170229509. (task 1002620)
|
|
2073
|
+
1.19.799 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2074
|
+
landed since 1.19.798 with no explicit bump. run 35170980369. (task 1002620)
|
|
2073
2075
|
---------------------------------------------------------------------------
|
|
2074
2076
|
```
|
|
@@ -18,6 +18,16 @@
|
|
|
18
18
|
-- release it, not a dep ship". But 009 releases on the first resolve, not the
|
|
19
19
|
-- last. One path fail-closes; the other did not. This aligns them.
|
|
20
20
|
--
|
|
21
|
+
-- CORRECTION (task 1003934, 2026-09-16): when this header was written, 176's
|
|
22
|
+
-- clause was ALREADY GONE from the live predicate, so the sentence above
|
|
23
|
+
-- described a symmetry that did not exist. 177 dropped the clause by whole-body
|
|
24
|
+
-- rewrite and 178 re-dropped it reverting to the pre-176 body; core_245 restores
|
|
25
|
+
-- it. Nothing in THIS migration depended on that premise — the trigger below
|
|
26
|
+
-- deliberately does not call task_is_fully_unblocked (the note further down says
|
|
27
|
+
-- why, and tests/blocker_resolve_guard.mjs pins it) — so this was a stale
|
|
28
|
+
-- comment, not a stale behaviour. Annotated rather than rewritten, because the
|
|
29
|
+
-- original reasoning is why the trigger exists.
|
|
30
|
+
--
|
|
21
31
|
-- Live at the time of writing: task 1000594 was blocked by BOTH blocker 1000065
|
|
22
32
|
-- and blocker 1000093 — duplicates covering the same two owner decisions — so
|
|
23
33
|
-- closing either as a duplicate would have released the task with the decision
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
-- core_245_restore_open_blocker_clause.sql — put migration 176's open-blocker
|
|
2
|
+
-- clause back into task_is_fully_unblocked (task 1003934).
|
|
3
|
+
--
|
|
4
|
+
-- WHAT WAS LOST, AND BY WHOM. The predicate is CREATE OR REPLACE'd five times
|
|
5
|
+
-- now. Each definition is a WHOLE BODY, not a patch, so a migration drops an
|
|
6
|
+
-- earlier clause simply by not restating it:
|
|
7
|
+
--
|
|
8
|
+
-- 163_polymorphic_dependencies.sql the original deps-doneness predicate
|
|
9
|
+
-- 176_promotion_respects_open_blockers.sql ADDS the open-blocker clause
|
|
10
|
+
-- 177_require_reward_before_workable.sql adds a reward clause — and does
|
|
11
|
+
-- NOT restate 176's. This is where
|
|
12
|
+
-- the clause died; grep -c
|
|
13
|
+
-- task_blockers on 177 returns 0.
|
|
14
|
+
-- 178_reward_gate_auto_assign_revert_autopromote.sql reverts to what its own
|
|
15
|
+
-- header calls "byte-identical to
|
|
16
|
+
-- the 163 body" — but 163 PREDATES
|
|
17
|
+
-- 176, so the revert re-dropped the
|
|
18
|
+
-- clause a second time and shipped
|
|
19
|
+
-- the loss forward.
|
|
20
|
+
-- this one restores it.
|
|
21
|
+
--
|
|
22
|
+
-- Neither 177 nor 178 said it was touching the blocker rule; both believed they
|
|
23
|
+
-- were editing the reward gate. core_223's header still asserts the clause is in
|
|
24
|
+
-- place. Verified on production 2026-09-15: pg_get_functiondef returns the
|
|
25
|
+
-- deps-doneness body only, with no task_blockers reference anywhere in it.
|
|
26
|
+
--
|
|
27
|
+
-- WHAT BREAKS WITHOUT IT. A task whose dependencies all ship auto-promotes
|
|
28
|
+
-- backlog -> ready even while an open blocker is linked to it. The by-id claim
|
|
29
|
+
-- path still refuses (modules/lifecycle/db-claims.js checks b.status = 'open',
|
|
30
|
+
-- task 1002650), so this is NOT a gate bypass — it is the QUEUE LYING. The task
|
|
31
|
+
-- advertises as ready in /builder-start and the claimable feed, and the server
|
|
32
|
+
-- refuses it when someone tries. "ready" stops meaning "workable", which is the
|
|
33
|
+
-- exact failure V4.R15 / task 953 filed migration 176 to fix.
|
|
34
|
+
--
|
|
35
|
+
-- WHY THE INNER JOIN, AND NOT THE LEFT JOIN core_239 USES. These are two
|
|
36
|
+
-- different questions and they are meant to answer the legacy-link case
|
|
37
|
+
-- differently. core_239 governs claimable_tasks — VISIBILITY — and uses
|
|
38
|
+
-- LEFT JOIN ... (b.id IS NULL OR b.status = 'open') so that a legacy
|
|
39
|
+
-- markdown-anchor link (task_blockers.blocker_ref with no blockers row) keeps a
|
|
40
|
+
-- task hidden, fail-closed. This predicate governs PROMOTION, where 176 decided
|
|
41
|
+
-- the opposite on purpose: its header records that legacy anchor links "are not
|
|
42
|
+
-- tracked here and behave exactly as before", so an unresolvable link must not
|
|
43
|
+
-- freeze a task in backlog forever with nothing able to release it. The
|
|
44
|
+
-- promotion sweep's PROMOTABLE_BACKLOG_WHERE (modules/lifecycle/db-tasks.js)
|
|
45
|
+
-- already made the same choice for the same reason. Restoring 176's INNER join
|
|
46
|
+
-- verbatim keeps the three consistent; "tidying" it to the LEFT form here would
|
|
47
|
+
-- silently change which tasks can ever be promoted.
|
|
48
|
+
--
|
|
49
|
+
-- SCOPE. This is 176's body exactly — 176 predates 177, so it never carried the
|
|
50
|
+
-- reward clause that 178 correctly removed. Nothing about the reward gate is
|
|
51
|
+
-- reinstated. Pure CREATE OR REPLACE, so it is idempotent; the migration-163
|
|
52
|
+
-- auto-promotion triggers resolve the function by name and pick up the new body
|
|
53
|
+
-- on their next fire. No data backfill and no demotion: rows already in 'ready'
|
|
54
|
+
-- are never re-evaluated, so this migration moves nothing by itself. The one
|
|
55
|
+
-- live row currently at 'ready' with an open blocker is reconciled separately,
|
|
56
|
+
-- deliberately, by a human looking at that row — not bulk-updated here.
|
|
57
|
+
|
|
58
|
+
BEGIN;
|
|
59
|
+
|
|
60
|
+
CREATE OR REPLACE FUNCTION task_is_fully_unblocked(p_task_id bigint)
|
|
61
|
+
RETURNS boolean AS $$
|
|
62
|
+
SELECT
|
|
63
|
+
-- (1) no gating dependency edge points at an un-done node. An orphaned `to`
|
|
64
|
+
-- yields no EXISTS match -> counts as un-done -> task stays blocked
|
|
65
|
+
-- (fail-closed). Unchanged from 163/178.
|
|
66
|
+
NOT EXISTS (
|
|
67
|
+
WITH gating AS (
|
|
68
|
+
SELECT d.to_kind, d.to_id
|
|
69
|
+
FROM dependencies d
|
|
70
|
+
JOIN tasks t ON t.id = p_task_id
|
|
71
|
+
WHERE (d.from_kind = 'task' AND d.from_id = t.id)
|
|
72
|
+
OR (d.from_kind = 'goal' AND d.from_id = t.goal_id)
|
|
73
|
+
OR (d.from_kind = 'criterion' AND d.from_id IN (
|
|
74
|
+
SELECT tc.criterion_id FROM task_criteria tc WHERE tc.task_id = t.id))
|
|
75
|
+
)
|
|
76
|
+
SELECT 1
|
|
77
|
+
FROM gating g
|
|
78
|
+
WHERE NOT (
|
|
79
|
+
(g.to_kind = 'task' AND EXISTS (SELECT 1 FROM tasks x WHERE x.id = g.to_id AND x.status = 'shipped'))
|
|
80
|
+
OR (g.to_kind = 'criterion' AND EXISTS (SELECT 1 FROM done_when_criteria x WHERE x.id = g.to_id AND x.satisfied))
|
|
81
|
+
OR (g.to_kind = 'goal' AND EXISTS (SELECT 1 FROM goals x WHERE x.id = g.to_id AND x.status = 'achieved'))
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
-- (2) … AND the task has no OPEN blocker linked via task_blockers (V4.R15 /
|
|
85
|
+
-- task 953). Restored from migration 176, verbatim. "Open" means
|
|
86
|
+
-- blockers.status = 'open', the same spelling core_223's resolve trigger and
|
|
87
|
+
-- db-claims.js use, so every gate agrees on the word.
|
|
88
|
+
AND NOT EXISTS (
|
|
89
|
+
SELECT 1
|
|
90
|
+
FROM task_blockers tb
|
|
91
|
+
JOIN blockers b ON b.id = tb.blocker_id
|
|
92
|
+
WHERE tb.task_id = p_task_id
|
|
93
|
+
AND b.status = 'open'
|
|
94
|
+
);
|
|
95
|
+
$$ LANGUAGE sql STABLE;
|
|
96
|
+
|
|
97
|
+
COMMIT;
|
|
@@ -126,8 +126,11 @@ async function claimTask({ taskId, builderId, worktreeName, headSha, creatorSess
|
|
|
126
126
|
|
|
127
127
|
// task 1002650 (idea 1000644): the claimable FEED filters out ready tasks
|
|
128
128
|
// with an open blocker link, but the direct claim-by-id path never checked —
|
|
129
|
-
// the feed hid the trap and this path walked into it.
|
|
129
|
+
// the feed hid the trap and this path walked into it. Mirrors the
|
|
130
130
|
// task_is_fully_unblocked clause so both gates enforce the same rule.
|
|
131
|
+
// Redundant since core_245 (task 1003934) and kept anyway: the predicate lost
|
|
132
|
+
// that clause from 177 until core_245, and this query was the only gate still
|
|
133
|
+
// refusing. It is why a reverted predicate cost a lying queue, not a bypass.
|
|
131
134
|
const { rows: openBlockers } = await client.query(
|
|
132
135
|
`SELECT b.id, b.title
|
|
133
136
|
FROM task_blockers tb
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.799",
|
|
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.799",
|
|
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.799",
|
|
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.799'; // 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,123 @@
|
|
|
1
|
+
// tests/task_unblocked_open_blocker.mjs
|
|
2
|
+
//
|
|
3
|
+
// Task 1003934 — task_is_fully_unblocked must exclude a task with an OPEN
|
|
4
|
+
// blocker. Static shape guard for core_245, in the style of
|
|
5
|
+
// claimable_blocker_status.mjs (task 1003863): no live DB, assert the SQL TEXT,
|
|
6
|
+
// so a future whole-body rewrite cannot silently drop the clause again.
|
|
7
|
+
//
|
|
8
|
+
// THE BUG THIS PINS, AND WHY NOTHING CAUGHT IT FOR TWO MIGRATIONS. The predicate
|
|
9
|
+
// is CREATE OR REPLACE'd as a WHOLE BODY every time, so an earlier clause dies by
|
|
10
|
+
// not being restated. 177 added a reward clause and did not restate 176's blocker
|
|
11
|
+
// clause; 178 then reverted to what it called the "byte-identical" 163 body,
|
|
12
|
+
// which PREDATES 176, re-dropping it. Neither migration mentioned blockers.
|
|
13
|
+
//
|
|
14
|
+
// The existing coverage could not see any of that, and this is the lesson worth
|
|
15
|
+
// keeping: tests/dependencies_polymorphic.mjs asserts the clause against
|
|
16
|
+
// migrations/176's FILE, which still sits on disk unchanged and still contains
|
|
17
|
+
// it. That test passed green for the entire life of the outage. A test pinned to
|
|
18
|
+
// a historical artifact proves history, not the current state — hence the
|
|
19
|
+
// LAST-definition assertion below, which is the only shape that would have
|
|
20
|
+
// failed on the day 177 landed.
|
|
21
|
+
//
|
|
22
|
+
// Run: node tests/task_unblocked_open_blocker.mjs
|
|
23
|
+
|
|
24
|
+
import { strict as assert } from 'node:assert';
|
|
25
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { dirname, join } from 'node:path';
|
|
28
|
+
import { makeRunner } from './helpers.mjs';
|
|
29
|
+
|
|
30
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const repoRoot = join(__dirname, '..');
|
|
32
|
+
const read = (...p) => readFileSync(join(repoRoot, ...p), 'utf8');
|
|
33
|
+
|
|
34
|
+
const MIG = 'core_245_restore_open_blocker_clause.sql';
|
|
35
|
+
const DEFINES = /CREATE OR REPLACE FUNCTION task_is_fully_unblocked/;
|
|
36
|
+
// Comments carry the rationale; strip them before asserting on the executable
|
|
37
|
+
// body so prose can never stand in for the real SQL.
|
|
38
|
+
const body = read('migrations', MIG).replace(/^\s*--.*$/gm, '').replace(/\s+/g, ' ');
|
|
39
|
+
|
|
40
|
+
const { test, summary } = makeRunner();
|
|
41
|
+
|
|
42
|
+
await test('core_245 redefines the predicate', () => {
|
|
43
|
+
assert.match(body, DEFINES);
|
|
44
|
+
assert.match(body, /\$\$ LANGUAGE sql STABLE;/, 'same kind of function as every prior definition');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await test('the blocker clause is back, and filters on the blocker being OPEN', () => {
|
|
48
|
+
assert.match(body, /FROM task_blockers tb/, 'it looks at the link table');
|
|
49
|
+
assert.match(body, /JOIN blockers b ON b\.id = tb\.blocker_id/, 'and resolves the blocker itself');
|
|
50
|
+
assert.match(body, /tb\.task_id = p_task_id/, 'for THIS task');
|
|
51
|
+
assert.match(body, /b\.status = 'open'/, 'so a RESOLVED blocker stops blocking promotion');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await test('the join is the INNER form 176 chose, NOT core_239-s LEFT form', () => {
|
|
55
|
+
// These two answer the legacy-link case differently ON PURPOSE, so "tidying"
|
|
56
|
+
// one into the other silently changes behaviour with no error anywhere.
|
|
57
|
+
// core_239 governs claimable_tasks (VISIBILITY) and fails closed on a legacy
|
|
58
|
+
// markdown-anchor link. This predicate governs PROMOTION, where 176's header
|
|
59
|
+
// records that such links "are not tracked here and behave exactly as before"
|
|
60
|
+
// — an unresolvable link must not freeze a task in backlog with nothing able
|
|
61
|
+
// to release it. PROMOTABLE_BACKLOG_WHERE made the same call for the same
|
|
62
|
+
// reason; all three must agree.
|
|
63
|
+
assert.doesNotMatch(body, /LEFT JOIN blockers b/,
|
|
64
|
+
'a LEFT join here would let a legacy anchor link freeze a task in backlog forever');
|
|
65
|
+
assert.doesNotMatch(body, /b\.id IS NULL/, 'the fail-closed arm belongs to the VIEW, not the predicate');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await test('the deps-doneness clause SURVIVED the rewrite — the hazard runs both ways', () => {
|
|
69
|
+
// This migration could drop the dependency gate exactly as 177 dropped the
|
|
70
|
+
// blocker gate, and nothing would error. Both clauses, asserted together.
|
|
71
|
+
assert.match(body, /WITH gating AS/, 'the gating CTE is intact');
|
|
72
|
+
assert.match(body, /d\.from_kind = 'task' AND d\.from_id = t\.id/, 'own edges');
|
|
73
|
+
assert.match(body, /d\.from_kind = 'goal' AND d\.from_id = t\.goal_id/, 'goal-inherited edges');
|
|
74
|
+
assert.match(body, /d\.from_kind = 'criterion'/, 'criterion-inherited edges');
|
|
75
|
+
assert.match(body, /x\.status = 'shipped'/, 'a task dep is done when shipped');
|
|
76
|
+
assert.match(body, /x\.satisfied/, 'a criterion dep when satisfied');
|
|
77
|
+
assert.match(body, /x\.status = 'achieved'/, 'a goal dep when achieved');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await test('both clauses are ANDed — one without the other is half a predicate', () => {
|
|
81
|
+
// 177 kept a NOT EXISTS and still shipped the bug, so the presence of each
|
|
82
|
+
// clause is not enough; they have to be joined into one condition.
|
|
83
|
+
assert.match(body, /NOT EXISTS \(.*WITH gating AS.*\) AND NOT EXISTS \(.*FROM task_blockers tb/,
|
|
84
|
+
'deps-doneness AND no-open-blocker, in one predicate');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await test('core_245 is the LAST definition of task_is_fully_unblocked in migrations/', () => {
|
|
88
|
+
// THE ASSERTION THAT WOULD HAVE CAUGHT 177 ON THE DAY IT LANDED. A later
|
|
89
|
+
// whole-body redefinition inherits only what it restates, which is exactly how
|
|
90
|
+
// this clause was lost twice. Catch the next one at the source.
|
|
91
|
+
const defs = readdirSync(join(repoRoot, 'migrations'))
|
|
92
|
+
.filter((f) => f.endsWith('.sql'))
|
|
93
|
+
.filter((f) => DEFINES.test(read('migrations', f)))
|
|
94
|
+
.sort();
|
|
95
|
+
assert.ok(defs.includes(MIG), 'this migration defines the predicate');
|
|
96
|
+
assert.equal(defs[defs.length - 1], MIG,
|
|
97
|
+
`a later migration redefines task_is_fully_unblocked and must restate the open-blocker clause: ${defs.join(', ')}`);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await test('every OTHER gate still spells "open" the same way', () => {
|
|
101
|
+
// The whole bug class is gates disagreeing. The predicate governs promotion,
|
|
102
|
+
// db-claims.js governs claim-by-id, and the sweep promotes in bulk; if they
|
|
103
|
+
// drift on what "open" means, one of them lies.
|
|
104
|
+
const code = (...p) => read(...p)
|
|
105
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
106
|
+
.replace(/^\s*\/\/.*$/gm, ' ')
|
|
107
|
+
.replace(/\s+/g, ' ');
|
|
108
|
+
|
|
109
|
+
assert.match(code('modules', 'lifecycle', 'db-claims.js'), /b\.status = 'open'/,
|
|
110
|
+
'the by-id claim path checks blocker STATUS');
|
|
111
|
+
|
|
112
|
+
// The sweep keeps its OWN guard deliberately (task 1003932). Now that the
|
|
113
|
+
// predicate is fixed that guard is belt-and-braces — and it stays, because a
|
|
114
|
+
// sweep that mass-promotes must not inherit a single point of failure from a
|
|
115
|
+
// predicate that has already been reverted twice.
|
|
116
|
+
assert.match(
|
|
117
|
+
code('modules', 'lifecycle', 'db-tasks.js'),
|
|
118
|
+
/PROMOTABLE_BACKLOG_WHERE = ` WHERE t\.status = 'backlog' AND task_is_fully_unblocked\(t\.id\) AND NOT EXISTS \( SELECT 1 FROM task_blockers tb JOIN blockers b ON b\.id = tb\.blocker_id WHERE tb\.task_id = t\.id AND b\.status = 'open'\)`/,
|
|
119
|
+
'the sweep still calls the predicate AND keeps its own open-blocker guard'
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
summary();
|