@bongos/core 1.19.595 → 1.19.596
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 +27 -17
- package/docs/module-api-changelog.md +2 -0
- package/migrations/core_234_goals_is_maintenance.sql +57 -0
- package/modules/lifecycle/db-goals.js +96 -17
- package/modules/lifecycle/db-versions.js +12 -12
- package/modules/lifecycle/db.js +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/government_seed.mjs +0 -10
- package/tests/maintenance_goal.mjs +8 -2
- package/tests/maintenance_goal_flag.mjs +223 -0
- package/tests/version_close_route.mjs +10 -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.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.596",
|
|
6
|
+
"core_contract": "1.19.596",
|
|
7
|
+
"source_commit": "25b1abaa5a60deab7277d0a2179e58dfc482b6b0",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-08T04:01:26.614Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 457,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2071,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2552,
|
|
20
|
+
"tree_sha256": "fd2460b94c293de3373d6e8b25a51f6cfc3c851ef13536d9ae525b90d0ae5d2c",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -2727,7 +2727,7 @@
|
|
|
2727
2727
|
{
|
|
2728
2728
|
"path": "docs/module-api-changelog.md",
|
|
2729
2729
|
"mode": "0000644",
|
|
2730
|
-
"sha256": "
|
|
2730
|
+
"sha256": "8cd803732ddfd0a2b36f63b63b6a280665a86bfed256198589f00c4a2c288609"
|
|
2731
2731
|
},
|
|
2732
2732
|
{
|
|
2733
2733
|
"path": "docs/modules-contract.md",
|
|
@@ -3734,6 +3734,11 @@
|
|
|
3734
3734
|
"mode": "0000644",
|
|
3735
3735
|
"sha256": "ca03e583bc418f600c1b1ba464bcaf4e380916033ec06cd68990cf153184d883"
|
|
3736
3736
|
},
|
|
3737
|
+
{
|
|
3738
|
+
"path": "migrations/core_234_goals_is_maintenance.sql",
|
|
3739
|
+
"mode": "0000644",
|
|
3740
|
+
"sha256": "92964dc3578fb2f4b1b7547867d0f53b2998c30bdb39271336e9b9aa348a1b37"
|
|
3741
|
+
},
|
|
3737
3742
|
{
|
|
3738
3743
|
"path": "modules/agents/lib/validate.js",
|
|
3739
3744
|
"mode": "0000644",
|
|
@@ -5522,7 +5527,7 @@
|
|
|
5522
5527
|
{
|
|
5523
5528
|
"path": "modules/lifecycle/db-goals.js",
|
|
5524
5529
|
"mode": "0000644",
|
|
5525
|
-
"sha256": "
|
|
5530
|
+
"sha256": "433f3ef7e9c81190b7d7fa7cc71e74e46c768d54240f6d89678b2437dd67496b"
|
|
5526
5531
|
},
|
|
5527
5532
|
{
|
|
5528
5533
|
"path": "modules/lifecycle/db-grade.js",
|
|
@@ -5557,12 +5562,12 @@
|
|
|
5557
5562
|
{
|
|
5558
5563
|
"path": "modules/lifecycle/db-versions.js",
|
|
5559
5564
|
"mode": "0000644",
|
|
5560
|
-
"sha256": "
|
|
5565
|
+
"sha256": "9d909149be4152335f6d259a32621c2e3917e98c2595ac1c8ff0d2202891dbcb"
|
|
5561
5566
|
},
|
|
5562
5567
|
{
|
|
5563
5568
|
"path": "modules/lifecycle/db.js",
|
|
5564
5569
|
"mode": "0000644",
|
|
5565
|
-
"sha256": "
|
|
5570
|
+
"sha256": "33fc8959d522bcf18c445b1a5710940379f84d1e938eecdc52cdf52bec9c4b3d"
|
|
5566
5571
|
},
|
|
5567
5572
|
{
|
|
5568
5573
|
"path": "modules/lifecycle/dead-deps.js",
|
|
@@ -7612,12 +7617,12 @@
|
|
|
7612
7617
|
{
|
|
7613
7618
|
"path": "package-lock.json",
|
|
7614
7619
|
"mode": "0000644",
|
|
7615
|
-
"sha256": "
|
|
7620
|
+
"sha256": "e7a26121f535ff4cdd5eec05579d16849da238adb5932a4f94815e67d5de4745"
|
|
7616
7621
|
},
|
|
7617
7622
|
{
|
|
7618
7623
|
"path": "package.json",
|
|
7619
7624
|
"mode": "0000644",
|
|
7620
|
-
"sha256": "
|
|
7625
|
+
"sha256": "4f2e2854f84a88237284dbbe30261670e3fe6683fdb46dd7c24b09f18ebf9f82"
|
|
7621
7626
|
},
|
|
7622
7627
|
{
|
|
7623
7628
|
"path": "public-docs/index.html",
|
|
@@ -9332,7 +9337,7 @@
|
|
|
9332
9337
|
{
|
|
9333
9338
|
"path": "src/module-api.js",
|
|
9334
9339
|
"mode": "0000644",
|
|
9335
|
-
"sha256": "
|
|
9340
|
+
"sha256": "ad5397fd512a3a25f3ad6a50a96f628ba1f43b6330ce2799f34c9cb099d70cc2"
|
|
9336
9341
|
},
|
|
9337
9342
|
{
|
|
9338
9343
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10722,7 +10727,7 @@
|
|
|
10722
10727
|
{
|
|
10723
10728
|
"path": "tests/government_seed.mjs",
|
|
10724
10729
|
"mode": "0000644",
|
|
10725
|
-
"sha256": "
|
|
10730
|
+
"sha256": "6fa11446c88018ab6e62a72756f96f176bfaf6cd6099dc231746645ba0f538d2"
|
|
10726
10731
|
},
|
|
10727
10732
|
{
|
|
10728
10733
|
"path": "tests/government_session_scope.mjs",
|
|
@@ -11282,7 +11287,12 @@
|
|
|
11282
11287
|
{
|
|
11283
11288
|
"path": "tests/maintenance_goal.mjs",
|
|
11284
11289
|
"mode": "0000644",
|
|
11285
|
-
"sha256": "
|
|
11290
|
+
"sha256": "aa0d19a1d9c2f5ae9e89a77ef8e5c485f0f759e6a36dab988c8c49f0f2b59d27"
|
|
11291
|
+
},
|
|
11292
|
+
{
|
|
11293
|
+
"path": "tests/maintenance_goal_flag.mjs",
|
|
11294
|
+
"mode": "0000644",
|
|
11295
|
+
"sha256": "37356bcd78ff92537a5512b60f2274d573b6c90bb30f5deca006a14d4841fc95"
|
|
11286
11296
|
},
|
|
11287
11297
|
{
|
|
11288
11298
|
"path": "tests/manage_manifest_shared_read.mjs",
|
|
@@ -12682,7 +12692,7 @@
|
|
|
12682
12692
|
{
|
|
12683
12693
|
"path": "tests/version_close_route.mjs",
|
|
12684
12694
|
"mode": "0000644",
|
|
12685
|
-
"sha256": "
|
|
12695
|
+
"sha256": "9a5b8f753c4b898b447b257d64c0034a83fe257c23208ce714e2e469fcaf735d"
|
|
12686
12696
|
},
|
|
12687
12697
|
{
|
|
12688
12698
|
"path": "tests/version_literals.mjs",
|
|
@@ -1639,5 +1639,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1639
1639
|
landed since 1.19.593 with no explicit bump. run 34184562228. (task 1002620)
|
|
1640
1640
|
1.19.595 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1641
1641
|
landed since 1.19.594 with no explicit bump. run 34185042811. (task 1002620)
|
|
1642
|
+
1.19.596 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1643
|
+
landed since 1.19.595 with no explicit bump. run 34185477798. (task 1002620)
|
|
1642
1644
|
---------------------------------------------------------------------------
|
|
1643
1645
|
```
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
-- core_234_goals_is_maintenance.sql — say which goal is the version's maintenance
|
|
2
|
+
-- goal in a COLUMN, not in its title (BV1.R18, task 1003605, goal 1000086,
|
|
3
|
+
-- ADR 0263 §7).
|
|
4
|
+
--
|
|
5
|
+
-- WHY THIS IS LOAD-BEARING AND NOT COSMETIC. Until now the maintenance goal was
|
|
6
|
+
-- matched by title — `ensureMaintenanceGoal` (task 1003691) looks up
|
|
7
|
+
-- `"<version> — maintenance"` and its own comment flagged this task as the debt.
|
|
8
|
+
-- For ROUTING a bug that is a provenance wrinkle: the worst case is a
|
|
9
|
+
-- hand-titled goal receiving a defect. For the CLOSE COUNT it is far worse.
|
|
10
|
+
-- `maintenanceGoalExemptSql` decides which goals hold a VERSION open, so with a
|
|
11
|
+
-- title match any Metic+ able to create a goal could name it
|
|
12
|
+
-- `"BONGOS-V1 — maintenance"` and silently exempt it from the gate that decides
|
|
13
|
+
-- when a version may close — widening scope by choosing a string. A boolean
|
|
14
|
+
-- column cannot be spelled into existence.
|
|
15
|
+
--
|
|
16
|
+
-- THE BACKFILL IS BY TITLE, ONCE, and that is the only place the title match is
|
|
17
|
+
-- allowed to survive: the rows that already exist were created by the title
|
|
18
|
+
-- convention, so the convention is how they are identified — exactly once, here,
|
|
19
|
+
-- with the schema then carrying the fact forward. `ensureMaintenanceGoal` stops
|
|
20
|
+
-- reading titles in the same task.
|
|
21
|
+
--
|
|
22
|
+
-- NOT UNIQUE PER VERSION, deliberately. A partial unique index on
|
|
23
|
+
-- (version_id) WHERE is_maintenance would be the tidy thing and would FAIL on any
|
|
24
|
+
-- database that already has two title-matching goals for one version — which is
|
|
25
|
+
-- possible, because `goals` has no unique constraint on (version_id, title) and
|
|
26
|
+
-- the pre-lock window in migration 160's era could produce one. The find-or-create
|
|
27
|
+
-- is already serialised by pg_advisory_xact_lock, and R18 makes it read the flag
|
|
28
|
+
-- with LIMIT 1, so a duplicate is survivable rather than fatal. Adding the
|
|
29
|
+
-- constraint is a follow-up that must first prove the data allows it — the same
|
|
30
|
+
-- lesson core_233 recorded about the building-version index.
|
|
31
|
+
--
|
|
32
|
+
-- CORE, not modules/lifecycle/migrations/: `goals` is a CORE table (migration
|
|
33
|
+
-- 160), and a module migration may only touch its own <key>_-prefixed objects —
|
|
34
|
+
-- fitness.js checkModuleMigrationsAdditive hard-fails an `ALTER TABLE goals` from
|
|
35
|
+
-- a module (ADR 0083 Decision #5).
|
|
36
|
+
--
|
|
37
|
+
-- Forward-safe: one additive column with a NOT NULL default, so old code on the
|
|
38
|
+
-- new schema neither sees nor needs it (every reader SELECTs an explicit column
|
|
39
|
+
-- list). A rollback leaves the column populated and unread.
|
|
40
|
+
|
|
41
|
+
ALTER TABLE goals
|
|
42
|
+
ADD COLUMN IF NOT EXISTS is_maintenance boolean NOT NULL DEFAULT false;
|
|
43
|
+
|
|
44
|
+
-- Backfill: every goal whose title is the maintenance convention for its own
|
|
45
|
+
-- version. Anchored on the version id rather than a bare LIKE '%maintenance%',
|
|
46
|
+
-- so a goal legitimately titled "Maintenance of the art pipeline" is untouched.
|
|
47
|
+
UPDATE goals
|
|
48
|
+
SET is_maintenance = true
|
|
49
|
+
WHERE is_maintenance = false
|
|
50
|
+
AND title = version_id || ' — maintenance';
|
|
51
|
+
|
|
52
|
+
-- Finding the version's maintenance goal is now a flag read on a hot-ish path
|
|
53
|
+
-- (every homeless bug, every close count), so give it an index. Partial: the
|
|
54
|
+
-- overwhelming majority of goals are not maintenance goals.
|
|
55
|
+
CREATE INDEX IF NOT EXISTS goals_maintenance_by_version_idx
|
|
56
|
+
ON goals (version_id)
|
|
57
|
+
WHERE is_maintenance;
|
|
@@ -34,8 +34,13 @@ const { SMOKE_NOT_LIKE } = require('./db-shared.js');
|
|
|
34
34
|
|
|
35
35
|
// Shared column list so every goal read/return shapes a row identically.
|
|
36
36
|
|
|
37
|
+
// `is_maintenance` (BV1.R18, task 1003605, migration core_234) rides the shared
|
|
38
|
+
// list because it is a fact ABOUT the goal that several surfaces need to read —
|
|
39
|
+
// the close count's exemption, and the hall, which should not render the one goal
|
|
40
|
+
// nobody chose as though a person had scoped it. Additive: every consumer selects
|
|
41
|
+
// this list, so nothing had to be found and changed.
|
|
37
42
|
const GOAL_COLS =
|
|
38
|
-
'id, version_id, title, subtitle, description, scope_modules, status, visibility, accepting_requests, created_by, succeeded_by_goal_id, sort_order, category_id, created_at, updated_at';
|
|
43
|
+
'id, version_id, title, subtitle, description, scope_modules, status, visibility, accepting_requests, created_by, succeeded_by_goal_id, sort_order, category_id, is_maintenance, created_at, updated_at';
|
|
39
44
|
|
|
40
45
|
// Shared column list for a goal_membership_requests row (task 1951 / ADR 0112) —
|
|
41
46
|
// the invite / join-request queue behind private goals. Every helper below shapes
|
|
@@ -128,19 +133,26 @@ async function ensureMaintenanceGoal(versionId, deps = {}) {
|
|
|
128
133
|
|
|
129
134
|
await exec.query(`SELECT pg_advisory_xact_lock(hashtext($1))`, [title]);
|
|
130
135
|
|
|
131
|
-
// MATCHED BY
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
136
|
+
// MATCHED BY FLAG since BV1.R18 (task 1003605, migration core_234). This used
|
|
137
|
+
// to match by TITLE, and its own comment called that "a provenance wrinkle
|
|
138
|
+
// rather than a way in" — true for routing, where the worst case was a
|
|
139
|
+
// hand-titled goal receiving a bug. It stopped being true once the same
|
|
140
|
+
// identification decided which goals hold a VERSION open
|
|
141
|
+
// (`maintenanceGoalExemptSql`): a Metic+ could then exempt a goal from the
|
|
142
|
+
// close gate by naming it. `goals.is_maintenance` cannot be spelled.
|
|
138
143
|
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
144
|
+
// LIMIT 1 with no unique constraint behind it is deliberate — see core_234 on
|
|
145
|
+
// why the partial unique index is a follow-up rather than part of this
|
|
146
|
+
// migration. The advisory lock above is what keeps this find-or-create from
|
|
147
|
+
// racing itself; the LIMIT is what keeps a pre-existing duplicate survivable
|
|
148
|
+
// instead of fatal. Ordered by id so the answer is at least stable.
|
|
149
|
+
//
|
|
150
|
+
// `goals_maintenance_by_version_idx` (core_234) backs this read.
|
|
141
151
|
const { rows: found } = await exec.query(
|
|
142
|
-
`SELECT ${GOAL_COLS} FROM goals
|
|
143
|
-
|
|
152
|
+
`SELECT ${GOAL_COLS} FROM goals
|
|
153
|
+
WHERE version_id = $1 AND is_maintenance
|
|
154
|
+
ORDER BY id LIMIT 1`,
|
|
155
|
+
[versionId]
|
|
144
156
|
);
|
|
145
157
|
if (found[0]) return found[0];
|
|
146
158
|
|
|
@@ -158,21 +170,87 @@ async function ensureMaintenanceGoal(versionId, deps = {}) {
|
|
|
158
170
|
// split (sub-Metic lands 'backlog', never 'ready') and createTask's own derived
|
|
159
171
|
// floor. Do not "fix" this by inventing a scope list — narrow the GRANT instead,
|
|
160
172
|
// and write down why.
|
|
173
|
+
// is_maintenance = true is what makes this goal exempt from the version-close
|
|
174
|
+
// count (ADR 0263 §7) — set HERE, in the same statement that creates it, so a
|
|
175
|
+
// maintenance goal cannot exist unflagged for even one transaction. The title
|
|
176
|
+
// is still written to the convention, because it is what a person reads on the
|
|
177
|
+
// board; nothing decides anything from it any more.
|
|
161
178
|
const { rows: made } = await exec.query(
|
|
162
|
-
`INSERT INTO goals (version_id, title, description, scope_modules, status, created_by, sort_order)
|
|
163
|
-
SELECT $1, $2, $3, $4, 'open', NULL, COALESCE(MAX(sort_order) + 1, 0)
|
|
179
|
+
`INSERT INTO goals (version_id, title, description, scope_modules, status, created_by, sort_order, is_maintenance)
|
|
180
|
+
SELECT $1, $2, $3, $4, 'open', NULL, COALESCE(MAX(sort_order) + 1, 0), true
|
|
164
181
|
FROM goals WHERE version_id = $1
|
|
165
182
|
RETURNING ${GOAL_COLS}`,
|
|
166
183
|
[versionId, title,
|
|
167
184
|
`Where a reported defect lands when its reporter named no goal (ADR 0262). ` +
|
|
168
|
-
`Auto-created by the bug-routing path
|
|
169
|
-
`the
|
|
170
|
-
`forward to the successor version.`,
|
|
185
|
+
`Auto-created by the bug-routing path, flagged \`is_maintenance\` so it is exempt ` +
|
|
186
|
+
`from the version-close count and never holds a version open (ADR 0263 §7). Its ` +
|
|
187
|
+
`unfinished bugs carry forward to the successor version's maintenance goal.`,
|
|
171
188
|
MAINTENANCE_SCOPE_MODULES]
|
|
172
189
|
);
|
|
173
190
|
return made[0] || null;
|
|
174
191
|
}
|
|
175
192
|
|
|
193
|
+
// carryBugsForward — move a closing version's unfinished defects into the
|
|
194
|
+
// successor's maintenance goal (BV1.R18, task 1003605, goal 1000086, ADR 0263 §7).
|
|
195
|
+
//
|
|
196
|
+
// THE ONE GOAL THAT LEGITIMATELY SPANS VERSIONS. Every other goal is scoped to a
|
|
197
|
+
// version and dies with it; the maintenance goal is where a defect against
|
|
198
|
+
// already-shipped work lands, and defects do not respect a version boundary. ADR
|
|
199
|
+
// 0250 §3 makes that a pressure valve — a bug must never need an Archon override
|
|
200
|
+
// to be filed — and ADR 0263 §7 points out the valve only works if the queue
|
|
201
|
+
// SURVIVES the boundary. A version close that left the open bugs in a shipped
|
|
202
|
+
// version's goal would entomb them exactly where nobody looks, which is the 1000771
|
|
203
|
+
// bug this whole path was built to end.
|
|
204
|
+
//
|
|
205
|
+
// RUNS ON THE CALLER'S CLIENT, inside the close transaction, and AFTER the
|
|
206
|
+
// successor has been promoted to `building` — `ensureMaintenanceGoal` returns null
|
|
207
|
+
// for a version that is not building, so a carry-over run before the promotion
|
|
208
|
+
// silently carries nothing. That ordering is R20's (task 1003607) to keep; this
|
|
209
|
+
// function fails loudly rather than quietly if it is broken.
|
|
210
|
+
//
|
|
211
|
+
// IT DOES NOT REFUSE AN ACTIVELY-CLAIMED TASK, and that is a deliberate divergence
|
|
212
|
+
// from `archiveGoalWithDispositions` (R14), which does. The two are different acts:
|
|
213
|
+
// archiving ABANDONS a task out from under whoever is working it, so refusing
|
|
214
|
+
// protects them. Carrying forward only re-points which goal the task belongs to —
|
|
215
|
+
// the claim references the task and is untouched, the builder loses nothing — and
|
|
216
|
+
// refusing here would let one in-flight bug fix block a version from ever closing.
|
|
217
|
+
// A close must not be hostage to a claim on a defect.
|
|
218
|
+
//
|
|
219
|
+
// Returns { fromGoalId, toGoalId, moved } — `moved` is 0 whenever there is nothing
|
|
220
|
+
// to carry, which is the common case and is not an error.
|
|
221
|
+
async function carryBugsForward({ fromVersionId, toVersionId }, deps = {}) {
|
|
222
|
+
const exec = deps.client || ((deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool);
|
|
223
|
+
if (fromVersionId == null || toVersionId == null) return { fromGoalId: null, toGoalId: null, moved: 0 };
|
|
224
|
+
|
|
225
|
+
// The source goal is read by FLAG, not created: a version that never received a
|
|
226
|
+
// homeless bug has no maintenance goal, and inventing one on the way out would
|
|
227
|
+
// leave an empty goal on a shipped version forever.
|
|
228
|
+
const { rows: from } = await exec.query(
|
|
229
|
+
`SELECT id FROM goals WHERE version_id = $1 AND is_maintenance ORDER BY id LIMIT 1`,
|
|
230
|
+
[fromVersionId]
|
|
231
|
+
);
|
|
232
|
+
if (!from[0]) return { fromGoalId: null, toGoalId: null, moved: 0 };
|
|
233
|
+
|
|
234
|
+
const to = await ensureMaintenanceGoal(toVersionId, deps);
|
|
235
|
+
if (!to) {
|
|
236
|
+
throw Object.assign(new Error('successor version cannot hold a maintenance goal'), {
|
|
237
|
+
code: 'CARRY_FORWARD_NO_DESTINATION', toVersionId,
|
|
238
|
+
hint: 'ensureMaintenanceGoal returns null unless the destination version is `building` — promote it before carrying forward (ADR 0263 §8).',
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Only the UNFINISHED ones move. A shipped or abandoned defect is history and
|
|
243
|
+
// belongs to the version it was fixed in — dragging it forward would make every
|
|
244
|
+
// successor's maintenance goal a growing archive of everything ever reported.
|
|
245
|
+
const { rows: moved } = await exec.query(
|
|
246
|
+
`UPDATE tasks SET goal_id = $2, updated_at = now()
|
|
247
|
+
WHERE goal_id = $1 AND ${nonTerminalSql('status')}
|
|
248
|
+
RETURNING id::text`,
|
|
249
|
+
[from[0].id, to.id]
|
|
250
|
+
);
|
|
251
|
+
return { fromGoalId: String(from[0].id), toGoalId: String(to.id), moved: moved.length };
|
|
252
|
+
}
|
|
253
|
+
|
|
176
254
|
// listVersionAdmissions (BV1.R07, task 1003594) — every goal admitted into ONE
|
|
177
255
|
// version, newest first. ADR 0250 §3: "the overrides are counted and shown on the
|
|
178
256
|
// version" — a visible counter is what keeps the exception honest, because an
|
|
@@ -1092,6 +1170,7 @@ async function listPendingRequestsForGoal(goalId, deps = {}) {
|
|
|
1092
1170
|
module.exports = {
|
|
1093
1171
|
acceptMembershipRequestAndAddMember,
|
|
1094
1172
|
ensureMaintenanceGoal,
|
|
1173
|
+
carryBugsForward,
|
|
1095
1174
|
achieveGoalIfComplete,
|
|
1096
1175
|
archiveGoalWithDispositions,
|
|
1097
1176
|
addGoalMember,
|
|
@@ -190,19 +190,19 @@ async function currentBuildingVersionId(deps = {}) {
|
|
|
190
190
|
// maintenanceGoalExemptSql — "this goal is NOT the version's maintenance goal",
|
|
191
191
|
// as one SQL predicate with ONE definition (BV1.R12, task 1003599; ADR 0263 §7).
|
|
192
192
|
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
193
|
+
// R18 (task 1003605) REPLACED THE BODY, exactly as R12 said it would, and the
|
|
194
|
+
// seam is why that was a one-line change with no call sites to hunt. It used to
|
|
195
|
+
// read `g.title <> ($1 || ' — maintenance')`, which decided by STRING which goals
|
|
196
|
+
// hold a version open — so any Metic+ able to create a goal could name it
|
|
197
|
+
// `"BONGOS-V1 — maintenance"` and silently exempt it from the gate that decides
|
|
198
|
+
// when a version may close. Widening scope by choosing a title is not a wrinkle,
|
|
199
|
+
// it is a way in. `goals.is_maintenance` (migration core_234) cannot be spelled
|
|
200
|
+
// into existence.
|
|
200
201
|
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
return `g.title <> (${titleParam} || ' — maintenance')`;
|
|
202
|
+
// The parameter is now unused and kept ONLY so the call sites do not change
|
|
203
|
+
// shape; drop it the next time this file is touched for another reason.
|
|
204
|
+
function maintenanceGoalExemptSql(_titleParam) {
|
|
205
|
+
return 'NOT g.is_maintenance';
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
// openNonMaintenanceGoals — the goals that hold a version open, id + title +
|
package/modules/lifecycle/db.js
CHANGED
|
@@ -71,6 +71,7 @@ const {
|
|
|
71
71
|
addGoalScopeModules,
|
|
72
72
|
createGoal,
|
|
73
73
|
ensureMaintenanceGoal,
|
|
74
|
+
carryBugsForward,
|
|
74
75
|
createMembershipRequest,
|
|
75
76
|
getGoal,
|
|
76
77
|
getGoalAdmission,
|
|
@@ -225,6 +226,7 @@ module.exports = {
|
|
|
225
226
|
currentBuildingVersionId,
|
|
226
227
|
createGoal,
|
|
227
228
|
ensureMaintenanceGoal,
|
|
229
|
+
carryBugsForward,
|
|
228
230
|
getGoal,
|
|
229
231
|
getGoalAdmission,
|
|
230
232
|
listVersionAdmissions,
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.596",
|
|
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.596",
|
|
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.596",
|
|
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
|
@@ -55,7 +55,7 @@ const { buildInfo } = require('./build-info');
|
|
|
55
55
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
56
56
|
// the entry to that file. Look for a version's history there, not here.
|
|
57
57
|
// ---------------------------------------------------------------------------
|
|
58
|
-
const CORE_VERSION = '1.19.
|
|
58
|
+
const CORE_VERSION = '1.19.596'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
59
59
|
|
|
60
60
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
61
61
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -90,16 +90,6 @@ function grantBlocksForRole(role) {
|
|
|
90
90
|
const REVOKE_RE =
|
|
91
91
|
/DELETE\s+FROM\s+govern(?:ance_role|ment_rank)_permissions\s+WHERE\s+(?:rank_key|role_key)\s*=\s*'([^']+)'\s+AND\s+permission_key\s*=\s*'([^']+)'/gi;
|
|
92
92
|
|
|
93
|
-
function revokeBlocksForRole(role) {
|
|
94
|
-
const blocks = [];
|
|
95
|
-
for (const [file, sql] of grantSqlByFile) {
|
|
96
|
-
const code = sql.replace(/--[^\n]*/g, '');
|
|
97
|
-
for (const m of code.matchAll(new RegExp(REVOKE_RE.source, 'gi'))) {
|
|
98
|
-
if (m[1] === role) blocks.push({ file, key: m[2] });
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return blocks;
|
|
102
|
-
}
|
|
103
93
|
|
|
104
94
|
// The EFFECTIVE grant set: walk the migrations in filename order, adding each
|
|
105
95
|
// file's grants and removing its revocations, so a key granted early and revoked
|
|
@@ -96,9 +96,15 @@ await test('ensureMaintenanceGoal: an EXISTING goal is returned, and nothing is
|
|
|
96
96
|
const goal = await ensureMaintenanceGoal('BONGOS-V1', { pool });
|
|
97
97
|
assert.equal(goal.id, 77);
|
|
98
98
|
assert.ok(!find(pool.queries, /INSERT INTO goals/), 'find-or-create must not create when it finds');
|
|
99
|
-
//
|
|
99
|
+
// BV1.R18 (task 1003605, migration core_234): the lookup is keyed on the
|
|
100
|
+
// VERSION and the `is_maintenance` flag — the title is no longer a parameter,
|
|
101
|
+
// because the title no longer decides anything. It used to bind
|
|
102
|
+
// ['BONGOS-V1', 'BONGOS-V1 — maintenance'].
|
|
100
103
|
const lookup = find(pool.queries, /^SELECT .* FROM goals WHERE version_id/);
|
|
101
|
-
assert.deepEqual(lookup.params, ['BONGOS-V1'
|
|
104
|
+
assert.deepEqual(lookup.params, ['BONGOS-V1']);
|
|
105
|
+
assert.match(lookup.sql, /is_maintenance/);
|
|
106
|
+
assert.equal(/title/.test(lookup.sql.replace(/SELECT[\s\S]*?FROM/, 'SELECT FROM')), false,
|
|
107
|
+
'the title must not appear in the WHERE clause — a goal named into the convention would otherwise be adopted');
|
|
102
108
|
});
|
|
103
109
|
|
|
104
110
|
await test('ensureMaintenanceGoal: the LOCK is taken BEFORE the read, or find-or-create races', async () => {
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// tests/maintenance_goal_flag.mjs — which goal is the maintenance goal is a
|
|
2
|
+
// COLUMN, not a title (BV1.R18, task 1003605, goal 1000086, ADR 0263 §7).
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS MATTERS MORE THAN IT LOOKS. The maintenance goal was identified by
|
|
5
|
+
// matching the title `"<version> — maintenance"`. For ROUTING a homeless bug that
|
|
6
|
+
// was a provenance wrinkle: the worst case is a hand-titled goal receiving a
|
|
7
|
+
// defect. Then R12 made the SAME identification decide the version-close count —
|
|
8
|
+
// `maintenanceGoalExemptSql` answers "which goals hold this version open" — and a
|
|
9
|
+
// wrinkle became a way in: any Metic+ able to create a goal could exempt it from
|
|
10
|
+
// the gate that decides when a version may close, by choosing a string.
|
|
11
|
+
// `goals.is_maintenance` (migration core_234) cannot be spelled into existence.
|
|
12
|
+
//
|
|
13
|
+
// The BACKFILL is the one place the title match legitimately survives: the rows
|
|
14
|
+
// that already exist were made by the convention, so the convention identifies
|
|
15
|
+
// them — once, in the migration, with the schema carrying the fact afterwards.
|
|
16
|
+
//
|
|
17
|
+
// Run: node --test tests/maintenance_goal_flag.mjs
|
|
18
|
+
|
|
19
|
+
import assert from 'node:assert/strict';
|
|
20
|
+
import { test } from 'node:test';
|
|
21
|
+
import { readFileSync } from 'node:fs';
|
|
22
|
+
import { createRequire } from 'node:module';
|
|
23
|
+
import { makeSqlAwareClient } from './helpers.mjs';
|
|
24
|
+
|
|
25
|
+
process.env.NODE_ENV = 'test';
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
const { ensureMaintenanceGoal, carryBugsForward } = require('../modules/lifecycle/db.js');
|
|
28
|
+
const { maintenanceGoalExemptSql } = require('../modules/lifecycle/db-versions.js');
|
|
29
|
+
|
|
30
|
+
const src = (rel) => readFileSync(new URL('../' + rel, import.meta.url), 'utf8');
|
|
31
|
+
const MIGRATION = src('migrations/core_234_goals_is_maintenance.sql');
|
|
32
|
+
const DB_GOALS = src('modules/lifecycle/db-goals.js');
|
|
33
|
+
|
|
34
|
+
function recorder(handler) {
|
|
35
|
+
const seen = [];
|
|
36
|
+
const c = makeSqlAwareClient((sql, params) => { seen.push({ sql, params }); return handler(sql, params); });
|
|
37
|
+
return { client: { query: (sql, params) => c._client.query(sql, params) }, seen };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// The predicate that decides which goals hold a version open
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
test('the close-count exemption reads the FLAG, never a title', () => {
|
|
45
|
+
const sql = maintenanceGoalExemptSql('$1');
|
|
46
|
+
assert.equal(sql, 'NOT g.is_maintenance');
|
|
47
|
+
// The specific regression: any form of title comparison here lets a goal
|
|
48
|
+
// exempt itself from the version-close gate by being named a certain way.
|
|
49
|
+
assert.equal(/title/i.test(sql), false,
|
|
50
|
+
'a title comparison here is a scope widening anyone who can name a goal could perform');
|
|
51
|
+
assert.equal(/maintenance'/.test(sql), false, 'no string literal decides this');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// find-or-create
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
test('ensureMaintenanceGoal FINDS by flag, not by title', async () => {
|
|
59
|
+
const r = recorder((sql) => {
|
|
60
|
+
if (/FROM versions/.test(sql)) return { rows: [{ status: 'building' }] };
|
|
61
|
+
if (/pg_advisory_xact_lock/.test(sql)) return { rows: [] };
|
|
62
|
+
if (/^SELECT .* FROM goals/m.test(sql)) return { rows: [{ id: 7, is_maintenance: true }] };
|
|
63
|
+
return { rows: [] };
|
|
64
|
+
});
|
|
65
|
+
const got = await ensureMaintenanceGoal('BONGOS-V1', { client: r.client });
|
|
66
|
+
assert.equal(got.id, 7);
|
|
67
|
+
const lookup = r.seen.find((q) => /FROM goals/.test(q.sql) && /SELECT/.test(q.sql));
|
|
68
|
+
assert.match(lookup.sql, /WHERE version_id = \$1 AND is_maintenance/);
|
|
69
|
+
assert.equal(/title = \$2/.test(lookup.sql), false, 'the title must no longer select the goal');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('the advisory lock is still taken BEFORE the read', async () => {
|
|
73
|
+
// The lock is what makes find-or-create safe without a unique constraint, and
|
|
74
|
+
// it is keyed on the title purely as a stable per-version string. Moving the
|
|
75
|
+
// identification to a flag must not accidentally drop the serialisation.
|
|
76
|
+
const r = recorder((sql) => {
|
|
77
|
+
if (/FROM versions/.test(sql)) return { rows: [{ status: 'building' }] };
|
|
78
|
+
return { rows: [{ id: 7 }] };
|
|
79
|
+
});
|
|
80
|
+
await ensureMaintenanceGoal('BONGOS-V1', { client: r.client });
|
|
81
|
+
const lockAt = r.seen.findIndex((q) => /pg_advisory_xact_lock/.test(q.sql));
|
|
82
|
+
const readAt = r.seen.findIndex((q) => /FROM goals/.test(q.sql) && /is_maintenance/.test(q.sql));
|
|
83
|
+
assert.ok(lockAt !== -1, 'the advisory lock must still be taken');
|
|
84
|
+
assert.ok(lockAt < readAt, 'the lock must precede the read, or two writers both see "none"');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('a created maintenance goal is flagged in the SAME statement that creates it', () => {
|
|
88
|
+
// Not a follow-up UPDATE: a maintenance goal must never exist unflagged, even
|
|
89
|
+
// for one transaction, or a concurrent close count would not exempt it.
|
|
90
|
+
const insert = DB_GOALS.slice(DB_GOALS.indexOf('INSERT INTO goals (version_id, title, description'));
|
|
91
|
+
assert.match(insert.slice(0, 400), /is_maintenance/);
|
|
92
|
+
assert.match(insert.slice(0, 400), /true/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('a version that cannot hold work gets no maintenance goal', async () => {
|
|
96
|
+
for (const status of ['shipped', 'planning', 'frozen']) {
|
|
97
|
+
const r = recorder(() => ({ rows: [{ status }] }));
|
|
98
|
+
assert.equal(await ensureMaintenanceGoal('V', { client: r.client }), null, `status=${status}`);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// The carry-over — the one goal that legitimately spans versions
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
test('carryBugsForward moves only NON-TERMINAL tasks', async () => {
|
|
107
|
+
const r = recorder((sql) => {
|
|
108
|
+
// `SELECT id FROM goals` is carryBugsForward's own source read;
|
|
109
|
+
// `SELECT id, version_id, …` is ensureMaintenanceGoal's GOAL_COLS read.
|
|
110
|
+
// The sql-aware client collapses whitespace, so the two must be told apart
|
|
111
|
+
// by their projection rather than by line breaks.
|
|
112
|
+
if (/SELECT id FROM goals/.test(sql)) return { rows: [{ id: 1 }] };
|
|
113
|
+
if (/FROM versions/.test(sql)) return { rows: [{ status: 'building' }] };
|
|
114
|
+
if (/pg_advisory_xact_lock/.test(sql)) return { rows: [] };
|
|
115
|
+
if (/SELECT id, version_id/.test(sql)) return { rows: [{ id: 5 }] };
|
|
116
|
+
if (/UPDATE tasks/.test(sql)) return { rows: [{ id: '11' }, { id: '12' }] };
|
|
117
|
+
return { rows: [] };
|
|
118
|
+
});
|
|
119
|
+
const out = await carryBugsForward({ fromVersionId: 'V1', toVersionId: 'V2' }, { client: r.client });
|
|
120
|
+
assert.deepEqual(out, { fromGoalId: '1', toGoalId: '5', moved: 2 });
|
|
121
|
+
const move = r.seen.find((q) => /UPDATE tasks/.test(q.sql));
|
|
122
|
+
assert.match(move.sql, /status NOT IN \('shipped', 'abandoned'\)/,
|
|
123
|
+
'a fixed or cut defect belongs to the version it was resolved in — carrying it forward would make every successor an archive');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('a version with no maintenance goal carries nothing, and that is not an error', async () => {
|
|
127
|
+
const r = recorder(() => ({ rows: [] }));
|
|
128
|
+
const out = await carryBugsForward({ fromVersionId: 'V1', toVersionId: 'V2' }, { client: r.client });
|
|
129
|
+
assert.deepEqual(out, { fromGoalId: null, toGoalId: null, moved: 0 });
|
|
130
|
+
assert.equal(r.seen.some((q) => /UPDATE tasks/.test(q.sql)), false, 'nothing is written');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('carrying into a version that is not building FAILS LOUDLY', async () => {
|
|
134
|
+
// ADR 0263 §8: promotion must happen BEFORE the carry-over, because
|
|
135
|
+
// ensureMaintenanceGoal returns null for a version that is not `building`. A
|
|
136
|
+
// silent no-op here would strand the bug queue on a shipped version — the
|
|
137
|
+
// exact entombment this whole path exists to end — and look like success.
|
|
138
|
+
const r = recorder((sql) => {
|
|
139
|
+
if (/SELECT id FROM goals/.test(sql)) return { rows: [{ id: 1 }] };
|
|
140
|
+
if (/FROM versions/.test(sql)) return { rows: [{ status: 'planning' }] };
|
|
141
|
+
return { rows: [] };
|
|
142
|
+
});
|
|
143
|
+
await assert.rejects(
|
|
144
|
+
() => carryBugsForward({ fromVersionId: 'V1', toVersionId: 'V2' }, { client: r.client }),
|
|
145
|
+
(e) => e.code === 'CARRY_FORWARD_NO_DESTINATION'
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('carryBugsForward does NOT refuse an actively-claimed task', () => {
|
|
150
|
+
// A deliberate divergence from archiveGoalWithDispositions (R14), which does.
|
|
151
|
+
// Archiving abandons a task out from under whoever holds it; carrying forward
|
|
152
|
+
// only re-points its goal, and the claim references the task, so nothing is
|
|
153
|
+
// taken from anyone. Refusing here would let one in-flight bug fix block a
|
|
154
|
+
// version from ever closing.
|
|
155
|
+
const fn = DB_GOALS.slice(DB_GOALS.indexOf('async function carryBugsForward'));
|
|
156
|
+
const body = fn.slice(0, fn.indexOf('\n}\n'));
|
|
157
|
+
assert.equal(/FROM claims/.test(body), false, 'a close must not be hostage to a claim on a defect');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// The property the whole task exists for: the flag has no user write path
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
test('NOTHING but the system create can set is_maintenance', () => {
|
|
165
|
+
// THE POINT OF THE COLUMN. Moving the identification from a title to a flag
|
|
166
|
+
// only closes the vector if the flag itself cannot be set by whoever could
|
|
167
|
+
// previously choose the title. Every `UPDATE goals` in the lifecycle names its
|
|
168
|
+
// columns explicitly — there is no generic passthrough — so this asserts that
|
|
169
|
+
// none of them names this one, and that the sole writer is the system
|
|
170
|
+
// find-or-create (`created_by NULL`) plus the one-time migration backfill.
|
|
171
|
+
//
|
|
172
|
+
// If a future edit adds a goal-field update that accepts arbitrary columns,
|
|
173
|
+
// this is what should stop it.
|
|
174
|
+
const files = [
|
|
175
|
+
'modules/lifecycle/db-goals.js',
|
|
176
|
+
'modules/lifecycle/db-versions.js',
|
|
177
|
+
'modules/lifecycle/goal-edits.js',
|
|
178
|
+
'modules/lifecycle/goal-category.js',
|
|
179
|
+
'modules/lifecycle/done-when.js',
|
|
180
|
+
];
|
|
181
|
+
for (const rel of files) {
|
|
182
|
+
const code = src(rel).replace(/\/\/[^\n]*/g, '');
|
|
183
|
+
for (const m of code.matchAll(/UPDATE goals[\s\S]{0,400}?(?=RETURNING|WHERE id|;|`)/g)) {
|
|
184
|
+
assert.equal(/\bis_maintenance\s*=/.test(m[0]), false,
|
|
185
|
+
`${rel}: an UPDATE sets is_maintenance — the flag must have no write path a caller can reach, or naming a goal is replaced by flagging one`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// The migration
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
test('the column is NOT NULL with a false default, so no goal is ambiguously flagged', () => {
|
|
195
|
+
assert.match(MIGRATION, /ADD COLUMN IF NOT EXISTS is_maintenance boolean NOT NULL DEFAULT false/);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('the backfill is anchored on the version id, not a bare LIKE', () => {
|
|
199
|
+
// `title LIKE '%maintenance%'` would have flagged a goal legitimately called
|
|
200
|
+
// "Maintenance of the art pipeline" and silently exempted it from the close
|
|
201
|
+
// gate — the very failure the column exists to remove.
|
|
202
|
+
assert.match(MIGRATION, /title = version_id \|\| ' — maintenance'/);
|
|
203
|
+
// Comments stripped first: this file's own header explains why a bare
|
|
204
|
+
// `LIKE '%maintenance%'` would be wrong, and a check that read the prose
|
|
205
|
+
// would fail on the explanation rather than on the SQL.
|
|
206
|
+
const sqlOnly = MIGRATION.replace(/--[^\n]*/g, '');
|
|
207
|
+
assert.equal(/LIKE\s*'%maintenance/i.test(sqlOnly), false);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('the migration records WHY there is no unique constraint yet', () => {
|
|
211
|
+
// A partial unique index would be tidy and would FAIL on any database that
|
|
212
|
+
// already holds two title-matching goals for one version — which is possible,
|
|
213
|
+
// because `goals` has no unique constraint on (version_id, title). Same lesson
|
|
214
|
+
// core_233 recorded about the building-version index.
|
|
215
|
+
assert.match(MIGRATION, /NOT UNIQUE PER VERSION/);
|
|
216
|
+
assert.match(MIGRATION, /core_233/, 'the precedent is named so the reasoning is findable');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('the find-or-create survives a pre-existing duplicate rather than throwing', () => {
|
|
220
|
+
const fn = DB_GOALS.slice(DB_GOALS.indexOf('async function ensureMaintenanceGoal'));
|
|
221
|
+
assert.match(fn.slice(0, 2500), /ORDER BY id LIMIT 1/,
|
|
222
|
+
'with no unique constraint, a duplicate must be survivable and the answer stable');
|
|
223
|
+
});
|
|
@@ -130,9 +130,16 @@ await test('roll_forward WITH a planning version proceeds', () => {
|
|
|
130
130
|
// ---- the maintenance exemption ---------------------------------------------
|
|
131
131
|
|
|
132
132
|
await test('the exemption has ONE definition, and the open-goals read is its only caller', () => {
|
|
133
|
-
// R18 (task 1003605)
|
|
134
|
-
//
|
|
135
|
-
|
|
133
|
+
// R18 (task 1003605, migration core_234) SWAPPED this body, exactly as R12
|
|
134
|
+
// predicted here — the single definition is what made it a one-line change
|
|
135
|
+
// instead of a hunt, and this assertion moved with it.
|
|
136
|
+
//
|
|
137
|
+
// The old body was `g.title <> ($1 || ' — maintenance')`, which decided by
|
|
138
|
+
// STRING which goals hold a version open: anyone able to name a goal could
|
|
139
|
+
// exempt it from the close gate. A boolean column cannot be spelled.
|
|
140
|
+
assert.equal(maintenanceGoalExemptSql('$1'), 'NOT g.is_maintenance');
|
|
141
|
+
assert.equal(/title/i.test(maintenanceGoalExemptSql('$1')), false,
|
|
142
|
+
'no title comparison may decide which goals hold a version open');
|
|
136
143
|
const dbv = src('modules/lifecycle/db-versions.js');
|
|
137
144
|
const uses = (dbv.match(/maintenanceGoalExemptSql\(/g) || []).length;
|
|
138
145
|
assert.equal(uses, 3, 'declaration + the open-goals read + the post-apply re-count, and nothing else');
|