@bongos/core 1.19.786 → 1.19.787

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 CHANGED
@@ -2,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.786",
6
- "core_contract": "1.19.786",
7
- "source_commit": "785c921afb48af049814782f27929cc27885667c",
5
+ "core_version": "1.19.787",
6
+ "core_contract": "1.19.787",
7
+ "source_commit": "b32b78533de72aa607032c7eeea369d98103ef3b",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-16T22:20:27.767Z",
9
+ "built_at": "2026-09-16T22:32:51.862Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 483,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 2668,
20
- "tree_sha256": "3d454ef4a876f5368603708cbe4f13b53dc57a854dc03e217285b327b6dd0bf9",
20
+ "tree_sha256": "6633332cd057cd360e74849250fa2f7ab2458f7f683c8c6315c4820d19d06ac7",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -207,7 +207,7 @@
207
207
  {
208
208
  "path": ".claude/skills/planning-session/SKILL.md",
209
209
  "mode": "0000644",
210
- "sha256": "8ed3e62213268015e49ad180d9f9c0c3c875867b392e93430a9850962a70455b"
210
+ "sha256": "ede2aa15718bff19d38ffa7f42d64bae7490a9025f7e85d43c43306da3f611b6"
211
211
  },
212
212
  {
213
213
  "path": ".claude/skills/priority-session/SKILL.md",
@@ -2512,7 +2512,7 @@
2512
2512
  {
2513
2513
  "path": "docs/module-api-changelog.md",
2514
2514
  "mode": "0000644",
2515
- "sha256": "580f9ae0d3677bc11fe335f73027c314edd8633f2397cd71400e07725a8f4a03"
2515
+ "sha256": "3c0fb2e250b9dca8299cae204683c9fb815421e52206b44472b94952d7e61ede"
2516
2516
  },
2517
2517
  {
2518
2518
  "path": "docs/modules-contract.md",
@@ -5447,7 +5447,7 @@
5447
5447
  {
5448
5448
  "path": "modules/lifecycle/db-deps-criteria.js",
5449
5449
  "mode": "0000644",
5450
- "sha256": "04406d383068e42eafed42fa51cf9108df2907f0807b1ed7bcd0abc9eaf7bfb7"
5450
+ "sha256": "2fefa2efa05b2a63ee510345180c04d211d5068c8bc82c050392dac3d02cfac2"
5451
5451
  },
5452
5452
  {
5453
5453
  "path": "modules/lifecycle/db-goals.js",
@@ -5737,12 +5737,12 @@
5737
5737
  {
5738
5738
  "path": "modules/lifecycle/routes/task-write-routes.js",
5739
5739
  "mode": "0000644",
5740
- "sha256": "b821b384c67c5182f52653a283674da62756885c4fa5cf6790cea5eccd688f49"
5740
+ "sha256": "9cff3bcd27467dc413aa975cd20262b47e8595a50a845d93b557a24828719bbf"
5741
5741
  },
5742
5742
  {
5743
5743
  "path": "modules/lifecycle/routes/tasks.js",
5744
5744
  "mode": "0000644",
5745
- "sha256": "3c4bbd0b62fbef12e8511bfcbaae22fa677fc956b83423a8f213207fc1062c90"
5745
+ "sha256": "0497ab99a5c13399617b43f70d775e77773f1eed259548433f19c20c708ebab9"
5746
5746
  },
5747
5747
  {
5748
5748
  "path": "modules/lifecycle/routes/version-route-authz.js",
@@ -7622,12 +7622,12 @@
7622
7622
  {
7623
7623
  "path": "package-lock.json",
7624
7624
  "mode": "0000644",
7625
- "sha256": "a08e66b825d352b3b0c128f3b5b71d4665183bf734d69d0e837adf8946a28f19"
7625
+ "sha256": "c99209b9bfd8f2191eb0e622e45d4875760a65c7ae4f8653758667e11bd0bb66"
7626
7626
  },
7627
7627
  {
7628
7628
  "path": "package.json",
7629
7629
  "mode": "0000644",
7630
- "sha256": "9f9dfe9f5f8cc3d3ebc1cd46909eb73ac9ef03046933478f391cb6c23680cfa6"
7630
+ "sha256": "fae2586d07f9b98f2631be427db59eb76c7149b49a01c404af51a40edde911a3"
7631
7631
  },
7632
7632
  {
7633
7633
  "path": "public-docs/index.html",
@@ -8702,7 +8702,7 @@
8702
8702
  {
8703
8703
  "path": "scripts/gds/seed-bongos-v1-idea-routing-tasks.js",
8704
8704
  "mode": "0000644",
8705
- "sha256": "d028e80a7c1cd85805954187e43ec32d624244bf9b3374b4f27c5d47ced79f95"
8705
+ "sha256": "fc30acb5a295289c446b5fd7dd79bcd4e2ba8a3b90bbe1c426fecc087bbefadd"
8706
8706
  },
8707
8707
  {
8708
8708
  "path": "scripts/gds/seed-bongos-v1-seo-tasks.js",
@@ -8727,7 +8727,7 @@
8727
8727
  {
8728
8728
  "path": "scripts/gds/seed-bv1-profiles-tasks.js",
8729
8729
  "mode": "0000644",
8730
- "sha256": "a1101fcbb8988a19bd34f5c80789b8d69e421907be4c6143f6052e594c0b6944"
8730
+ "sha256": "53353029323de1e200b11bceccf822f524a19ff857cf17f6fa4b1ada73408c40"
8731
8731
  },
8732
8732
  {
8733
8733
  "path": "scripts/gds/seed-bv1-r01-delta-tasks.js",
@@ -9492,7 +9492,7 @@
9492
9492
  {
9493
9493
  "path": "src/module-api.js",
9494
9494
  "mode": "0000644",
9495
- "sha256": "0eb65d596ccf4f9af9cb2ec8769e373c18f3148b93e785157895d4dbb94299c7"
9495
+ "sha256": "37250a4442445fd5b73d199d17c2646944e633f7816a366ef7f6477168c6c2bd"
9496
9496
  },
9497
9497
  {
9498
9498
  "path": "src/module-loader/catalog.js",
@@ -10342,7 +10342,7 @@
10342
10342
  {
10343
10343
  "path": "tests/criterion_ref_parse.mjs",
10344
10344
  "mode": "0000644",
10345
- "sha256": "8eca863d641ce68a3edad2b902ecc15e9ec64c14fb420418a463461f29e44ff1"
10345
+ "sha256": "80ce711abf62a78dc4f36b513b852a5396173a2ff50e5deab2460531d17911e4"
10346
10346
  },
10347
10347
  {
10348
10348
  "path": "tests/criterion_suggest.mjs",
@@ -172,7 +172,7 @@ Don't tunnel-vision. Surface lateral options the user might not have thought of:
172
172
  - **Criteria (done-when)** — the goal's criteria verbatim by slug (C1, C2, …); every task links to one.
173
173
  - **Design decisions & assumptions** — what we're committing to and why; note grounding from the codebase (existing modules/tables/routes to reuse). Starts from your best defaults; the Phase-4 interview fills and revises this.
174
174
  - **Open questions (for the interview)** — the non-obvious decisions still unresolved, each with why-it-matters + the options you see. This is the raw material for Phase 4; empty it as answers land.
175
- - **Task outline** — grouped by criterion (and phase, if phased), one row per task: `R## · kind · title · one-line done-when · deps:[R##] · criterion_ids:[Cn]`, tagged `[E]`/`[I]`/`[N]`. Mark **spikes** (needs-exploration) and **proofs** (the live demo per criterion).
175
+ - **Task outline** — grouped by criterion (and phase, if phased), one row per task: `R## · kind · title · one-line done-when · deps:[R##] · criterion_ids:[<criterion-slug>]`, tagged `[E]`/`[I]`/`[N]`. Mark **spikes** (needs-exploration) and **proofs** (the live demo per criterion).
176
176
  - **Dependency graph & ranking** — filled in Phase 4 (topo order + priority tiers).
177
177
  - **Out of scope** — what this goal explicitly does NOT do, so the interview and tasks stay bounded.
178
178
 
@@ -269,10 +269,10 @@ Once the spec is finalized (Phase 4), **read `docs/specs/<ver>-<goal-slug>.md` a
269
269
  - **Migration** at `migrations/<NN>_<ver_slug>_planning_seed.sql` (CORE — platform-universal, non-scope-truth changes only): `UPDATE versions SET done_when = ...` and `UPDATE tasks` for existing-task priority + V##.R## title prefix (guarded by `WHERE title NOT LIKE 'V<ver>.R%'`). **Do NOT put versions/goals/`done_when_criteria` INSERTs here** — that's scope-truth and fails the `selfhost-boot` gate (ADR 0105).
270
270
 
271
271
  3. **Write a seed script** at `scripts/gds/seed-<ver_slug>-tasks.js` modeled on `scripts/gds/seed-v3-tasks.js` containing:
272
- - The TASKS array — one entry per net-new task with full description, kind, discipline, priority, est_minutes, manual_degree, credits_reward, **optional** `touches` (see below), optional `promoted_idea_id`, `depends_on_refs: ['R##', ...]` (the dep edges captured during brainstorm), AND **`criterion_ids: ['Cn', ...]`** — the done-when criterion(s) this task gates. The brainstorm in Phase 3 is already grouped by criterion, so this is just transcribing that grouping. Use the positional `"Cn"` token (C1 = the version's first criterion); the server resolves it against the task's version at create (ADR 0025 / task 438). **This replaces the old `**Criterion:** Cn` prose** — don't write that line anymore; pass `criterion_ids` instead so the task is linked in `task_criteria` at creation and `/status` counts it with no backfill.
272
+ - The TASKS array — one entry per net-new task with full description, kind, discipline, priority, est_minutes, manual_degree, credits_reward, **optional** `touches` (see below), optional `promoted_idea_id`, `depends_on_refs: ['R##', ...]` (the dep edges captured during brainstorm), AND **`criterion_ids: ['Cn', ...]`** — the done-when criterion(s) this task gates. The brainstorm in Phase 3 is already grouped by criterion, so this is just transcribing that grouping. **Use the criterion_id SLUG, not a positional `"Cn"` token.** `Cn` ranks over the whole VERSION's criteria, so on a version holding more than one goal it resolves to some other goal's criterion — which is how all 15 tasks of goal 1000071 were mislinked while the seed printed success (learning 1000188). Since task 1003108 the server **refuses** a positional ref on a multi-goal version (`400 ambiguous_criterion_ref`) rather than guessing, so a slug is the only ref that works there. The server resolves it against the task's version at create (ADR 0025 / task 438). **This replaces the old `**Criterion:** Cn` prose** — don't write that line anymore; pass `criterion_ids` instead so the task is linked in `task_criteria` at creation and `/status` counts it with no backfill.
273
273
  - **`touches` is now OPTIONAL (task 882 / [ADR 0049](../../../docs/adr/0049-split-parallel-safety-contract.md)).** Omit it (or leave it blank) unless you already know a tight file set and want the advisory overlap signal during the brainstorm. At first `/builder-ship` the server backfills `touches[]` from the real committed diff — the declared set becomes what git actually reported. Don't burn planning effort predicting file lists; a rough or absent prediction is fine and no longer ship-blocking.
274
274
  - **`requires_rank` AUTO-DERIVES — you don't hand-set it per task (task [#1498](https://example.com/builders#/task/1498) / [ADR 0084](../../../docs/adr/0084-auto-derived-per-task-rank-floor.md)).** On create the server floors a task at `metic` when it is `security_sensitive` or its `touches` reach the permission / authz / ship-grade / deploy / migration core (the same protected paths a sub-Metic builder can't push to); everything else stays the open `xenos` default. To deliberately gate a task **higher** than the auto-floor (e.g. owner-only work), pass `requires_rank: 'metic' | 'archon'` on the task — it can only RAISE the floor, never drop below it.
275
- - **Pass `goal_id: <gid>` on each task** so it lands under its goal (not the version's default catch-all). `POST /tasks` now honors `goal_id` (validated to belong to the version); to move an already-created task, `PATCH /tasks/:id` with `{goal_id}` (task 1763). `criterion_ids` accepts a **slug** (the `criterion_id` you set above), a `"Cn"` position, or a numeric id — the slug is position-independent and cleanest.
275
+ - **Pass `goal_id: <gid>` on each task** so it lands under its goal (not the version's default catch-all). `POST /tasks` now honors `goal_id` (validated to belong to the version); to move an already-created task, `PATCH /tasks/:id` with `{goal_id}` (task 1763). `criterion_ids` accepts a **slug** (the `criterion_id` you set above), a numeric id, or a `"Cn"` position — **but `Cn` is rejected outright once the version holds a second goal**, and a version almost always gains one. Treat the slug as the only form you write.
276
276
  - A main() that POSTs each task **with its `criterion_ids`** (idempotent on `source_ref`; the criterion links are inserted at create, idempotent on `(task_id, criterion_id)`), PATCHes `idea_inbox` for promotions, and after all tasks exist, POSTs `task_dependencies` via `POST /tasks/:id/dependencies` (resolving each R## to the just-created task ID). Idempotent on `(task_id, depends_on_task_id)`. (Need to re-attribute an already-seeded task? `POST /tasks/:id/criteria` with `{criterion_ids:['Cn',...]}`; `DELETE /tasks/:id/criteria/:criterionId` to detach — both Metic+ ([ADR 0090](../../../docs/adr/0090-metic-task-authoring.md)), mirroring the dependency endpoints.)
277
277
 
278
278
  > **Seeding contract — validated on goal 1000045 (2026-07-16):** proof-tasks use `kind='verify'` (a deliverable that IS a verification; can't be prematurely confirmed), spikes `kind='spike'`; `POST /tasks` accepts `criterion_ids` + `goal_id` inline and disciplines include `ui`; there is **no `promoted_idea_id`** field on `POST /tasks` — mark a promoted idea via `PATCH /inbox/:id`; **descoping a criterion has no API** (done-when is create/satisfy/unsatisfy only) — remove the row via a direct DB delete (`ssh … "sudo -u postgres psql -d <db> -c \"DELETE FROM done_when_criteria WHERE id=…\""`) under the meta-task claim, capturing the SQL in its notes; and a seed script **must throttle + honor `429` `retry_after`** (per-IP rate limit) — add a base delay between calls and back off on 429, or a large seed dies halfway. A completed reference seed: `scripts/gds/<redacted>.js`.
@@ -2046,5 +2046,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2046
2046
  landed since 1.19.784 with no explicit bump. run 35156349018. (task 1002620)
2047
2047
  1.19.786 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2048
2048
  landed since 1.19.785 with no explicit bump. run 35157132070. (task 1002620)
2049
+ 1.19.787 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2050
+ landed since 1.19.786 with no explicit bump. run 35158163962. (task 1002620)
2049
2051
  ---------------------------------------------------------------------------
2050
2052
  ```
@@ -286,13 +286,26 @@ function parseCriterionRef(raw) {
286
286
  }
287
287
 
288
288
  // Resolve an array of refs to done_when_criteria.id values within a version.
289
- // Returns { ids: [unique numeric ids, first-appearance order], unresolved: [refs] }.
290
-
291
- async function resolveCriterionIds(versionId, refs) {
289
+ // Returns { ids: [unique numeric ids, first-appearance order], unresolved: [refs],
290
+ // ambiguous: [positional refs refused because the version holds >1 goal] }.
291
+ //
292
+ // WHY ambiguous exists (task 1003108). A positional "Cn" ranks over the WHOLE
293
+ // version's criteria. That is unambiguous only while the version holds ONE goal:
294
+ // add a second and the "C1" a planner reads off a per-goal spec silently resolves
295
+ // to some other goal's criterion — a wrong link, not an error. It mislinked all 15
296
+ // tasks of goal 1000071 (2026-08-17, learning 1000188) and the seed still printed
297
+ // success. Task 1003076 added the after-the-fact tally that catches the symptom;
298
+ // this refuses the ref at the write, so the mislink cannot be created. Slugs are
299
+ // position-independent and stay legal on every version, which is why the skill now
300
+ // requires them. `executor` is the same testability seam countTasksWithoutCriterionLink
301
+ // uses — a fake SQL-recording client needs no live Postgres.
302
+
303
+ async function resolveCriterionIds(versionId, refs, executor = pool) {
292
304
  const ids = [];
293
305
  const unresolved = [];
294
- if (!Array.isArray(refs) || refs.length === 0) return { ids, unresolved };
295
- const { rows } = await pool.query(
306
+ const ambiguous = [];
307
+ if (!Array.isArray(refs) || refs.length === 0) return { ids, unresolved, ambiguous };
308
+ const { rows } = await executor.query(
296
309
  `SELECT id, criterion_id,
297
310
  ROW_NUMBER() OVER (ORDER BY sort_order, criterion_id) AS cnum
298
311
  FROM done_when_criteria
@@ -302,17 +315,31 @@ async function resolveCriterionIds(versionId, refs) {
302
315
  const byId = new Set(rows.map((r) => Number(r.id)));
303
316
  const byCnum = new Map(rows.map((r) => [Number(r.cnum), Number(r.id)]));
304
317
  const bySlug = new Map(rows.map((r) => [String(r.criterion_id), Number(r.id)]));
318
+ // Only pay for the goal count when a positional ref is actually present — the
319
+ // slug path (the one the skill now requires) costs no extra query.
320
+ let multiGoal = false;
321
+ if (refs.some((r) => parseCriterionRef(r).kind === 'cnum')) {
322
+ const { rows: g } = await executor.query(
323
+ 'SELECT COUNT(*)::int AS n FROM goals WHERE version_id = $1',
324
+ [versionId]
325
+ );
326
+ multiGoal = Number(g[0] ? g[0].n : 0) > 1;
327
+ }
305
328
  const seen = new Set();
306
329
  for (const raw of refs) {
307
330
  const p = parseCriterionRef(raw);
308
331
  let id = null;
309
- if (p.kind === 'cnum') id = byCnum.get(p.n) ?? null;
310
- else if (p.kind === 'id') id = byId.has(p.id) ? p.id : null;
332
+ if (p.kind === 'cnum') {
333
+ // Refuse rather than guess: resolving it would produce a plausible-looking
334
+ // wrong link that no later check attributes to the ref.
335
+ if (multiGoal) { ambiguous.push(String(raw).trim()); continue; }
336
+ id = byCnum.get(p.n) ?? null;
337
+ } else if (p.kind === 'id') id = byId.has(p.id) ? p.id : null;
311
338
  else id = bySlug.get(p.slug) ?? null;
312
339
  if (id == null) { unresolved.push(String(raw).trim()); continue; }
313
340
  if (!seen.has(id)) { seen.add(id); ids.push(id); }
314
341
  }
315
- return { ids, unresolved };
342
+ return { ids, unresolved, ambiguous };
316
343
  }
317
344
 
318
345
  // countTasksWithoutCriterionLink — task 1003157's DONE WHEN item 2: the count of
@@ -62,8 +62,9 @@ module.exports = function registerTaskWriteRoutes(router) {
62
62
  requires_rank: { type: 'string', maxLength: 16 }, // task 1498 / ADR 0084 — per-task rank floor (auto-derived; explicit value can only raise it)
63
63
  auto_detect_touches: { type: 'boolean' },
64
64
  // task 438: link the new task to done-when criteria at creation time. Each
65
- // element is a numeric done_when_criteria.id, a positional "Cn" token, or a
66
- // criterion_id slug — resolved against this task's version below (ADR 0025).
65
+ // element is a criterion_id slug (required on a multi-goal version — task
66
+ // 1003108), a numeric done_when_criteria.id, or a positional "Cn" token —
67
+ // resolved against this task's version below (ADR 0025).
67
68
  criterion_ids: { type: 'array', maxItems: 100 },
68
69
  // BV1.R53 (task 1422): module affinity label. Drives module-disjointness at
69
70
  // claim time and diff⊆module drift detection at ship time.
@@ -119,7 +120,10 @@ module.exports = function registerTaskWriteRoutes(router) {
119
120
  // is unambiguous and a numeric id from another version doesn't resolve.
120
121
  let criterionIdsResolved = [];
121
122
  if (Array.isArray(body.criterion_ids) && body.criterion_ids.length > 0) {
122
- const { ids, unresolved } = await db.resolveCriterionIds(body.version_id, body.criterion_ids);
123
+ const { ids, unresolved, ambiguous } = await db.resolveCriterionIds(body.version_id, body.criterion_ids);
124
+ if (ambiguous.length > 0) {
125
+ return res.fail('ambiguous_criterion_ref', { status: 400, message: `Version '${body.version_id}' holds more than one goal, so a positional "Cn" ref ranks over the WHOLE version's criteria — not this goal's — and would link to the wrong criterion silently (task 1003108). Re-send these as criterion_id slugs: ${ambiguous.join(', ')}.`, details: { ambiguous } });
126
+ }
123
127
  if (unresolved.length > 0) {
124
128
  return res.fail('bad_criterion_ref', { status: 400, message: `These criterion refs don't match any criterion of version '${body.version_id}': ` +
125
129
  `${unresolved.join(', ')}. Use a numeric done_when_criteria.id, a "Cn" position, or a criterion_id slug.`, details: { unresolved } });
@@ -452,7 +452,10 @@ module.exports = function buildTasksRouter() {
452
452
 
453
453
  const task = await db.getTask(id);
454
454
  if (!task) return res.fail('task_not_found', 404);
455
- const { ids, unresolved } = await db.resolveCriterionIds(task.version_id, refs);
455
+ const { ids, unresolved, ambiguous } = await db.resolveCriterionIds(task.version_id, refs);
456
+ if (ambiguous.length > 0) {
457
+ return res.fail('ambiguous_criterion_ref', { status: 400, message: `Version '${task.version_id}' holds more than one goal, so a positional "Cn" ref ranks over the WHOLE version's criteria — not this goal's — and would link to the wrong criterion silently (task 1003108). Re-send these as criterion_id slugs: ${ambiguous.join(', ')}.`, details: { ambiguous } });
458
+ }
456
459
  if (unresolved.length > 0) {
457
460
  return res.fail('bad_criterion_ref', { status: 400, message: `These criterion refs don't match any criterion of version '${task.version_id}': ${unresolved.join(', ')}.`, details: { unresolved } });
458
461
  }
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.786",
3
+ "version": "1.19.787",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.786",
9
+ "version": "1.19.787",
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.786",
3
+ "version": "1.19.787",
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",
@@ -1,3 +1,10 @@
1
+ // DO NOT COPY THE `crit:` TOKENS FROM THIS FILE (task 1003108) — copy its
2
+ // verifySeedCriteriaLinks tail instead, which is what this script is cited for.
3
+ // Its positional "Cn" refs rank over the whole VERSION's criteria, so on a
4
+ // multi-goal version they resolve to another goal's criterion silently (learning
5
+ // 1000188). POST /tasks now refuses a positional ref there (400
6
+ // ambiguous_criterion_ref). A new seed uses criterion_id SLUGS.
7
+ //
1
8
  // Seed the tasks for goal 1000071 "Idea routing — work that has a home lands in it" (BONGOS-V1).
2
9
  // Source of truth: docs/specs/bongos-v1-idea-routing.md (status: final).
3
10
  // Idempotent on (source, source_ref). Re-running resolves to the existing rows.
@@ -1,3 +1,12 @@
1
+ // DO NOT COPY THE `crit:` TOKENS FROM THIS FILE (task 1003108). Its positional
2
+ // "Cn" refs were written when BONGOS-V1 held one goal, where they were unambiguous.
3
+ // Cn ranks over the whole VERSION's criteria, so on a multi-goal version it resolves
4
+ // to another goal's criterion silently — that is how all 15 tasks of goal 1000071 were
5
+ // mislinked (learning 1000188). POST /tasks now refuses a positional ref on a
6
+ // multi-goal version (400 ambiguous_criterion_ref). A new seed uses criterion_id
7
+ // SLUGS. This file is kept as-is because it has already run and is idempotent on
8
+ // source_ref — a re-run returns the existing rows before criterion resolution.
9
+ //
1
10
  // Seed the tasks for goal 1000045 "Public builder profiles" (BONGOS-V1, internal).
2
11
  // Source of truth: docs/specs/bongos-v1-public-builder-profiles.md (status: final).
3
12
  // Idempotent on (source, source_ref). Re-running resolves to the existing rows.
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.786'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.787'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -6,8 +6,10 @@
6
6
  // - a numeric done_when id → { kind: 'id', id }
7
7
  // - a criterion_id slug → { kind: 'slug', slug }
8
8
  // resolveCriterionIds itself is DB-backed (it ranks a version's criteria), so the
9
- // parsing rule — the part that decides what a token MEANS — is what's unit-tested
10
- // here without a database. Run: node tests/criterion_ref_parse.mjs
9
+ // parsing rule — the part that decides what a token MEANS — is unit-tested here
10
+ // without a database. Task 1003108 added a second block covering resolveCriterionIds
11
+ // itself through its executor seam: a positional "Cn" must be REFUSED once the
12
+ // version holds more than one goal. Run: node tests/criterion_ref_parse.mjs
11
13
 
12
14
  import { strict as assert } from 'node:assert';
13
15
  import { createRequire } from 'node:module';
@@ -49,5 +51,82 @@ test('a "Cn" embedded in a longer string does NOT match (anchored)', () => {
49
51
  assert.equal(db.parseCriterionRef('see C8 above').kind, 'slug');
50
52
  });
51
53
 
52
- console.log(`\ncriterion ref parse (task 438) tests: ${passed} passed, ${failed} failed`);
53
- process.exit(failed > 0 ? 1 : 0);
54
+ // --- resolveCriterionIds: the multi-goal refusal (task 1003108) -------------
55
+ // resolveCriterionIds is DB-backed, but it takes an `executor` seam (the one
56
+ // countTasksWithoutCriterionLink uses), so a fake SQL-recording client covers the
57
+ // rule without Postgres. The rule: a positional "Cn" ranks over the WHOLE
58
+ // version's criteria, so once a version holds a second goal it silently resolves
59
+ // to another goal's criterion — the shape that mislinked all 15 tasks of goal
60
+ // 1000071. It must come back as `ambiguous`, never as an id.
61
+
62
+ function fakeExecutor(goalCount) {
63
+ const seen = [];
64
+ return {
65
+ seen,
66
+ async query(sql, params) {
67
+ seen.push(sql);
68
+ if (/FROM goals/.test(sql)) return { rows: [{ n: goalCount }] };
69
+ return { rows: [
70
+ { id: 501, criterion_id: 'first-slug', cnum: 1 },
71
+ { id: 502, criterion_id: 'second-slug', cnum: 2 },
72
+ ] };
73
+ },
74
+ };
75
+ }
76
+
77
+ async function atest(name, fn) {
78
+ try { await fn(); passed++; console.log(` ok ${name}`); }
79
+ catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
80
+ }
81
+
82
+ async function main() {
83
+ await atest('a positional "Cn" resolves on a SINGLE-goal version', async () => {
84
+ const r = await db.resolveCriterionIds('V9', ['C2'], fakeExecutor(1));
85
+ assert.deepEqual(r.ids, [502]);
86
+ assert.deepEqual(r.ambiguous, []);
87
+ assert.deepEqual(r.unresolved, []);
88
+ });
89
+
90
+ await atest('a positional "Cn" is REFUSED on a multi-goal version', async () => {
91
+ const r = await db.resolveCriterionIds('V9', ['C2'], fakeExecutor(2));
92
+ assert.deepEqual(r.ids, [], 'must not resolve to an id — refusing is the whole point');
93
+ assert.deepEqual(r.ambiguous, ['C2']);
94
+ assert.deepEqual(r.unresolved, [], 'ambiguous is not the same failure as unknown');
95
+ });
96
+
97
+ await atest('slugs still resolve on a multi-goal version', async () => {
98
+ const r = await db.resolveCriterionIds('V9', ['second-slug'], fakeExecutor(2));
99
+ assert.deepEqual(r.ids, [502]);
100
+ assert.deepEqual(r.ambiguous, []);
101
+ });
102
+
103
+ await atest('a mixed list refuses only the positional ref', async () => {
104
+ const r = await db.resolveCriterionIds('V9', ['first-slug', 'C2', '502'], fakeExecutor(2));
105
+ assert.deepEqual(r.ids, [501, 502], 'the slug and the numeric id survive');
106
+ assert.deepEqual(r.ambiguous, ['C2']);
107
+ });
108
+
109
+ await atest('an unknown slug is unresolved, not ambiguous', async () => {
110
+ const r = await db.resolveCriterionIds('V9', ['no-such-slug'], fakeExecutor(2));
111
+ assert.deepEqual(r.unresolved, ['no-such-slug']);
112
+ assert.deepEqual(r.ambiguous, []);
113
+ });
114
+
115
+ await atest('the goal count is NOT queried when no positional ref is present', async () => {
116
+ // The slug path is the one the skill now requires — it must not pay for an
117
+ // extra round trip. A green test here is what keeps the fast path fast.
118
+ const ex = fakeExecutor(2);
119
+ await db.resolveCriterionIds('V9', ['first-slug'], ex);
120
+ assert.equal(ex.seen.filter((s) => /FROM goals/.test(s)).length, 0);
121
+ });
122
+
123
+ await atest('an empty ref list returns all three arrays', async () => {
124
+ const r = await db.resolveCriterionIds('V9', [], fakeExecutor(2));
125
+ assert.deepEqual(r, { ids: [], unresolved: [], ambiguous: [] });
126
+ });
127
+
128
+ console.log(`\ncriterion ref parse (task 438) + multi-goal refusal (task 1003108) tests: ${passed} passed, ${failed} failed`);
129
+ process.exit(failed > 0 ? 1 : 0);
130
+ }
131
+
132
+ main();