@bongos/core 1.20.11 → 1.20.12
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 +20 -15
- package/docs/module-api-changelog.md +2 -0
- package/migrations/core_264_claimable_respects_dependencies.sql +115 -0
- package/modules/lifecycle/db-tasks.js +42 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/bongos/optimizer.js +26 -1
- package/src/module-api.js +1 -1
- package/tests/claimable_blocker_status.mjs +160 -3
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.20.
|
|
6
|
-
"core_contract": "1.20.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.20.12",
|
|
6
|
+
"core_contract": "1.20.12",
|
|
7
|
+
"source_commit": "157de30a7a166f1941ba54aaa8f57964f7f73362",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-30T11:33:34.437Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 556,
|
|
13
13
|
"agent_docs_stubbed": 25,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2626,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 3208,
|
|
20
|
+
"tree_sha256": "0da694c2943c85befcb0d6d52c1d9ae4480c66b425a0393140773878b4483763",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2772,7 +2772,7 @@
|
|
|
2772
2772
|
{
|
|
2773
2773
|
"path": "docs/module-api-changelog.md",
|
|
2774
2774
|
"mode": "0000644",
|
|
2775
|
-
"sha256": "
|
|
2775
|
+
"sha256": "86429e308db13d625c1255ae509c712448f405ada592fc1ea1c2fe6fe8866d46"
|
|
2776
2776
|
},
|
|
2777
2777
|
{
|
|
2778
2778
|
"path": "docs/modules-contract.md",
|
|
@@ -3944,6 +3944,11 @@
|
|
|
3944
3944
|
"mode": "0000644",
|
|
3945
3945
|
"sha256": "6f502903c02f48a65370d2ca167b42a5ea514bed38bb9862d34465ea627191f2"
|
|
3946
3946
|
},
|
|
3947
|
+
{
|
|
3948
|
+
"path": "migrations/core_264_claimable_respects_dependencies.sql",
|
|
3949
|
+
"mode": "0000644",
|
|
3950
|
+
"sha256": "2f578b536ed624b204fc201c0a5e8033a2417c189c9caaa8f2148408f76ae481"
|
|
3951
|
+
},
|
|
3947
3952
|
{
|
|
3948
3953
|
"path": "modules/agents/CLAUDE.md",
|
|
3949
3954
|
"mode": "0000644",
|
|
@@ -6142,7 +6147,7 @@
|
|
|
6142
6147
|
{
|
|
6143
6148
|
"path": "modules/lifecycle/db-tasks.js",
|
|
6144
6149
|
"mode": "0000644",
|
|
6145
|
-
"sha256": "
|
|
6150
|
+
"sha256": "bf527e80db47d586361be82ef568842e011484313f09cc6405d1c351f9e15df7"
|
|
6146
6151
|
},
|
|
6147
6152
|
{
|
|
6148
6153
|
"path": "modules/lifecycle/db-versions.js",
|
|
@@ -8857,12 +8862,12 @@
|
|
|
8857
8862
|
{
|
|
8858
8863
|
"path": "package-lock.json",
|
|
8859
8864
|
"mode": "0000644",
|
|
8860
|
-
"sha256": "
|
|
8865
|
+
"sha256": "464fd91cbcd4c95177dde0d4e40720505686cfd677a575c0f7d7a8415f9a957b"
|
|
8861
8866
|
},
|
|
8862
8867
|
{
|
|
8863
8868
|
"path": "package.json",
|
|
8864
8869
|
"mode": "0000644",
|
|
8865
|
-
"sha256": "
|
|
8870
|
+
"sha256": "2f825d119f8d7b46e1f4ffab61515532848aa2634b53b6b66adbf1581bffc3b3"
|
|
8866
8871
|
},
|
|
8867
8872
|
{
|
|
8868
8873
|
"path": "public-docs/index.html",
|
|
@@ -8882,7 +8887,7 @@
|
|
|
8882
8887
|
{
|
|
8883
8888
|
"path": "release-notes.json",
|
|
8884
8889
|
"mode": "0000644",
|
|
8885
|
-
"sha256": "
|
|
8890
|
+
"sha256": "fdb871eaf32d4f1b1525a225cc5b28207359df70e1714f735eb060fac12743aa"
|
|
8886
8891
|
},
|
|
8887
8892
|
{
|
|
8888
8893
|
"path": "scripts/bongos-mcp.js",
|
|
@@ -10802,7 +10807,7 @@
|
|
|
10802
10807
|
{
|
|
10803
10808
|
"path": "src/bongos/optimizer.js",
|
|
10804
10809
|
"mode": "0000644",
|
|
10805
|
-
"sha256": "
|
|
10810
|
+
"sha256": "70b093c1a7d3b9b7c169694a3dba463221297c14efde12efccf23b0a0a94dc4e"
|
|
10806
10811
|
},
|
|
10807
10812
|
{
|
|
10808
10813
|
"path": "src/bongos/package-registry.js",
|
|
@@ -10997,7 +11002,7 @@
|
|
|
10997
11002
|
{
|
|
10998
11003
|
"path": "src/module-api.js",
|
|
10999
11004
|
"mode": "0000644",
|
|
11000
|
-
"sha256": "
|
|
11005
|
+
"sha256": "159b8f961396d15ce8bf1a5589ec100b0b9b1d0bc02ccb8b1c37a8ce82512dd7"
|
|
11001
11006
|
},
|
|
11002
11007
|
{
|
|
11003
11008
|
"path": "src/module-loader/catalog.js",
|
|
@@ -11697,7 +11702,7 @@
|
|
|
11697
11702
|
{
|
|
11698
11703
|
"path": "tests/claimable_blocker_status.mjs",
|
|
11699
11704
|
"mode": "0000644",
|
|
11700
|
-
"sha256": "
|
|
11705
|
+
"sha256": "9106c65ea3c157953660a7e1af006724be9e781e1f74e95b36ccd76e20ffe987"
|
|
11701
11706
|
},
|
|
11702
11707
|
{
|
|
11703
11708
|
"path": "tests/claims_bind_session.mjs",
|
|
@@ -2679,5 +2679,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2679
2679
|
landed since 1.20.9 with no explicit bump. run 36670625760. (task 1002620)
|
|
2680
2680
|
1.20.11 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2681
2681
|
landed since 1.20.10 with no explicit bump. run 36672606449. (task 1002620)
|
|
2682
|
+
1.20.12 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2683
|
+
landed since 1.20.11 with no explicit bump. run 36709211337. (task 1002620)
|
|
2682
2684
|
---------------------------------------------------------------------------
|
|
2683
2685
|
```
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
-- core_264 — claimable_tasks must also consult the DEPENDENCY graph (task
|
|
2
|
+
-- 1004190, finding #60 of the task 1000379 audit).
|
|
3
|
+
--
|
|
4
|
+
-- THE DEFECT. core_239's view filters on status, smoke titles, open blockers and
|
|
5
|
+
-- touches-overlap — and never once on `dependencies`. So a task sitting at
|
|
6
|
+
-- 'ready' whose predecessor has not shipped is ADVERTISED by /builder-start, the
|
|
7
|
+
-- optimizer and every claimable feed, and only `POST /claims` refuses it
|
|
8
|
+
-- (DEPS_NOT_SHIPPED, step 10 of the refusal ladder in modules/lifecycle/CLAUDE.md).
|
|
9
|
+
-- That is the "feed lies" family: the queue offers work the server will not let
|
|
10
|
+
-- you take. core_239 named this gap by number in its own header and deferred it
|
|
11
|
+
-- deliberately; this is the deferred half.
|
|
12
|
+
--
|
|
13
|
+
-- HOW A 'ready' ROW GETS UNSHIPPED DEPS AT ALL. The migration-163 auto-promotion
|
|
14
|
+
-- triggers gate backlog -> ready on task_is_fully_unblocked, so the common path
|
|
15
|
+
-- cannot produce one. The paths that CAN: a hand-promoted row
|
|
16
|
+
-- (POST /tasks/:id/promote), a dependency edge ADDED after the task reached
|
|
17
|
+
-- 'ready' (nothing demotes it), and a dependency target that leaves 'shipped'
|
|
18
|
+
-- later. db-claims.js calls its own dep check "the second line of defence" for
|
|
19
|
+
-- exactly these; the feed had no first line at all.
|
|
20
|
+
--
|
|
21
|
+
-- WHICH PREDICATE, AND WHY NOT task_is_fully_unblocked. There are two dependency
|
|
22
|
+
-- predicates in this codebase and they disagree on ONE case:
|
|
23
|
+
--
|
|
24
|
+
-- unsatisfiedDeps (modules/lifecycle/db-deps-criteria.js) — what POST /claims
|
|
25
|
+
-- actually runs. Each arm JOINs the target table, so an edge whose target row
|
|
26
|
+
-- no longer exists produces no row and does not gate.
|
|
27
|
+
-- task_is_fully_unblocked (core_245) — what the promotion triggers run. Its
|
|
28
|
+
-- header states the opposite reading: an orphaned `to` "counts as un-done ->
|
|
29
|
+
-- task stays blocked (fail-closed)".
|
|
30
|
+
--
|
|
31
|
+
-- Calling task_is_fully_unblocked here would have been the shorter line, and it
|
|
32
|
+
-- would have re-created core_239's bug inverted: a task with one orphaned edge
|
|
33
|
+
-- would vanish from the feed while POST /claims cheerfully accepted it —
|
|
34
|
+
-- workable by id, unfindable in the queue, permanently, with no blocker row to
|
|
35
|
+
-- explain it. The DONE-WHEN of this task is that the view and the claim path
|
|
36
|
+
-- AGREE, so the view mirrors unsatisfiedDeps' three UNION arms verbatim, not the
|
|
37
|
+
-- promotion predicate. Fail-closed is the right default when the two gates are
|
|
38
|
+
-- answering different questions; here they are answering the SAME question and
|
|
39
|
+
-- matching beats hedging. (The blocker clause below is untouched and stays
|
|
40
|
+
-- fail-closed — that one IS core_239's own question.)
|
|
41
|
+
--
|
|
42
|
+
-- ABANDONED STAYS GATING. `x.status <> 'shipped'` counts 'abandoned' as un-done,
|
|
43
|
+
-- so a task behind a cut prerequisite is now hidden from the feed rather than
|
|
44
|
+
-- offered and refused. That is the owner ruling of 2026-08-21 (modules/lifecycle/
|
|
45
|
+
-- CLAUDE.md) applied consistently, NOT finding #61: nothing here skips abandoned,
|
|
46
|
+
-- and the loud surfaces the ruling made load-bearing are untouched —
|
|
47
|
+
-- `scripts/gds/audit-deps.js --stranded`, `/backlog-review`'s depGated bucket and
|
|
48
|
+
-- the dead_deps refusal hint all read the dependency graph directly, never this
|
|
49
|
+
-- view.
|
|
50
|
+
--
|
|
51
|
+
-- THE THREE OTHER COPIES. The same view body is inlined as CLAIMABLE_SQL in
|
|
52
|
+
-- modules/lifecycle/db-tasks.js and inside src/bongos/optimizer.js's query (task
|
|
53
|
+
-- 2181: raw-table reads so the feed cannot park behind a view DDL lock). core_239
|
|
54
|
+
-- changed the view alone and the feed did not move for the whole life of task
|
|
55
|
+
-- 1003913. Both copies carry this clause as of this change, and
|
|
56
|
+
-- tests/claimable_blocker_status.mjs pins all three against each other.
|
|
57
|
+
--
|
|
58
|
+
-- COST, AND WHY THIS SHIPS NO INDEX. This is the /builder-start hot path, so the
|
|
59
|
+
-- question is fair and the answer is that every lookup the clause makes is already
|
|
60
|
+
-- covered — which is why there is no index migration beside this one:
|
|
61
|
+
--
|
|
62
|
+
-- the three from_kind arms -> idx_dependencies_from (from_kind, from_id), migration 163.
|
|
63
|
+
-- All three probe the SAME two columns, so the planner
|
|
64
|
+
-- can BitmapOr three index scans rather than seq-scan.
|
|
65
|
+
-- the criterion arm's IN -> idx_task_criteria_task (task_id), migration 055.
|
|
66
|
+
-- the three doneness EXISTS -> the primary keys of tasks / done_when_criteria / goals.
|
|
67
|
+
--
|
|
68
|
+
-- `d.to_kind` / `d.to_id` appear only as filters on rows already found through the
|
|
69
|
+
-- from-side index, never as a lookup key, so idx_dependencies_to is not on this path.
|
|
70
|
+
-- Asserted, not just claimed: tests/claimable_blocker_status.mjs fails if either
|
|
71
|
+
-- named index leaves migrations/, because a comment cannot notice that.
|
|
72
|
+
--
|
|
73
|
+
-- Idempotent: DROP VIEW IF EXISTS + CREATE, the shape core_231 and core_239 used.
|
|
74
|
+
-- The column list stays the `t.*` wildcard plus the calibrated-minutes column,
|
|
75
|
+
-- byte-identical to core_239 (core_227's header records six columns that silently
|
|
76
|
+
-- never reached the feed because a rewrite spelled the list out instead).
|
|
77
|
+
|
|
78
|
+
BEGIN;
|
|
79
|
+
|
|
80
|
+
DROP VIEW IF EXISTS claimable_tasks;
|
|
81
|
+
CREATE VIEW claimable_tasks AS
|
|
82
|
+
SELECT t.*,
|
|
83
|
+
pms_calibrated_minutes(t.est_minutes, t.kind) AS est_minutes_calibrated
|
|
84
|
+
FROM tasks t
|
|
85
|
+
WHERE t.status = 'ready'
|
|
86
|
+
AND t.title NOT LIKE '\_\_smoke\_\_%' ESCAPE '\'
|
|
87
|
+
AND NOT EXISTS (
|
|
88
|
+
SELECT 1
|
|
89
|
+
FROM task_blockers tb
|
|
90
|
+
LEFT JOIN blockers b ON b.id = tb.blocker_id
|
|
91
|
+
WHERE tb.task_id = t.id
|
|
92
|
+
AND (b.id IS NULL OR b.status = 'open')
|
|
93
|
+
)
|
|
94
|
+
AND NOT EXISTS (
|
|
95
|
+
SELECT 1
|
|
96
|
+
FROM dependencies d
|
|
97
|
+
WHERE ((d.from_kind = 'task' AND d.from_id = t.id)
|
|
98
|
+
OR (d.from_kind = 'goal' AND d.from_id = t.goal_id)
|
|
99
|
+
OR (d.from_kind = 'criterion' AND d.from_id IN (
|
|
100
|
+
SELECT tc.criterion_id FROM task_criteria tc WHERE tc.task_id = t.id)))
|
|
101
|
+
AND ((d.to_kind = 'task' AND EXISTS (SELECT 1 FROM tasks x WHERE x.id = d.to_id AND x.status <> 'shipped'))
|
|
102
|
+
OR (d.to_kind = 'criterion' AND EXISTS (SELECT 1 FROM done_when_criteria x WHERE x.id = d.to_id AND x.satisfied = false))
|
|
103
|
+
OR (d.to_kind = 'goal' AND EXISTS (SELECT 1 FROM goals x WHERE x.id = d.to_id AND x.status <> 'achieved')))
|
|
104
|
+
)
|
|
105
|
+
AND NOT EXISTS (
|
|
106
|
+
SELECT 1 FROM claims c
|
|
107
|
+
JOIN tasks t2 ON t2.id = c.task_id
|
|
108
|
+
WHERE c.released_at IS NULL
|
|
109
|
+
AND t2.touches && t.touches
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
COMMIT;
|
|
113
|
+
|
|
114
|
+
INSERT INTO schema_migrations (version) VALUES ('core_264_claimable_respects_dependencies')
|
|
115
|
+
ON CONFLICT (version) DO NOTHING;
|
|
@@ -883,11 +883,52 @@ const OPEN_BLOCKER_GUARD = `
|
|
|
883
883
|
AND (b.id IS NULL OR b.status = 'open')
|
|
884
884
|
)`;
|
|
885
885
|
|
|
886
|
+
// THE DEPENDENCY GUARD, spelled ONCE (task 1004190, finding #60).
|
|
887
|
+
//
|
|
888
|
+
// The sibling of OPEN_BLOCKER_GUARD, and it was missing entirely until now: the
|
|
889
|
+
// claimable feed filtered on status, smoke titles, open blockers and touches
|
|
890
|
+
// overlap, and never on `dependencies`. A task at 'ready' whose predecessor has
|
|
891
|
+
// not shipped was ADVERTISED here and then refused by POST /claims with
|
|
892
|
+
// DEPS_NOT_SHIPPED — the same "the feed lies" family core_239 fixed for blockers.
|
|
893
|
+
//
|
|
894
|
+
// THIS MIRRORS unsatisfiedDeps (db-deps-criteria.js), NOT task_is_fully_unblocked.
|
|
895
|
+
// The two predicates disagree on one case and the disagreement is deliberate on
|
|
896
|
+
// both sides: an edge whose target row no longer exists does NOT gate a claim
|
|
897
|
+
// (each of unsatisfiedDeps' arms JOINs the target, so an orphan produces no row)
|
|
898
|
+
// and DOES block promotion (core_245's header calls that fail-closed). Copying the
|
|
899
|
+
// promotion predicate into the feed would hide a task POST /claims would accept —
|
|
900
|
+
// core_239's bug inverted, and invisible to the builder looking for it. The feed
|
|
901
|
+
// must say exactly what the claim gate will do, so the three UNION arms of
|
|
902
|
+
// unsatisfiedDeps are reproduced here verbatim.
|
|
903
|
+
//
|
|
904
|
+
// `x.status <> 'shipped'` keeps 'abandoned' gating, per the owner ruling of
|
|
905
|
+
// 2026-08-21 (see ./CLAUDE.md). A task behind a cut prerequisite is now hidden
|
|
906
|
+
// rather than offered-and-refused; the surfaces that must stay LOUD about it read
|
|
907
|
+
// the dependency graph directly (audit-deps.js --stranded, /backlog-review, the
|
|
908
|
+
// dead_deps refusal hint) and never this feed.
|
|
909
|
+
//
|
|
910
|
+
// ONE query interpolates it, not two. SESSION_FIT_SQL is deliberately left alone:
|
|
911
|
+
// it is only ever JOINed onto this feed (here and in optimizer.js), so the guard
|
|
912
|
+
// already applies to every row it can see, and adding it there would change what
|
|
913
|
+
// the tasks_session_fit VIEW means with no migration saying so.
|
|
914
|
+
const UNSHIPPED_DEPS_GUARD = `
|
|
915
|
+
AND NOT EXISTS (
|
|
916
|
+
SELECT 1
|
|
917
|
+
FROM dependencies d
|
|
918
|
+
WHERE ((d.from_kind = 'task' AND d.from_id = t.id)
|
|
919
|
+
OR (d.from_kind = 'goal' AND d.from_id = t.goal_id)
|
|
920
|
+
OR (d.from_kind = 'criterion' AND d.from_id IN (
|
|
921
|
+
SELECT tc.criterion_id FROM task_criteria tc WHERE tc.task_id = t.id)))
|
|
922
|
+
AND ((d.to_kind = 'task' AND EXISTS (SELECT 1 FROM tasks x WHERE x.id = d.to_id AND x.status <> 'shipped'))
|
|
923
|
+
OR (d.to_kind = 'criterion' AND EXISTS (SELECT 1 FROM done_when_criteria x WHERE x.id = d.to_id AND x.satisfied = false))
|
|
924
|
+
OR (d.to_kind = 'goal' AND EXISTS (SELECT 1 FROM goals x WHERE x.id = d.to_id AND x.status <> 'achieved')))
|
|
925
|
+
)`;
|
|
926
|
+
|
|
886
927
|
const CLAIMABLE_SQL = `
|
|
887
928
|
SELECT t.*, pms_calibrated_minutes(t.est_minutes, t.kind) AS est_minutes_calibrated
|
|
888
929
|
FROM tasks t
|
|
889
930
|
WHERE t.status = 'ready'
|
|
890
|
-
AND t.title NOT LIKE '\\_\\_smoke\\_\\_%' ESCAPE '\\'${OPEN_BLOCKER_GUARD}
|
|
931
|
+
AND t.title NOT LIKE '\\_\\_smoke\\_\\_%' ESCAPE '\\'${OPEN_BLOCKER_GUARD}${UNSHIPPED_DEPS_GUARD}
|
|
891
932
|
AND NOT EXISTS (
|
|
892
933
|
SELECT 1 FROM claims c
|
|
893
934
|
JOIN tasks t2 ON t2.id = c.task_id
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.12",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.20.
|
|
9
|
+
"version": "1.20.12",
|
|
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.20.
|
|
3
|
+
"version": "1.20.12",
|
|
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/release-notes.json
CHANGED
|
@@ -8118,5 +8118,11 @@
|
|
|
8118
8118
|
"id": "1004404",
|
|
8119
8119
|
"text": "You can now choose which skills your speciality offers from Settings, not just through the API. You can do it when you create the speciality or later from its card. Each skill is listed with a plain description and what it cos"
|
|
8120
8120
|
}
|
|
8121
|
+
],
|
|
8122
|
+
"1.20.12": [
|
|
8123
|
+
{
|
|
8124
|
+
"id": "1004190",
|
|
8125
|
+
"text": "The task list no longer offers you work the system will refuse: a job waiting on another job that hasn't finished yet is now hidden until it's actually ready to start."
|
|
8126
|
+
}
|
|
8121
8127
|
]
|
|
8122
8128
|
}
|
package/src/bongos/optimizer.js
CHANGED
|
@@ -52,6 +52,31 @@ const OPEN_BLOCKER_GUARD = `
|
|
|
52
52
|
AND (b.id IS NULL OR b.status = 'open')
|
|
53
53
|
)`;
|
|
54
54
|
|
|
55
|
+
// The dependency guard, once (task 1004190, finding #60). Same predicate as
|
|
56
|
+
// migration core_264 and as db-tasks.js's UNSHIPPED_DEPS_GUARD, duplicated ACROSS
|
|
57
|
+
// those files for the ADR 0083 reason above and deduplicated WITHIN this one.
|
|
58
|
+
//
|
|
59
|
+
// It mirrors unsatisfiedDeps (the claim gate), not task_is_fully_unblocked (the
|
|
60
|
+
// promotion predicate): the two disagree on an edge whose target row is gone, and
|
|
61
|
+
// the feed must say what POST /claims will actually do. Full rationale in
|
|
62
|
+
// core_264's header.
|
|
63
|
+
//
|
|
64
|
+
// Only the claimable body below interpolates it — the session-fit body is JOINed
|
|
65
|
+
// onto that body, so every row it can see is already guarded, and putting it there
|
|
66
|
+
// would change what the tasks_session_fit VIEW means with no migration saying so.
|
|
67
|
+
const UNSHIPPED_DEPS_GUARD = `
|
|
68
|
+
AND NOT EXISTS (
|
|
69
|
+
SELECT 1
|
|
70
|
+
FROM dependencies d
|
|
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
|
+
AND ((d.to_kind = 'task' AND EXISTS (SELECT 1 FROM tasks x WHERE x.id = d.to_id AND x.status <> 'shipped'))
|
|
76
|
+
OR (d.to_kind = 'criterion' AND EXISTS (SELECT 1 FROM done_when_criteria x WHERE x.id = d.to_id AND x.satisfied = false))
|
|
77
|
+
OR (d.to_kind = 'goal' AND EXISTS (SELECT 1 FROM goals x WHERE x.id = d.to_id AND x.status <> 'achieved')))
|
|
78
|
+
)`;
|
|
79
|
+
|
|
55
80
|
const VALID_MODES = ['active', 'check-in', 'autonomous'];
|
|
56
81
|
const DEFAULT_MAX_PARALLEL = 3;
|
|
57
82
|
|
|
@@ -90,7 +115,7 @@ async function optimizeSession({ builderId, mode, timeBudgetMin, maxParallel })
|
|
|
90
115
|
FROM tasks t
|
|
91
116
|
WHERE t.status = 'ready'
|
|
92
117
|
AND t.title NOT LIKE '\\_\\_smoke\\_\\_%' ESCAPE '\\'
|
|
93
|
-
${OPEN_BLOCKER_GUARD}
|
|
118
|
+
${OPEN_BLOCKER_GUARD}${UNSHIPPED_DEPS_GUARD}
|
|
94
119
|
AND NOT EXISTS (
|
|
95
120
|
SELECT 1 FROM claims c
|
|
96
121
|
JOIN tasks t2 ON t2.id = c.task_id
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.20.
|
|
78
|
+
const CORE_VERSION = '1.20.12'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -13,6 +13,16 @@
|
|
|
13
13
|
// feed. Latent when found (every blocker was open), but eleven tasks were one
|
|
14
14
|
// resolve away from vanishing.
|
|
15
15
|
//
|
|
16
|
+
// TASK 1004190 WIDENED THIS FILE TO THE SECOND CLAUSE. core_239's header deferred
|
|
17
|
+
// the OTHER gap in the same view by name — it never consulted `dependencies`, so a
|
|
18
|
+
// 'ready' task whose predecessor had not shipped was advertised by /builder-start
|
|
19
|
+
// and then refused by POST /claims with DEPS_NOT_SHIPPED. core_264 closes it. The
|
|
20
|
+
// "LAST definition" test below is what forced the blocker clause to be carried
|
|
21
|
+
// forward into that rewrite rather than lost the way 177 lost migration 176's;
|
|
22
|
+
// it now pins core_264, and every clause assertion here reads the LIVE definition,
|
|
23
|
+
// because a correct older migration still sitting in the tree proves nothing about
|
|
24
|
+
// what Postgres runs.
|
|
25
|
+
//
|
|
16
26
|
// Run: node tests/claimable_blocker_status.mjs
|
|
17
27
|
|
|
18
28
|
import { strict as assert } from 'node:assert';
|
|
@@ -25,7 +35,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
25
35
|
const repoRoot = join(__dirname, '..');
|
|
26
36
|
const read = (...p) => readFileSync(join(repoRoot, ...p), 'utf8');
|
|
27
37
|
|
|
28
|
-
const MIG = '
|
|
38
|
+
const MIG = 'core_264_claimable_respects_dependencies.sql';
|
|
29
39
|
const mig = read('migrations', MIG);
|
|
30
40
|
// Comments carry the rationale; strip them when asserting on the executable body
|
|
31
41
|
// so a claim in prose can never stand in for the real SQL.
|
|
@@ -33,7 +43,7 @@ const body = mig.replace(/^\s*--.*$/gm, '').replace(/\s+/g, ' ');
|
|
|
33
43
|
|
|
34
44
|
const { test, summary } = makeRunner();
|
|
35
45
|
|
|
36
|
-
await test('
|
|
46
|
+
await test('the live migration replaces claimable_tasks, in core_231-s drop-and-create shape', () => {
|
|
37
47
|
assert.match(body, /DROP VIEW IF EXISTS claimable_tasks;/);
|
|
38
48
|
assert.match(body, /CREATE VIEW claimable_tasks AS/);
|
|
39
49
|
});
|
|
@@ -74,10 +84,15 @@ await test('the column list stays the t.* wildcard (core_227-s silent-column les
|
|
|
74
84
|
assert.match(body, /pms_calibrated_minutes\(t\.est_minutes, t\.kind\) AS est_minutes_calibrated/);
|
|
75
85
|
});
|
|
76
86
|
|
|
77
|
-
await test('
|
|
87
|
+
await test('the pinned migration is the LAST definition of claimable_tasks in migrations/', () => {
|
|
78
88
|
// If a later migration redefines the view, it inherits whatever that one says —
|
|
79
89
|
// which is exactly how 177 silently dropped migration 176's blocker clause from
|
|
80
90
|
// task_is_fully_unblocked. Catch the next one at the source.
|
|
91
|
+
//
|
|
92
|
+
// This pin is not theoretical: it is what made core_264 (task 1004190) carry
|
|
93
|
+
// core_239's blocker clause forward instead of replacing it. Moving MIG to the
|
|
94
|
+
// new file is HALF the job — every clause assertion above then runs against the
|
|
95
|
+
// new definition, and a rewrite that dropped one reds this file.
|
|
81
96
|
const defs = readdirSync(join(repoRoot, 'migrations'))
|
|
82
97
|
.filter((f) => f.endsWith('.sql'))
|
|
83
98
|
.filter((f) => /CREATE VIEW claimable_tasks/.test(read('migrations', f)))
|
|
@@ -201,4 +216,146 @@ await test('the promotion sweep keeps its own, deliberately different, guard', (
|
|
|
201
216
|
'the sweep resolves the blocker too — it just answers the legacy-link case differently');
|
|
202
217
|
});
|
|
203
218
|
|
|
219
|
+
/* ───────────────────────────────────────────────────────────────────────────
|
|
220
|
+
Task 1004190 / finding #60 — THE SECOND CLAUSE.
|
|
221
|
+
|
|
222
|
+
core_239 deferred the other half of the same defect by name: the view filtered
|
|
223
|
+
on status, smoke titles, blockers and touches-overlap and never once on
|
|
224
|
+
`dependencies`, so a 'ready' task whose predecessor had not shipped was
|
|
225
|
+
advertised by /builder-start and then refused by POST /claims. core_264 adds
|
|
226
|
+
the clause, and it has the same three copies the blocker guard does — so it
|
|
227
|
+
gets the same three-file treatment, because a migration alone did not move the
|
|
228
|
+
feed last time either.
|
|
229
|
+
─────────────────────────────────────────────────────────────────────────── */
|
|
230
|
+
|
|
231
|
+
// The dependency predicate, lifted out of the LIVE migration. Everything below
|
|
232
|
+
// compares the JS copies against what Postgres actually runs rather than against a
|
|
233
|
+
// second hand-written spelling of the same idea — the same reason GUARDED above is
|
|
234
|
+
// derived from the migration and not typed out here.
|
|
235
|
+
const DEPS_START = 'NOT EXISTS ( SELECT 1 FROM dependencies d';
|
|
236
|
+
const DEPS_END = "x.status <> 'achieved'))) )";
|
|
237
|
+
function depsClause(src, what) {
|
|
238
|
+
const i = src.indexOf(DEPS_START);
|
|
239
|
+
const j = i < 0 ? -1 : src.indexOf(DEPS_END, i);
|
|
240
|
+
assert.ok(i >= 0 && j > i, `${what} carries the dependency clause`);
|
|
241
|
+
return src.slice(i, j + DEPS_END.length);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
await test('the view resolves the same three gating sources as the claim gate', () => {
|
|
245
|
+
const clause = depsClause(body, MIG);
|
|
246
|
+
assert.match(clause, /d\.from_kind = 'task' AND d\.from_id = t\.id/, "the task's own edges");
|
|
247
|
+
assert.match(clause, /d\.from_kind = 'goal' AND d\.from_id = t\.goal_id/, 'plus what it inherits from its goal');
|
|
248
|
+
assert.match(clause, /d\.from_kind = 'criterion' AND d\.from_id IN \( SELECT tc\.criterion_id FROM task_criteria tc WHERE tc\.task_id = t\.id\)/,
|
|
249
|
+
'and from every criterion it fulfils');
|
|
250
|
+
assert.match(clause, /d\.to_kind = 'task' AND EXISTS \(SELECT 1 FROM tasks x WHERE x\.id = d\.to_id AND x\.status <> 'shipped'\)/);
|
|
251
|
+
assert.match(clause, /d\.to_kind = 'criterion' AND EXISTS \(SELECT 1 FROM done_when_criteria x WHERE x\.id = d\.to_id AND x\.satisfied = false\)/);
|
|
252
|
+
assert.match(clause, /d\.to_kind = 'goal' AND EXISTS \(SELECT 1 FROM goals x WHERE x\.id = d\.to_id AND x\.status <> 'achieved'\)/);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
await test('the view mirrors unsatisfiedDeps (the CLAIM path), not task_is_fully_unblocked (promotion)', () => {
|
|
256
|
+
// The two dependency predicates in this codebase disagree on exactly one case,
|
|
257
|
+
// and both sides of the disagreement are deliberate: an edge whose target row no
|
|
258
|
+
// longer exists gates PROMOTION (core_245's header calls that fail-closed) and
|
|
259
|
+
// does NOT gate a claim (every unsatisfiedDeps arm JOINs the target, so an orphan
|
|
260
|
+
// yields no row). Calling task_is_fully_unblocked from the view is the shorter
|
|
261
|
+
// line and it would hide a task POST /claims accepts — core_239's bug inverted,
|
|
262
|
+
// with no blocker row to explain it. The done-when of 1004190 is that the feed
|
|
263
|
+
// and the gate AGREE, so this asserts WHICH one the view copied.
|
|
264
|
+
assert.doesNotMatch(body, /task_is_fully_unblocked/,
|
|
265
|
+
'the feed must not borrow the promotion predicate — it answers the orphaned-edge case the other way');
|
|
266
|
+
const deps = code('modules', 'lifecycle', 'db-deps-criteria.js');
|
|
267
|
+
const clause = depsClause(body, MIG);
|
|
268
|
+
for (const frag of [
|
|
269
|
+
"d.from_kind = 'task' AND d.from_id = t.id",
|
|
270
|
+
"d.from_kind = 'goal' AND d.from_id = t.goal_id",
|
|
271
|
+
"d.from_kind = 'criterion' AND d.from_id IN ( SELECT tc.criterion_id FROM task_criteria tc WHERE tc.task_id = t.id)",
|
|
272
|
+
"x.status <> 'shipped'",
|
|
273
|
+
'x.satisfied = false',
|
|
274
|
+
"x.status <> 'achieved'",
|
|
275
|
+
]) {
|
|
276
|
+
assert.ok(deps.includes(frag),
|
|
277
|
+
`unsatisfiedDeps no longer spells it "${frag}" — the view copied that spelling and must be re-checked against it`);
|
|
278
|
+
assert.ok(clause.includes(frag), `the view must carry "${frag}" exactly as the claim gate does`);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
await test('each runtime copy declares the dependency guard ONCE, and the claimable query uses it', () => {
|
|
283
|
+
for (const f of RUNTIME) {
|
|
284
|
+
const src = code(...f);
|
|
285
|
+
const decls = (src.match(/const UNSHIPPED_DEPS_GUARD = /g) || []).length;
|
|
286
|
+
const uses = (src.match(/\$\{UNSHIPPED_DEPS_GUARD\}/g) || []).length;
|
|
287
|
+
assert.equal(decls, 1, `${f.join('/')} must declare the dependency guard exactly once`);
|
|
288
|
+
// ONE, where the blocker guard has two — and that asymmetry is the decision,
|
|
289
|
+
// not an omission. The session-fit body is only ever JOINed onto the claimable
|
|
290
|
+
// body (here and in listClaimableTasks), so every row it can reach is already
|
|
291
|
+
// guarded; putting the clause there would change what the tasks_session_fit
|
|
292
|
+
// VIEW means with no migration saying so.
|
|
293
|
+
assert.equal(uses, 1, `${f.join('/')}: the CLAIMABLE body interpolates it; the session-fit body deliberately does not`);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
await test('each runtime copy-s dependency guard is the live migration-s predicate, verbatim', () => {
|
|
298
|
+
const fromMigration = depsClause(body, MIG);
|
|
299
|
+
for (const f of RUNTIME) {
|
|
300
|
+
const decl = (code(...f).match(/const UNSHIPPED_DEPS_GUARD = `[^`]*`/) || [])[0];
|
|
301
|
+
assert.ok(decl, `${f.join('/')} declares the dependency guard as a template literal`);
|
|
302
|
+
assert.ok(decl.includes(fromMigration),
|
|
303
|
+
`${f.join('/')}'s dependency guard must be the migration's predicate verbatim — a paraphrase is how the next one drifts`);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
await test('the dependency guard actually reaches the composed claimable statement', () => {
|
|
308
|
+
const fromMigration = depsClause(body, MIG);
|
|
309
|
+
for (const f of RUNTIME) {
|
|
310
|
+
const raw = read(...f);
|
|
311
|
+
const gm = raw.match(/const UNSHIPPED_DEPS_GUARD = `([\s\S]*?)`;/);
|
|
312
|
+
assert.ok(gm, `${f.join('/')} declares the dependency guard`);
|
|
313
|
+
const expanded = raw.split('${UNSHIPPED_DEPS_GUARD}').join(gm[1]).replace(/\s+/g, ' ');
|
|
314
|
+
const hits = expanded.split(fromMigration).length - 1;
|
|
315
|
+
assert.equal(hits, 2, `${f.join('/')}: one declaration plus one expanded use`);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
await test('every lookup the dependency clause makes is index-covered', () => {
|
|
320
|
+
// This runs on the /builder-start hot path, so "a correlated NOT EXISTS with no
|
|
321
|
+
// index migration beside it" is the right thing to ask about the clause. The
|
|
322
|
+
// answer is that nothing new was needed — but that answer lived in two OTHER
|
|
323
|
+
// migrations, which is to say it was unverifiable from here and therefore not
|
|
324
|
+
// really an answer. Pin it, so deleting the cover reds this file instead of
|
|
325
|
+
// quietly turning the queue into a seq scan.
|
|
326
|
+
const allSql = readdirSync(join(repoRoot, 'migrations'))
|
|
327
|
+
.filter((f) => f.endsWith('.sql'))
|
|
328
|
+
.map((f) => read('migrations', f))
|
|
329
|
+
.join('\n');
|
|
330
|
+
assert.match(allSql, /CREATE INDEX IF NOT EXISTS idx_dependencies_from ON dependencies \(from_kind, from_id\)/,
|
|
331
|
+
'all three from_kind arms probe (from_kind, from_id) — migration 163');
|
|
332
|
+
assert.match(allSql, /CREATE INDEX IF NOT EXISTS idx_task_criteria_task\s+ON task_criteria \(task_id\)/,
|
|
333
|
+
"the criterion arm's IN probes task_criteria by task_id — migration 055");
|
|
334
|
+
assert.doesNotMatch(allSql, /DROP INDEX[^;]*idx_dependencies_from/,
|
|
335
|
+
'a later migration drops the index the claimable feed now depends on');
|
|
336
|
+
assert.doesNotMatch(allSql, /DROP INDEX[^;]*idx_task_criteria_task/,
|
|
337
|
+
'a later migration drops the index the criterion arm now depends on');
|
|
338
|
+
// The three doneness EXISTS lookups are by primary key (tasks.id,
|
|
339
|
+
// done_when_criteria.id, goals.id) and need nothing of their own.
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
await test('the dependency clause closes every paren it opens, in all three copies', () => {
|
|
343
|
+
// Nothing in this repo can reach a Postgres, so a stray paren in a clause this
|
|
344
|
+
// nested ships green and takes the ENTIRE claimable feed down with a syntax error
|
|
345
|
+
// on the first query after deploy — the feed, the optimizer and /builder-start at
|
|
346
|
+
// once. Counting is the cheapest real check available, and it is the failure mode
|
|
347
|
+
// hand-written inline SQL actually has.
|
|
348
|
+
const sources = [[body, MIG], ...RUNTIME.map((f) => [code(...f), f.join('/')])];
|
|
349
|
+
for (const [src, what] of sources) {
|
|
350
|
+
const clause = depsClause(src, what);
|
|
351
|
+
let depth = 0;
|
|
352
|
+
for (const ch of clause) {
|
|
353
|
+
if (ch === '(') depth++;
|
|
354
|
+
else if (ch === ')') depth--;
|
|
355
|
+
assert.ok(depth >= 0, `${what}: a closing paren with nothing open`);
|
|
356
|
+
}
|
|
357
|
+
assert.equal(depth, 0, `${what}: the clause closes every group it opens`);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
204
361
|
summary();
|