@bongos/core 1.21.3 → 1.21.4
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 +22 -17
- package/docs/module-api-changelog.md +2 -0
- package/migrations/core_272_rename_cn_criterion_slugs.sql +40 -0
- package/modules/lifecycle/criterion-ref.js +11 -0
- package/modules/lifecycle/db-tasks.js +7 -0
- package/modules/lifecycle/routes/done-when.js +6 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/criterion_goal_attach.mjs +12 -4
- package/tests/criterion_ref_parse.mjs +41 -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.21.
|
|
6
|
-
"core_contract": "1.21.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.21.4",
|
|
6
|
+
"core_contract": "1.21.4",
|
|
7
|
+
"source_commit": "227ae34b0487683d4edf44ffe2bc124e642bdad7",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-10-
|
|
9
|
+
"built_at": "2026-10-02T02:09:36.609Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 576,
|
|
13
13
|
"agent_docs_stubbed": 27,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2849,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 3453,
|
|
20
|
+
"tree_sha256": "745f91270b3cdca0328608ed1dad6684e2aebe6a52a1fe7e294d9016458f470b",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2817,7 +2817,7 @@
|
|
|
2817
2817
|
{
|
|
2818
2818
|
"path": "docs/module-api-changelog.md",
|
|
2819
2819
|
"mode": "0000644",
|
|
2820
|
-
"sha256": "
|
|
2820
|
+
"sha256": "53996e5c9e4a988cff115ec80dc2d61b9f16fa151c4c96fb6ccd2b5e1b6b6a29"
|
|
2821
2821
|
},
|
|
2822
2822
|
{
|
|
2823
2823
|
"path": "docs/modules-contract.md",
|
|
@@ -4029,6 +4029,11 @@
|
|
|
4029
4029
|
"mode": "0000644",
|
|
4030
4030
|
"sha256": "7b05f548a3498aa472267ca10b26236cc81fa52df0334be9ec89c9d620866e2f"
|
|
4031
4031
|
},
|
|
4032
|
+
{
|
|
4033
|
+
"path": "migrations/core_272_rename_cn_criterion_slugs.sql",
|
|
4034
|
+
"mode": "0000644",
|
|
4035
|
+
"sha256": "a9a435bf6e5d6bc94f6029a9375519c80041cbac8d791f98989dffc09b5b1dfe"
|
|
4036
|
+
},
|
|
4032
4037
|
{
|
|
4033
4038
|
"path": "modules/agents/CLAUDE.md",
|
|
4034
4039
|
"mode": "0000644",
|
|
@@ -6572,7 +6577,7 @@
|
|
|
6572
6577
|
{
|
|
6573
6578
|
"path": "modules/lifecycle/criterion-ref.js",
|
|
6574
6579
|
"mode": "0000644",
|
|
6575
|
-
"sha256": "
|
|
6580
|
+
"sha256": "90f77854fb0d0c7c70d9bd20f018100e555c72920840e84afe24789531f1db2f"
|
|
6576
6581
|
},
|
|
6577
6582
|
{
|
|
6578
6583
|
"path": "modules/lifecycle/criterion-suggest.js",
|
|
@@ -6652,7 +6657,7 @@
|
|
|
6652
6657
|
{
|
|
6653
6658
|
"path": "modules/lifecycle/db-tasks.js",
|
|
6654
6659
|
"mode": "0000644",
|
|
6655
|
-
"sha256": "
|
|
6660
|
+
"sha256": "2efcf47f6240b7a21e1d908440a04e3f0af3e86ca22aed5b74ba03ed354de437"
|
|
6656
6661
|
},
|
|
6657
6662
|
{
|
|
6658
6663
|
"path": "modules/lifecycle/db-versions.js",
|
|
@@ -6997,7 +7002,7 @@
|
|
|
6997
7002
|
{
|
|
6998
7003
|
"path": "modules/lifecycle/routes/done-when.js",
|
|
6999
7004
|
"mode": "0000644",
|
|
7000
|
-
"sha256": "
|
|
7005
|
+
"sha256": "a6adbf715fb5f2994a0f4603859e5047b321311b66e04d9fcb96ce9ce854c5b3"
|
|
7001
7006
|
},
|
|
7002
7007
|
{
|
|
7003
7008
|
"path": "modules/lifecycle/routes/gate-approvals.js",
|
|
@@ -9602,12 +9607,12 @@
|
|
|
9602
9607
|
{
|
|
9603
9608
|
"path": "package-lock.json",
|
|
9604
9609
|
"mode": "0000644",
|
|
9605
|
-
"sha256": "
|
|
9610
|
+
"sha256": "b2c580b1a1dac4d154f0210d9a505aff2793d869c4a9daf2e74662684924e71b"
|
|
9606
9611
|
},
|
|
9607
9612
|
{
|
|
9608
9613
|
"path": "package.json",
|
|
9609
9614
|
"mode": "0000644",
|
|
9610
|
-
"sha256": "
|
|
9615
|
+
"sha256": "ea4e035d42d25f49b75519085179ae3162c28a9ea263d47db99532354c4e4c4b"
|
|
9611
9616
|
},
|
|
9612
9617
|
{
|
|
9613
9618
|
"path": "public-docs/index.html",
|
|
@@ -9627,7 +9632,7 @@
|
|
|
9627
9632
|
{
|
|
9628
9633
|
"path": "release-notes.json",
|
|
9629
9634
|
"mode": "0000644",
|
|
9630
|
-
"sha256": "
|
|
9635
|
+
"sha256": "39ceec2a9fbb1e216f935e73b23f3a380ba319100a13f99bf71bc3783d90e987"
|
|
9631
9636
|
},
|
|
9632
9637
|
{
|
|
9633
9638
|
"path": "scripts/bongos-mcp.js",
|
|
@@ -11842,7 +11847,7 @@
|
|
|
11842
11847
|
{
|
|
11843
11848
|
"path": "src/module-api.js",
|
|
11844
11849
|
"mode": "0000644",
|
|
11845
|
-
"sha256": "
|
|
11850
|
+
"sha256": "59ddf172c5c74312f7140b65decd20aa6e6e54778ba47b7d3b78913fdea45d4a"
|
|
11846
11851
|
},
|
|
11847
11852
|
{
|
|
11848
11853
|
"path": "src/module-loader/catalog.js",
|
|
@@ -12897,7 +12902,7 @@
|
|
|
12897
12902
|
{
|
|
12898
12903
|
"path": "tests/criterion_goal_attach.mjs",
|
|
12899
12904
|
"mode": "0000644",
|
|
12900
|
-
"sha256": "
|
|
12905
|
+
"sha256": "49a2d85d679669efd63c0b2600bd2f207106f37f00cb5beebcd0e6a4775b6452"
|
|
12901
12906
|
},
|
|
12902
12907
|
{
|
|
12903
12908
|
"path": "tests/criterion_progress.mjs",
|
|
@@ -12912,7 +12917,7 @@
|
|
|
12912
12917
|
{
|
|
12913
12918
|
"path": "tests/criterion_ref_parse.mjs",
|
|
12914
12919
|
"mode": "0000644",
|
|
12915
|
-
"sha256": "
|
|
12920
|
+
"sha256": "8373511dace3ecb37d88961d22a9f8fb2f01fb34d12a0075d287b78ef02d398a"
|
|
12916
12921
|
},
|
|
12917
12922
|
{
|
|
12918
12923
|
"path": "tests/criterion_ref_status.mjs",
|
|
@@ -2833,5 +2833,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2833
2833
|
landed since 1.21.1 with no explicit bump. run 36943290103. (task 1002620)
|
|
2834
2834
|
1.21.3 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2835
2835
|
landed since 1.21.2 with no explicit bump. run 36944637655. (task 1002620)
|
|
2836
|
+
1.21.4 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2837
|
+
landed since 1.21.3 with no explicit bump. run 36954271746. (task 1002620)
|
|
2836
2838
|
---------------------------------------------------------------------------
|
|
2837
2839
|
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
-- core_272 — rename goal 1000045's criteria off the positional shape (task 1003122)
|
|
2
|
+
--
|
|
3
|
+
-- parseCriterionRef reads a ref shaped C1, C2... as a POSITION before it ever
|
|
4
|
+
-- consults slugs, so a criterion whose criterion_id is literally 'C5' cannot be
|
|
5
|
+
-- named by its own slug (and on a multi-goal version the ref is refused as
|
|
6
|
+
-- ambiguous). Goal 1000045 slugged ten criteria C1..C10, and a seeder passing 'C5'
|
|
7
|
+
-- meaning that goal's C5 got another goal's criterion. createCriterion now refuses
|
|
8
|
+
-- the shape; this renames the ten that already exist, once, owner-approved names.
|
|
9
|
+
--
|
|
10
|
+
-- Links are by done_when_criteria.id (task_criteria.criterion_id), so no task link
|
|
11
|
+
-- moves. Scoped by goal AND old slug, so it is idempotent and touches nothing on an
|
|
12
|
+
-- instance that never had this goal. Data only: forward-safe.
|
|
13
|
+
--
|
|
14
|
+
-- WHY core/ AND NOT migrations/instance/ (ADR 0105). That directory does not exist
|
|
15
|
+
-- in the portable core, and the instance seed path only runs under the owner opt-in
|
|
16
|
+
-- GDS_APPLY_INSTANCE_SEEDS=1 (a DR step), so a rename placed there would never run
|
|
17
|
+
-- on the live instance that needs it. The cost is a no-op UPDATE on every other
|
|
18
|
+
-- host; the double guard (this goal id AND the exact old slug AND the new slug free)
|
|
19
|
+
-- keeps it from touching an unrelated goal that happens to share the id.
|
|
20
|
+
|
|
21
|
+
UPDATE done_when_criteria c
|
|
22
|
+
SET criterion_id = m.new_slug
|
|
23
|
+
FROM (VALUES
|
|
24
|
+
('C1', 'bp-public-cross-project-record'),
|
|
25
|
+
('C2', 'bp-hide-my-record'),
|
|
26
|
+
('C3', 'bp-apply-from-orbs'),
|
|
27
|
+
('C4', 'bp-application-carries-profile'),
|
|
28
|
+
('C5', 'bp-owner-browses-builders'),
|
|
29
|
+
('C6', 'bp-builder-connections'),
|
|
30
|
+
('C7', 'bp-guilds'),
|
|
31
|
+
('C8', 'bp-guild-apply-and-invite'),
|
|
32
|
+
('C9', 'bp-guild-map'),
|
|
33
|
+
('C10', 'bp-connection-graph-to-guild')
|
|
34
|
+
) AS m(old_slug, new_slug)
|
|
35
|
+
WHERE c.goal_id = 1000045
|
|
36
|
+
AND c.criterion_id = m.old_slug
|
|
37
|
+
AND NOT EXISTS (
|
|
38
|
+
SELECT 1 FROM done_when_criteria d
|
|
39
|
+
WHERE d.version_id = c.version_id AND d.criterion_id = m.new_slug
|
|
40
|
+
);
|
|
@@ -46,6 +46,16 @@ function parseCriterionRef(raw) {
|
|
|
46
46
|
return { kind: 'slug', slug: ref };
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// A criterion_id slug may not be shaped like a positional token (task 1003122).
|
|
50
|
+
// parseCriterionRef reads 'C5' as a position BEFORE it ever consults slugs, so a
|
|
51
|
+
// criterion whose slug is literally 'C5' is unreachable by its own name — and on a
|
|
52
|
+
// multi-goal version the ref is refused as ambiguous. Goal 1000045 slugged ten
|
|
53
|
+
// criteria 'C1'..'C10' and a seeder's 'C5' landed on another goal's criterion.
|
|
54
|
+
// Refuse the shape at write time so the ambiguity cannot exist (option b).
|
|
55
|
+
function isReservedCriterionSlug(raw) {
|
|
56
|
+
return parseCriterionRef(raw).kind === 'cnum';
|
|
57
|
+
}
|
|
58
|
+
|
|
49
59
|
// A version id as this project writes them: an uppercase prefix (possibly none)
|
|
50
60
|
// followed by V + digits — V1, PMS-V2, GDS-V4, BONGOS-V2, CB-V1.
|
|
51
61
|
//
|
|
@@ -101,6 +111,7 @@ function criterionLabel(c) {
|
|
|
101
111
|
|
|
102
112
|
module.exports = {
|
|
103
113
|
parseCriterionRef,
|
|
114
|
+
isReservedCriterionSlug,
|
|
104
115
|
looksLikeVersionId,
|
|
105
116
|
resolveCriterionIn,
|
|
106
117
|
criterionLabel,
|
|
@@ -18,6 +18,7 @@ const { matchProtected } = api.permissionPathCheck;
|
|
|
18
18
|
const { decorateGoalMembership, capNewcomerTasks } = require('./claim-feed.js');
|
|
19
19
|
const { deriveRequiredRank, highestRank } = require('./db-rank-authz.js');
|
|
20
20
|
const { executorFor } = require('./db-executor.js');
|
|
21
|
+
const { isReservedCriterionSlug } = require('./criterion-ref.js');
|
|
21
22
|
const { resolveWriteDiscipline, WRITABLE_DISCIPLINES } = require('./task-classifier.js');
|
|
22
23
|
|
|
23
24
|
// -------------------------------------------------------------------------
|
|
@@ -366,6 +367,12 @@ async function updateTaskColumn(id, column, value) {
|
|
|
366
367
|
// it are one decision, and splitting them across connections reopens a narrower
|
|
367
368
|
// version of the same race — two creates reading the same next order.
|
|
368
369
|
async function createCriterion({ versionId, criterionId, criterionMd, goalId = null, sortOrder = null }, deps = {}) {
|
|
370
|
+
if (isReservedCriterionSlug(criterionId)) {
|
|
371
|
+
// Task 1003122: a 'Cn' slug is unreachable by name (the positional reading wins).
|
|
372
|
+
const err = new Error(`criterion_id '${criterionId}' is shaped like a positional ref (C1, C2, ...); choose a descriptive slug.`);
|
|
373
|
+
err.code = 'RESERVED_CRITERION_SLUG';
|
|
374
|
+
throw err;
|
|
375
|
+
}
|
|
369
376
|
const executor = executorFor(deps, pool);
|
|
370
377
|
let order = sortOrder;
|
|
371
378
|
if (order == null) {
|
|
@@ -23,6 +23,7 @@ const { isGoalOwnerOrManager } = require('../goal-authz');
|
|
|
23
23
|
const goalEdits = require('../goal-edits.js');
|
|
24
24
|
// task 1004392 — /satisfy is the override now and records itself as one.
|
|
25
25
|
const uatDb = require('../criterion-uat-db.js');
|
|
26
|
+
const { isReservedCriterionSlug } = require('../criterion-ref.js');
|
|
26
27
|
const { validateOrRespond, parseId, asyncHandler, LIMITS } = api;
|
|
27
28
|
|
|
28
29
|
// authorizeCriterionCreate — the ORDERED, FAIL-CLOSED decision for a non-Archon
|
|
@@ -132,6 +133,11 @@ module.exports = function buildDoneWhenRouter() {
|
|
|
132
133
|
goal_id: { type: 'integer', min: 1 },
|
|
133
134
|
sort_order: { type: 'integer', min: 0 },
|
|
134
135
|
})) return;
|
|
136
|
+
// Task 1003122: refuse before the transaction takes its locks. createCriterion
|
|
137
|
+
// enforces the same rule for every other caller (the ratified-goal path).
|
|
138
|
+
if (isReservedCriterionSlug(req.body.criterion_id)) {
|
|
139
|
+
return res.fail('reserved_criterion_slug', { status: 400, message: 'criterion_id may not be shaped like a positional ref (C1, C2, ...); choose a descriptive slug.' });
|
|
140
|
+
}
|
|
135
141
|
// bad_goal_id now means exactly what it says: the goal is absent or belongs
|
|
136
142
|
// to another version. (The unauthorized case rendered through this same
|
|
137
143
|
// helper until task 1002614 — see the mask note in authorizeCriterionCreate.)
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.4",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.4",
|
|
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.21.
|
|
3
|
+
"version": "1.21.4",
|
|
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
|
@@ -8710,5 +8710,11 @@
|
|
|
8710
8710
|
"id": "1004532",
|
|
8711
8711
|
"text": "Hovering over a badge on the builders list or the public status page now shows its real name, such as \"Art Pipeline Engineer\" or \"A Thought That Built Ten\", instead of words made from its internal code. Screen readers read the"
|
|
8712
8712
|
}
|
|
8713
|
+
],
|
|
8714
|
+
"1.21.4": [
|
|
8715
|
+
{
|
|
8716
|
+
"id": "1003122",
|
|
8717
|
+
"text": "New criteria can no longer be named like C5, which the system always read as a position and so could never find by name."
|
|
8718
|
+
}
|
|
8713
8719
|
]
|
|
8714
8720
|
}
|
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.21.
|
|
78
|
+
const CORE_VERSION = '1.21.4'; // 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');
|
|
@@ -487,7 +487,7 @@ await test('FOR UPDATE outside a transaction is refused, not silently downgraded
|
|
|
487
487
|
await test('CREATE: the goal is locked, inside the transaction (task 1003840)', async () => {
|
|
488
488
|
actor = ARCHON;
|
|
489
489
|
goalReads.length = 0;
|
|
490
|
-
const r = await post(V1, { criterion_id: '
|
|
490
|
+
const r = await post(V1, { criterion_id: 'brand-new-slug', criterion_md: 'x', goal_id: 3 });
|
|
491
491
|
assert.equal(r.status, 201, JSON.stringify(r.body));
|
|
492
492
|
assert.ok(goalReads.length > 0, 'the route read the goal');
|
|
493
493
|
for (const g of goalReads) {
|
|
@@ -498,7 +498,7 @@ await test('CREATE: the goal is locked, inside the transaction (task 1003840)',
|
|
|
498
498
|
|
|
499
499
|
await test('CREATE: the INSERT runs ON the transaction client (task 1003840)', async () => {
|
|
500
500
|
actor = ARCHON;
|
|
501
|
-
await post(V1, { criterion_id: '
|
|
501
|
+
await post(V1, { criterion_id: 'brand-new-slug', criterion_md: 'x', goal_id: 3 });
|
|
502
502
|
const inserts = sqlLog.filter((q) => /INSERT INTO done_when_criteria/.test(q.sql));
|
|
503
503
|
assert.equal(inserts.length, 1, 'exactly one INSERT');
|
|
504
504
|
assert.equal(inserts[0].via, 'client', 'the INSERT must not leave the transaction');
|
|
@@ -512,7 +512,7 @@ await test('CREATE: the INSERT runs ON the transaction client (task 1003840)', a
|
|
|
512
512
|
await test('CREATE: a refusal writes nothing and releases the client (task 1003840)', async () => {
|
|
513
513
|
actor = METIC;
|
|
514
514
|
goals.set(3, { id: 3, version_id: V1, owner_builder_id: 999, status: 'open' });
|
|
515
|
-
const r = await post(V1, { criterion_id: '
|
|
515
|
+
const r = await post(V1, { criterion_id: 'brand-new-slug', criterion_md: 'x', goal_id: 3 });
|
|
516
516
|
assert.ok(r.status >= 400, `expected a refusal, got ${r.status}`);
|
|
517
517
|
assert.equal(sqlLog.filter((q) => /INSERT INTO done_when_criteria/.test(q.sql)).length, 0, 'nothing was inserted');
|
|
518
518
|
assert.ok(clients.length > 0 && clients.every((c) => c.released), 'the client is released on the refusal path');
|
|
@@ -524,7 +524,7 @@ await test('the VERSION is locked too, not only the goal (task 1003840)', async
|
|
|
524
524
|
// version could still be frozen between this read and this COMMIT.
|
|
525
525
|
actor = ARCHON;
|
|
526
526
|
versionReads.length = 0;
|
|
527
|
-
await post(V1, { criterion_id: '
|
|
527
|
+
await post(V1, { criterion_id: 'brand-new-slug', criterion_md: 'x', goal_id: 3 });
|
|
528
528
|
assert.ok(versionReads.length > 0, 'the route read the version');
|
|
529
529
|
for (const v of versionReads) {
|
|
530
530
|
assert.ok(v.deps && v.deps.client, 'the version was read outside the transaction');
|
|
@@ -566,6 +566,14 @@ await test('setGoalStatus writes on the transaction client (task 1003840)', asyn
|
|
|
566
566
|
assert.match(seen[1].sql, /UPDATE goal_membership_requests/);
|
|
567
567
|
});
|
|
568
568
|
|
|
569
|
+
await test('POST refuses a Cn-shaped criterion_id with a 400 before any SQL runs (task 1003122)', async () => {
|
|
570
|
+
actor = ARCHON;
|
|
571
|
+
const r = await post(V1, { criterion_id: 'C5', criterion_md: 'x', goal_id: 3 });
|
|
572
|
+
assert.equal(r.status, 400);
|
|
573
|
+
assert.equal(r.body.error, 'reserved_criterion_slug');
|
|
574
|
+
assert.equal(sqlLog.length, 0, 'refused before the transaction took any lock');
|
|
575
|
+
});
|
|
576
|
+
|
|
569
577
|
// The wrapper installed above is removed, so nothing after this file's own tests
|
|
570
578
|
// inherits a patched module export.
|
|
571
579
|
db.getGoal = realGetGoal;
|
|
@@ -16,6 +16,7 @@ import { createRequire } from 'node:module';
|
|
|
16
16
|
|
|
17
17
|
const require = createRequire(import.meta.url);
|
|
18
18
|
const db = require('../modules/lifecycle/db.js');
|
|
19
|
+
const { isReservedCriterionSlug } = require('../modules/lifecycle/criterion-ref.js');
|
|
19
20
|
|
|
20
21
|
let passed = 0;
|
|
21
22
|
let failed = 0;
|
|
@@ -51,6 +52,20 @@ test('a "Cn" embedded in a longer string does NOT match (anchored)', () => {
|
|
|
51
52
|
assert.equal(db.parseCriterionRef('see C8 above').kind, 'slug');
|
|
52
53
|
});
|
|
53
54
|
|
|
55
|
+
test('a Cn-shaped slug is reserved; real slugs are not (task 1003122)', () => {
|
|
56
|
+
for (const r of ['C5', 'c10', ' C1 ']) assert.equal(isReservedCriterionSlug(r), true, r);
|
|
57
|
+
for (const r of ['C', 'C8x', 'wa6-kernel', 'C-5', '5', '']) assert.equal(isReservedCriterionSlug(r), false, r);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("core_272 renames all ten of goal 1000045's Cn slugs to non-Cn names (task 1003122)", () => {
|
|
61
|
+
const sql = require('node:fs').readFileSync(new URL('../migrations/core_272_rename_cn_criterion_slugs.sql', import.meta.url), 'utf8');
|
|
62
|
+
const pairs = [...sql.matchAll(/\('(C\d+)',\s*'([^']+)'\)/g)].map((m) => [m[1], m[2]]);
|
|
63
|
+
assert.deepEqual(pairs.map((p) => p[0]), ['C1','C2','C3','C4','C5','C6','C7','C8','C9','C10']);
|
|
64
|
+
assert.equal(new Set(pairs.map((p) => p[1])).size, 10, 'new slugs are distinct');
|
|
65
|
+
for (const [, n] of pairs) assert.equal(isReservedCriterionSlug(n), false, n);
|
|
66
|
+
assert.match(sql, /goal_id = 1000045/);
|
|
67
|
+
});
|
|
68
|
+
|
|
54
69
|
// --- resolveCriterionIds: the multi-goal refusal (task 1003108) -------------
|
|
55
70
|
// resolveCriterionIds is DB-backed, but it takes an `executor` seam (the one
|
|
56
71
|
// countTasksWithoutCriterionLink uses), so a fake SQL-recording client covers the
|
|
@@ -125,6 +140,32 @@ async function main() {
|
|
|
125
140
|
assert.deepEqual(r, { ids: [], unresolved: [], ambiguous: [] });
|
|
126
141
|
});
|
|
127
142
|
|
|
143
|
+
await atest('a criterion whose slug is literally "C5" is read positionally, never by slug (task 1003122)', async () => {
|
|
144
|
+
// The incident shape: goal 1000045 slugs ten criteria C1..C10. The ref "C5"
|
|
145
|
+
// takes the positional branch on a single-goal version (the 5th row, NOT the
|
|
146
|
+
// row slugged C5) and is refused on a multi-goal one, so the slug is
|
|
147
|
+
// unreachable — which is why creation now refuses the shape.
|
|
148
|
+
const rows = [
|
|
149
|
+
{ id: 601, criterion_id: 'alpha', cnum: 1 },
|
|
150
|
+
{ id: 602, criterion_id: 'C1', cnum: 2 },
|
|
151
|
+
{ id: 603, criterion_id: 'C5', cnum: 3 },
|
|
152
|
+
];
|
|
153
|
+
const ex = (goals) => ({ async query(sql) { return /FROM goals/.test(sql) ? { rows: [{ n: goals }] } : { rows }; } });
|
|
154
|
+
const single = await db.resolveCriterionIds('V9', ['C1'], ex(1));
|
|
155
|
+
assert.deepEqual(single.ids, [601], 'C1 means position 1, not the row slugged C1 (602)');
|
|
156
|
+
const multi = await db.resolveCriterionIds('V9', ['C5'], ex(2));
|
|
157
|
+
assert.deepEqual(multi.ids, []);
|
|
158
|
+
assert.deepEqual(multi.ambiguous, ['C5']);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
await atest('createCriterion refuses a Cn slug before touching the database (task 1003122)', async () => {
|
|
162
|
+
const ex = { query: async () => { throw new Error('must not query'); } };
|
|
163
|
+
await assert.rejects(
|
|
164
|
+
db.createCriterion({ versionId: 'V9', criterionId: 'C5', criterionMd: 'x', goalId: 1 }, { client: ex }),
|
|
165
|
+
(e) => e.code === 'RESERVED_CRITERION_SLUG'
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
128
169
|
console.log(`\ncriterion ref parse (task 438) + multi-goal refusal (task 1003108) tests: ${passed} passed, ${failed} failed`);
|
|
129
170
|
process.exit(failed > 0 ? 1 : 0);
|
|
130
171
|
}
|