@bongos/core 1.19.551 → 1.19.552

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,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.551",
6
- "core_contract": "1.19.551",
7
- "source_commit": "de0b6c99753fd502f2356bcfb6bf338227445cc7",
5
+ "core_version": "1.19.552",
6
+ "core_contract": "1.19.552",
7
+ "source_commit": "c33c4fe6dd1cf9319051a7bee1e9c940472549a5",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-04T20:39:18.276Z",
9
+ "built_at": "2026-09-06T15:55:11.203Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 442,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 2018,
14
+ "functional_verbatim": 2019,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2484,
20
- "tree_sha256": "38fea0008aa898d10d6e7388c30a7d422be81dcde8d201172848e551d72d361b",
19
+ "file_count": 2485,
20
+ "tree_sha256": "c628eebd2a60fa4e7346604f1f5507a913cfac851f45fc632f3eff4aedf36487",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -2652,7 +2652,7 @@
2652
2652
  {
2653
2653
  "path": "docs/module-api-changelog.md",
2654
2654
  "mode": "0000644",
2655
- "sha256": "1389ff20919a19aef2fbff28102f596f0ca168c34bc4c4114cf557bbf52627a5"
2655
+ "sha256": "2fb63f0c4427c7f7bff711fcf41f5637cca7b31a8009a21305530748b130e310"
2656
2656
  },
2657
2657
  {
2658
2658
  "path": "docs/modules-contract.md",
@@ -5427,7 +5427,7 @@
5427
5427
  {
5428
5428
  "path": "modules/lifecycle/db-goals.js",
5429
5429
  "mode": "0000644",
5430
- "sha256": "3ad7f78cbad987307d2203facbc91cec879565f398f9893a0218bbf29fc061bb"
5430
+ "sha256": "713b7165b1dbbfe43a77db5678ea98ebb03e17209d7b7bee5dbd1fc416be5cc2"
5431
5431
  },
5432
5432
  {
5433
5433
  "path": "modules/lifecycle/db-grade.js",
@@ -5477,7 +5477,7 @@
5477
5477
  {
5478
5478
  "path": "modules/lifecycle/done-when.js",
5479
5479
  "mode": "0000644",
5480
- "sha256": "fab91acf54bea4fca254bfd420a9a934e6eae434101ff57ae2a1bed48d2342d5"
5480
+ "sha256": "4e276151342f0eab3d4c0cea1ed55db8716a1d3d4b40568966be495f4892c1f4"
5481
5481
  },
5482
5482
  {
5483
5483
  "path": "modules/lifecycle/est-advisory.js",
@@ -5517,7 +5517,7 @@
5517
5517
  {
5518
5518
  "path": "modules/lifecycle/goal-rollup.js",
5519
5519
  "mode": "0000644",
5520
- "sha256": "cd3c3c28e9e1361b1f02a8de7a973d0d4d87b916477af98b9032de87dfbc588d"
5520
+ "sha256": "1debae561ef745ba22440ea2b32f27dd567e9e7ed56f8a5509e90fc1ddecd10e"
5521
5521
  },
5522
5522
  {
5523
5523
  "path": "modules/lifecycle/goal-suggest.js",
@@ -5734,6 +5734,11 @@
5734
5734
  "mode": "0000644",
5735
5735
  "sha256": "7ed98bbd3d4954808dc64228acb6899a96083081ecc241e81f2b5db9eb568f1f"
5736
5736
  },
5737
+ {
5738
+ "path": "modules/lifecycle/task-status.js",
5739
+ "mode": "0000644",
5740
+ "sha256": "28db8b28430bbb1bc901b300322ce42ed5e9254e0b934941d3647b83c92b2083"
5741
+ },
5737
5742
  {
5738
5743
  "path": "modules/lifecycle/task-visual-db.js",
5739
5744
  "mode": "0000644",
@@ -5752,7 +5757,7 @@
5752
5757
  {
5753
5758
  "path": "modules/lifecycle/version-close.js",
5754
5759
  "mode": "0000644",
5755
- "sha256": "d71d0c0583cfa873001c0e359d6ee6e0e0a26d846121064c25ed36a1d27054ae"
5760
+ "sha256": "a977d807adb1e73544d1c02f6231005c2dbbb1a491316a8160080fd279e1b7a0"
5756
5761
  },
5757
5762
  {
5758
5763
  "path": "modules/memory/CLAUDE.md",
@@ -7467,12 +7472,12 @@
7467
7472
  {
7468
7473
  "path": "package-lock.json",
7469
7474
  "mode": "0000644",
7470
- "sha256": "c71d59c13b87d9ffbd6c792eb56b26eb434704e855aed3afdfc58914dba02e29"
7475
+ "sha256": "cc08dead6d4714851b7db549d9b6ad9829bbd0e22722fab29cef54f9b28f7693"
7471
7476
  },
7472
7477
  {
7473
7478
  "path": "package.json",
7474
7479
  "mode": "0000644",
7475
- "sha256": "6a2c55e98a36d257177254a100569a14bec9b3720931caf9ef265971b81ba319"
7480
+ "sha256": "36e67dc71f4fced7aa31d42102c8ff9c424ded06dea1a6f2f67d45888e95e406"
7476
7481
  },
7477
7482
  {
7478
7483
  "path": "public-docs/index.html",
@@ -8497,7 +8502,7 @@
8497
8502
  {
8498
8503
  "path": "scripts/gds/sequence.js",
8499
8504
  "mode": "0000644",
8500
- "sha256": "52c43371c2e6e8f7ffeac588143167ffc43e0010bceb7137ebce4ed61eaa64e6"
8505
+ "sha256": "08b2165b899aa71983826789613d7eadd1bdded7f4ef12b05d7c1a60bb79337e"
8501
8506
  },
8502
8507
  {
8503
8508
  "path": "scripts/gds/session-cost.js",
@@ -9167,7 +9172,7 @@
9167
9172
  {
9168
9173
  "path": "src/module-api.js",
9169
9174
  "mode": "0000644",
9170
- "sha256": "95e7b476152291d84e955bfd585e62c48c48d85dcab0ca8d73c4519af2997e8b"
9175
+ "sha256": "bd96dabd37302c58d3519dde872d4a77f20302b5da263dc829db0fda37654edb"
9171
9176
  },
9172
9177
  {
9173
9178
  "path": "src/module-loader/catalog.js",
@@ -9427,7 +9432,7 @@
9427
9432
  {
9428
9433
  "path": "tests/auto_satisfy_criteria.mjs",
9429
9434
  "mode": "0000644",
9430
- "sha256": "135d0af6b1535e50d68ed62baa36b616ba28888e899940cabf2f2940f3cd6f9d"
9435
+ "sha256": "548e6a477660bb5e5cb260a0d5821bc3e11201225e64cfa45611c2fae07c80b2"
9431
9436
  },
9432
9437
  {
9433
9438
  "path": "tests/autonomous_fit_protected_paths.mjs",
@@ -10302,7 +10307,7 @@
10302
10307
  {
10303
10308
  "path": "tests/goal_sort_staleness.mjs",
10304
10309
  "mode": "0000644",
10305
- "sha256": "1c34a429a1e96c409e8f6e2199d79a0f75b95f25a2cd5f1a86b6f2e254d6ac40"
10310
+ "sha256": "cf399c4c8ea36f96bbdb1f68e36a71eb3d0f4965c3e88278c9387d92896e7db4"
10306
10311
  },
10307
10312
  {
10308
10313
  "path": "tests/goal_subtitle.mjs",
@@ -1551,5 +1551,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1551
1551
  landed since 1.19.549 with no explicit bump. run 33907331050. (task 1002620)
1552
1552
  1.19.551 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1553
1553
  landed since 1.19.550 with no explicit bump. run 33917261720. (task 1002620)
1554
+ 1.19.552 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1555
+ landed since 1.19.551 with no explicit bump. run 34043771286. (task 1002620)
1554
1556
  ---------------------------------------------------------------------------
1555
1557
  ```
@@ -14,6 +14,7 @@ const api = require('../../src/module-api');
14
14
  const { pool } = api;
15
15
  const doneWhen = require('./done-when.js');
16
16
  const { deriveRequiredRank, highestRank } = require('./db-rank-authz.js');
17
+ const { nonTerminalSql } = require('./task-status');
17
18
 
18
19
  // -------------------------------------------------------------------------
19
20
  // Goals (ADR 0086 §2 / BV1.R56 — task 1510)
@@ -328,7 +329,7 @@ async function listCriteriaForGoal(goalId, deps = {}) {
328
329
  -- task 1002651 (migration 017's family): abandoned is a terminal
329
330
  -- state (deliberately cut), not pending work — it must not hold a
330
331
  -- criterion's "met — pending review" flip hostage forever.
331
- count(*) FILTER (WHERE t.status NOT IN ('shipped', 'abandoned'))::int AS unshipped_tasks
332
+ count(*) FILTER (WHERE ${nonTerminalSql()})::int AS unshipped_tasks
332
333
  FROM task_criteria tc
333
334
  JOIN tasks t ON t.id = tc.task_id
334
335
  WHERE tc.criterion_id IN (SELECT id FROM crit)
@@ -24,6 +24,7 @@
24
24
  // POST /api/gds/done-when/:criterionId/unsatisfy — auth
25
25
 
26
26
  const { pool } = require('../../src/module-api');
27
+ const { nonTerminalSql } = require('./task-status');
27
28
 
28
29
  // isPendingReview — the ONE derivation of the "met — pending review" auto-flag
29
30
  // (ADR 0086 §6 / BV1.R63): a criterion with >=1 linked task, every one shipped,
@@ -134,7 +135,7 @@ async function listPendingReviewCriteria({ versionId = null } = {}, deps = {}) {
134
135
  JOIN tasks t ON t.id = tc.task_id
135
136
  WHERE c.satisfied = false
136
137
  GROUP BY c.id
137
- HAVING count(*) FILTER (WHERE t.status NOT IN ('shipped', 'abandoned')) = 0
138
+ HAVING count(*) FILTER (WHERE ${nonTerminalSql()}) = 0
138
139
  ),
139
140
  goal_unsat AS (
140
141
  SELECT goal_id, count(*)::int AS unsatisfied
@@ -426,7 +427,7 @@ async function autoSatisfyShippedCriteria(exec, { taskId = null } = {}) {
426
427
  AND ($1::bigint IS NULL OR c.id IN (
427
428
  SELECT criterion_id FROM task_criteria WHERE task_id = $1::bigint))
428
429
  GROUP BY c.id
429
- HAVING count(*) FILTER (WHERE t.status NOT IN ('shipped', 'abandoned')) = 0
430
+ HAVING count(*) FILTER (WHERE ${nonTerminalSql()}) = 0
430
431
  AND count(*) FILTER (WHERE t.status = 'shipped') >= 1
431
432
  )
432
433
  UPDATE done_when_criteria c
@@ -32,10 +32,12 @@
32
32
  const api = require('../../src/module-api');
33
33
  const { pool } = api;
34
34
 
35
- // The status vocabularies, kept beside the SQL that uses them so a reader can
36
- // check them against goals-lib.js in one glance.
37
- const DONE_STATUSES = ['shipped', 'confirmed', 'completed'];
38
- const CUT_STATUSES = ['abandoned', 'dropped'];
35
+ // The status vocabularies live in task-status.js (task 1003588) so this module
36
+ // has ONE spelling of each. These two are the PRESENTATIONAL buckets — they
37
+ // mirror goals-lib.js `readinessOf`, and they are NOT the terminal set: `done`
38
+ // here includes the mid-ship states on purpose, which is right for a progress
39
+ // ring and wrong for any question about whether a goal may close.
40
+ const { DONE_STATUSES, CUT_STATUSES } = require('./task-status');
39
41
 
40
42
  // goalRollups — one row per goal that has at least one task in `versionId`.
41
43
  // Returns [{ goal_id, done, active, ready, blocked, backlog, cut, total,
@@ -0,0 +1,72 @@
1
+ 'use strict';
2
+
3
+ // modules/lifecycle/task-status.js — the task-status vocabulary, declared once
4
+ // (task 1003588, goal 1000086, criterion sv-goal-outlives-nothing).
5
+ //
6
+ // WHY THIS FILE EXISTS. "Finished" had two spellings inside one module.
7
+ // `goal-rollup.js` counted shipped|confirmed|completed as done; `done-when.js`
8
+ // counted only shipped and treated abandoned as accounted-for; `db-goals.js`,
9
+ // `version-close.js` and the `sequence` CLI each re-typed the SQL literal
10
+ // `NOT IN ('shipped', 'abandoned')` by hand. Two definitions of finished in one
11
+ // module is precisely what lets a goal read closed on one surface and open on
12
+ // another — the failure goal 1000086 exists to make impossible. So the
13
+ // vocabulary is declared here, once, and every consumer reads it.
14
+ //
15
+ // THE TWO SETS ARE NOT THE SAME QUESTION, and conflating them is the bug in the
16
+ // other direction:
17
+ //
18
+ // TERMINAL_STATUSES — the LEDGER truth. A task is finished with the project
19
+ // when it shipped or was deliberately abandoned. Nothing else is terminal:
20
+ // `completed` and `confirmed` are mid-ship states that can still fail a
21
+ // grade or strand, so work sitting in them is unfinished work and must hold
22
+ // its goal open. This is the set the closure invariants read.
23
+ //
24
+ // DONE_STATUSES / CUT_STATUSES — a PRESENTATIONAL bucketing for the goals
25
+ // board, mirroring the client's `goals-lib.js readinessOf` verbatim (a
26
+ // divergence shows a person different numbers per surface, which
27
+ // tests/goal_sort_staleness.mjs pins). "Done" there means "no longer in the
28
+ // queue", which deliberately includes the mid-ship states. It is a progress
29
+ // ring, not a ledger, and must never be used to decide whether a goal may
30
+ // close.
31
+ //
32
+ // NO DEPENDENCIES, deliberately. This file requires nothing — not even
33
+ // `module-api` — so a CLI (`scripts/gds/sequence.js`) can read the vocabulary
34
+ // without dragging the server's `db.js` and its connection pool into a process
35
+ // that only needs a Set of two strings.
36
+
37
+ // TERMINAL_STATUSES — the only spelling of "this task is finished with the
38
+ // project". shipped = delivered; abandoned = deliberately cut (task 1002651,
39
+ // migration 017's family). Everything else holds work open.
40
+ const TERMINAL_STATUSES = ['shipped', 'abandoned'];
41
+
42
+ // TERMINAL_STATUS_SET — the same set for JS membership tests.
43
+ const TERMINAL_STATUS_SET = new Set(TERMINAL_STATUSES);
44
+
45
+ // isTerminalStatus — true when a task in this status is finished with the
46
+ // project and can no longer hold a criterion, a goal, or a version open.
47
+ function isTerminalStatus(status) {
48
+ return TERMINAL_STATUS_SET.has(status);
49
+ }
50
+
51
+ // nonTerminalSql — the SQL predicate for "this task is still unfinished",
52
+ // rendered from TERMINAL_STATUSES so the literal exists in exactly one place.
53
+ // `column` is the status expression in the caller's query (`t.status` under a
54
+ // join, bare `status` in a single-table read). Output is the byte-identical
55
+ // `<column> NOT IN ('shipped', 'abandoned')` every call site used to hardcode.
56
+ function nonTerminalSql(column = 't.status') {
57
+ return `${column} NOT IN (${TERMINAL_STATUSES.map((s) => `'${s}'`).join(', ')})`;
58
+ }
59
+
60
+ // DONE_STATUSES / CUT_STATUSES — the goals-board buckets. Presentational only;
61
+ // see the header. Mirrors goals-lib.js `readinessOf`.
62
+ const DONE_STATUSES = ['shipped', 'confirmed', 'completed'];
63
+ const CUT_STATUSES = ['abandoned', 'dropped'];
64
+
65
+ module.exports = {
66
+ TERMINAL_STATUSES,
67
+ TERMINAL_STATUS_SET,
68
+ isTerminalStatus,
69
+ nonTerminalSql,
70
+ DONE_STATUSES,
71
+ CUT_STATUSES,
72
+ };
@@ -11,6 +11,7 @@
11
11
  // (separate task #43).
12
12
 
13
13
  const { pool } = require('../../src/module-api');
14
+ const { nonTerminalSql } = require('./task-status');
14
15
 
15
16
  const PERCENT_GATE = 80;
16
17
 
@@ -54,7 +55,7 @@ async function getCarryOverCount(versionId) {
54
55
  `SELECT count(*) AS n
55
56
  FROM tasks
56
57
  WHERE version_id = $1
57
- AND status NOT IN ('shipped', 'abandoned')
58
+ AND ${nonTerminalSql('status')}
58
59
  AND title NOT LIKE '\\_\\_smoke\\_\\_%' ESCAPE '\\'`,
59
60
  [versionId]
60
61
  );
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.551",
3
+ "version": "1.19.552",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.551",
9
+ "version": "1.19.552",
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.551",
3
+ "version": "1.19.552",
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",
@@ -33,8 +33,10 @@ const RANK_TIER = { xenos: 1, thetes: 2, metic: 3, archon: 4 };
33
33
 
34
34
  // A task in one of these statuses is done as far as the chain is concerned —
35
35
  // 'completed'/'confirmed' are mid-ship states, so the chain must not re-claim
36
- // them, but they are also not yet 'shipped' so successors stay gated.
37
- const TERMINAL = new Set(['shipped', 'abandoned']);
36
+ // them, but they are also not yet 'shipped' so successors stay gated. The set
37
+ // itself comes from the lifecycle's one declaration (task 1003588); that file
38
+ // requires nothing, so reading it costs this CLI no server pool.
39
+ const { TERMINAL_STATUS_SET: TERMINAL } = require('../../modules/lifecycle/task-status');
38
40
  const IN_FLIGHT = new Set(['active', 'completed', 'confirmed']);
39
41
 
40
42
  const DEFAULT_LIMIT = 50;
package/src/module-api.js CHANGED
@@ -49,7 +49,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
49
49
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
50
50
  // the entry to that file. Look for a version's history there, not here.
51
51
  // ---------------------------------------------------------------------------
52
- const CORE_VERSION = '1.19.551'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
52
+ const CORE_VERSION = '1.19.552'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
53
53
 
54
54
  // A namespaced logger so a module's log lines are attributable + consistent.
55
55
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -61,7 +61,10 @@ const CRIT = { id: 10, version_id: 'BONGOS-V1', criterion_id: 'c-one', criterion
61
61
  await test('GUARD (sql): the update requires BOTH nothing-pending AND >=1 shipped', () => {
62
62
  const src = readFileSync(new URL('../modules/lifecycle/done-when.js', import.meta.url), 'utf8');
63
63
  const body = src.slice(src.indexOf('async function autoSatisfyShippedCriteria'));
64
- assert.match(body, /HAVING count\(\*\) FILTER \(WHERE t\.status NOT IN \('shipped', 'abandoned'\)\) = 0/,
64
+ // task 1003588: the literal moved to task-status.js nonTerminalSql(), which
65
+ // renders this exact predicate - so the guard is asserted here on the CALL,
66
+ // and below on what that one declaration actually produces.
67
+ assert.match(body, /HAVING count\(\*\) FILTER \(WHERE \$\{nonTerminalSql\(\)\}\) = 0/,
65
68
  'nothing may still be pending');
66
69
  assert.match(body, /AND count\(\*\) FILTER \(WHERE t\.status = 'shipped'\) >= 1/,
67
70
  'THE GUARD: an all-abandoned gating set delivered nothing and must not close a criterion');
@@ -72,7 +75,7 @@ await test('ASYMMETRY (sql): the review queue deliberately does NOT carry the de
72
75
  const start = src.indexOf('async function listPendingReviewCriteria');
73
76
  const body = src.slice(start, src.indexOf('async function criterionProgress', start));
74
77
  assert.ok(body.length > 200, 'the slice must actually contain the function');
75
- assert.match(body, /HAVING count\(\*\) FILTER \(WHERE t\.status NOT IN \('shipped', 'abandoned'\)\) = 0/);
78
+ assert.match(body, /HAVING count\(\*\) FILTER \(WHERE \$\{nonTerminalSql\(\)\}\) = 0/);
76
79
  assert.ok(!/status = 'shipped'\) >= 1/.test(body),
77
80
  'task 1002651 intends an all-abandoned criterion to SURFACE for review; only auto-close is gated');
78
81
  });
@@ -211,3 +214,22 @@ await test('closeCompletedWorkForShip: silent on the common no-op, and logs what
211
214
  });
212
215
 
213
216
  summary();
217
+
218
+ // ---- 4. the terminal vocabulary has exactly one spelling (task 1003588) ----
219
+
220
+ await test('VOCABULARY: nonTerminalSql renders the predicate every call site used to hardcode', async () => {
221
+ const mod = await import('../modules/lifecycle/task-status.js');
222
+ const { nonTerminalSql, TERMINAL_STATUSES } = mod.default || mod;
223
+ assert.deepEqual(TERMINAL_STATUSES, ['shipped', 'abandoned'],
224
+ 'shipped or deliberately abandoned - the mid-ship states are NOT terminal');
225
+ assert.equal(nonTerminalSql(), "t.status NOT IN ('shipped', 'abandoned')");
226
+ assert.equal(nonTerminalSql('status'), "status NOT IN ('shipped', 'abandoned')");
227
+ });
228
+
229
+ await test('VOCABULARY: no consumer re-types the terminal set behind the constant', () => {
230
+ for (const f of ['done-when.js', 'db-goals.js', 'version-close.js', 'goal-rollup.js']) {
231
+ const src = readFileSync(new URL('../modules/lifecycle/' + f, import.meta.url), 'utf8');
232
+ assert.ok(!/NOT IN \('shipped', 'abandoned'\)/.test(src),
233
+ f + ' must read the terminal set from task-status.js, not spell it again');
234
+ }
235
+ });
@@ -156,10 +156,16 @@ await test('the SQL rollup buckets MIRROR goals-lib readinessOf exactly', () =>
156
156
  // surface they opened, which is the opposite of what this goal is for.
157
157
  const rollup = read('modules/lifecycle/goal-rollup.js');
158
158
  const lib = GOALS_LIB_SRC;
159
- // done + cut vocabularies
160
- assert.match(rollup, /const DONE_STATUSES = \['shipped', 'confirmed', 'completed'\]/);
159
+ // done + cut vocabularies. task 1003588 moved the server side's declaration
160
+ // into task-status.js so the module has one spelling of each; the mirror this
161
+ // test defends is unchanged, it is just read from where the constant now
162
+ // lives, plus the assertion that goal-rollup.js consumes it rather than
163
+ // re-declaring a second copy that could drift from the client.
164
+ const vocab = read('modules/lifecycle/task-status.js');
165
+ assert.match(rollup, /require\('\.\/task-status'\)/);
166
+ assert.match(vocab, /const DONE_STATUSES = \['shipped', 'confirmed', 'completed'\]/);
161
167
  assert.match(lib, /DONE_STATUSES = new Set\(\['completed', 'confirmed', 'shipped'\]\)/);
162
- assert.match(rollup, /const CUT_STATUSES = \['abandoned', 'dropped'\]/);
168
+ assert.match(vocab, /const CUT_STATUSES = \['abandoned', 'dropped'\]/);
163
169
  assert.match(lib, /CUT_STATUSES = new Set\(\['abandoned', 'dropped'\]\)/);
164
170
  // blocked = backlog with >=1 unshipped dependency, both sides
165
171
  assert.match(rollup, /WHEN s\.status = 'backlog' AND s\.has_unshipped_dep THEN 'blocked'/);