@bongos/core 1.19.557 → 1.19.559

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.557",
6
- "core_contract": "1.19.557",
7
- "source_commit": "07517c180e4fca730285ee6533c21d6b54f08273",
5
+ "core_version": "1.19.559",
6
+ "core_contract": "1.19.559",
7
+ "source_commit": "5a03a11dde038c01316ec0013d430fceee6b470c",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-06T16:32:40.671Z",
9
+ "built_at": "2026-09-06T17:00:29.990Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 443,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 2020,
14
+ "functional_verbatim": 2022,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2487,
20
- "tree_sha256": "a3c96b5cf7b985332e67132f9626118afc2968733a0e5cd2cf0a7fbe7d885540",
19
+ "file_count": 2489,
20
+ "tree_sha256": "b08a96dd47fdc32384cfc3f290607a768f383b389fd4856eb23ba1573206e9ba",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -1787,7 +1787,7 @@
1787
1787
  {
1788
1788
  "path": "docs/architecture.md",
1789
1789
  "mode": "0000644",
1790
- "sha256": "10a4bda9248333a4a3bca3b015c70ae9d2f973772be75e790a800420e5d5e8bf"
1790
+ "sha256": "1fe7548e94bd9b29642bdddc8dacb201072782f181b517be2bac5fed1b1cd356"
1791
1791
  },
1792
1792
  {
1793
1793
  "path": "docs/branding-contract.md",
@@ -2657,7 +2657,7 @@
2657
2657
  {
2658
2658
  "path": "docs/module-api-changelog.md",
2659
2659
  "mode": "0000644",
2660
- "sha256": "99b29fcf1f9a58ee17ba9d77e3c0c30b37940ee8af0c71396813f7514d960ba1"
2660
+ "sha256": "0f460fce31df884314ef67a464a571dd268fd80e09135c2dd7918a0c83617894"
2661
2661
  },
2662
2662
  {
2663
2663
  "path": "docs/modules-contract.md",
@@ -5482,7 +5482,7 @@
5482
5482
  {
5483
5483
  "path": "modules/lifecycle/done-when.js",
5484
5484
  "mode": "0000644",
5485
- "sha256": "4e276151342f0eab3d4c0cea1ed55db8716a1d3d4b40568966be495f4892c1f4"
5485
+ "sha256": "4e798aff55de6481ea9da3f8a08e68d99902f3eac4749e30d727517ff61176b5"
5486
5486
  },
5487
5487
  {
5488
5488
  "path": "modules/lifecycle/est-advisory.js",
@@ -7477,12 +7477,12 @@
7477
7477
  {
7478
7478
  "path": "package-lock.json",
7479
7479
  "mode": "0000644",
7480
- "sha256": "248924f051a38d806cdf6818c66768dd3d1cf7f62196a01aee776f7f1849374e"
7480
+ "sha256": "ea8aa9f5d4fa502d3d572540e1ee389972089112bc245cd0fd8f55056580800a"
7481
7481
  },
7482
7482
  {
7483
7483
  "path": "package.json",
7484
7484
  "mode": "0000644",
7485
- "sha256": "3fac6b2004dd142b67afdce4e5939f5166ab777e74900e25b522af68568eb23f"
7485
+ "sha256": "dec482642d3b8ab9427292ae88990b4ad007f043f21923fc24d043aed110ad57"
7486
7486
  },
7487
7487
  {
7488
7488
  "path": "public-docs/index.html",
@@ -7917,7 +7917,7 @@
7917
7917
  {
7918
7918
  "path": "scripts/gds/fitness-ratchets.js",
7919
7919
  "mode": "0000644",
7920
- "sha256": "fa16d4c62a16b7827fe82d556dd6a5de327362ba17fc54cc9d86e1d34e2a45f5"
7920
+ "sha256": "40f6cb65333235d35f6672c700ec91648549e57912ee7f522cff80af4b611fc2"
7921
7921
  },
7922
7922
  {
7923
7923
  "path": "scripts/gds/fitness.js",
@@ -7954,6 +7954,11 @@
7954
7954
  "mode": "0000644",
7955
7955
  "sha256": "26a3dcc15c98128229df015292b8f9e9d3a41152c8803555f858fa4bb6a26d53"
7956
7956
  },
7957
+ {
7958
+ "path": "scripts/gds/gds-literal-scan.js",
7959
+ "mode": "0000644",
7960
+ "sha256": "fe622c9abf848a668fe2672d699fef22141156f41bb2396523461d1484341ec1"
7961
+ },
7957
7962
  {
7958
7963
  "path": "scripts/gds/gen-api-client.js",
7959
7964
  "mode": "0000644",
@@ -9177,7 +9182,7 @@
9177
9182
  {
9178
9183
  "path": "src/module-api.js",
9179
9184
  "mode": "0000644",
9180
- "sha256": "e0d8e56756e81c431cc74eaf5cea2ed87839bf7e6a222c98e43c279fd66d5beb"
9185
+ "sha256": "95d3088b7661e4be4b1fd900805b075e1766cd4aeed428979e1d969f005a63fb"
9181
9186
  },
9182
9187
  {
9183
9188
  "path": "src/module-loader/catalog.js",
@@ -9437,7 +9442,7 @@
9437
9442
  {
9438
9443
  "path": "tests/auto_satisfy_criteria.mjs",
9439
9444
  "mode": "0000644",
9440
- "sha256": "548e6a477660bb5e5cb260a0d5821bc3e11201225e64cfa45611c2fae07c80b2"
9445
+ "sha256": "d63beea7b680a194d163aa5ad11806c9aa860de93d614195a05a33082e9f8adb"
9441
9446
  },
9442
9447
  {
9443
9448
  "path": "tests/autonomous_fit_protected_paths.mjs",
@@ -10159,6 +10164,11 @@
10159
10164
  "mode": "0000644",
10160
10165
  "sha256": "5e5b53d5a1dce6b7ffacbea7198375aac6f77259ee948c00c24eb157f62a034d"
10161
10166
  },
10167
+ {
10168
+ "path": "tests/fitness_gds_ratchet.mjs",
10169
+ "mode": "0000644",
10170
+ "sha256": "10b3d6db55dd8d20de69fe8757575cf965ca8a14e91512c2f7b71b62ba3af855"
10171
+ },
10162
10172
  {
10163
10173
  "path": "tests/fitness_ratchets.mjs",
10164
10174
  "mode": "0000644",
@@ -120,7 +120,8 @@ tasks(id, version_id, title, description, status, touches text[], est_minutes, e
120
120
  shipped = merged to main + deployed; credits awarded here (credit_log.reason =
121
121
  'task.shipped' — ADR 0120 pays on land, not on confirm; which streams
122
122
  actually pay depends on the instance reward mode, ADR 0146)
123
- indexes: (version_id, status), (priority), GIN(touches)
123
+ indexes: (version_id, status), (priority), GIN(touches), (goal_id), (parent_task_id),
124
+ (created_by), (shipped_by) WHERE needs_rebase
124
125
  task_blockers(task_id, blocker_ref) -- markdown anchors are canonical
125
126
  claims(id, task_id, builder_id, worktree_name, claimed_at, released_at, outcome)
126
127
  UNIQUE INDEX uniq_active_claim_per_task ON (task_id) WHERE released_at IS NULL
@@ -1563,5 +1563,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
1563
1563
  landed since 1.19.555 with no explicit bump. run 34045323629. (task 1002620)
1564
1564
  1.19.557 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1565
1565
  landed since 1.19.556 with no explicit bump. run 34045753301. (task 1002620)
1566
+ 1.19.558 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1567
+ landed since 1.19.557 with no explicit bump. run 34046699120. (task 1002620)
1568
+ 1.19.559 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1569
+ landed since 1.19.558 with no explicit bump. run 34047187381. (task 1002620)
1566
1570
  ---------------------------------------------------------------------------
1567
1571
  ```
@@ -25,6 +25,7 @@
25
25
 
26
26
  const { pool } = require('../../src/module-api');
27
27
  const { nonTerminalSql } = require('./task-status');
28
+ const { SMOKE_NOT_LIKE } = require('./db-shared.js');
28
29
 
29
30
  // isPendingReview — the ONE derivation of the "met — pending review" auto-flag
30
31
  // (ADR 0086 §6 / BV1.R63): a criterion with >=1 linked task, every one shipped,
@@ -445,8 +446,33 @@ async function autoSatisfyShippedCriteria(exec, { taskId = null } = {}) {
445
446
  // Cascade. Deliberately re-derived from the table rather than from `criteria`:
446
447
  // a goal completes when ALL of its criteria are satisfied, and the ones that
447
448
  // closed in THIS call may not be all of them. Mirrors db.achieveGoalIfComplete
448
- // exactly — open goals only, >=1 criterion, zero unsatisfied — but on `exec`,
449
- // so it sees the flips above inside the same transaction.
449
+ // exactly — open goals only, >=1 criterion, zero unsatisfied, AND no unfinished
450
+ // task (BV1.R09, task 1003596) — but on `exec`, so it sees the flips above
451
+ // inside the same transaction.
452
+ //
453
+ // THE THIRD CLAUSE, and why it must run HERE and not only in db-goals.js. This
454
+ // is the path that actually fires on every ship (`db-ship.js`) and every
455
+ // reconciler tick (`publish-reconciler.js`); `achieveGoalIfComplete` covers the
456
+ // manual `POST /done-when/:id/satisfy`. The two are a DELIBERATE duplicate —
457
+ // collapsing them into one helper creates a require cycle (db-goals requires
458
+ // done-when) — so the rule is written twice on purpose and both copies must
459
+ // move together, or a goal closes correctly through one door and wrongly
460
+ // through the other. ADR 0250 D2.
461
+ //
462
+ // `exec` is load-bearing for THIS clause specifically. The shipping task's flip
463
+ // to `shipped` is uncommitted at this point in the ship transaction; on the
464
+ // module pool the subquery would still see it as `confirmed` — non-terminal —
465
+ // and refuse to close a goal whose last task just landed. Every goal would then
466
+ // need a second, later trigger to close at all.
467
+ //
468
+ // Shipping is NOT blocked by any of this: the ship's own writes are untouched
469
+ // and this UPDATE simply matches no row, leaving the goal open.
470
+ //
471
+ // The subquery is index-served: migration 228 added idx_tasks_goal ON tasks
472
+ // (goal_id), so this is a small index scan per candidate goal, and the candidate
473
+ // set is only the goals whose criteria just closed in this call. (Raised in
474
+ // review off docs/architecture.md, whose tasks index list predated 228 and has
475
+ // been corrected.)
450
476
  const goalIds = [...new Set(criteria.map((c) => c.goal_id).filter((g) => g != null))].map(Number);
451
477
  if (goalIds.length === 0) return { criteria, goals: [] };
452
478
  const { rows: goals } = await exec.query(
@@ -456,6 +482,9 @@ async function autoSatisfyShippedCriteria(exec, { taskId = null } = {}) {
456
482
  AND g.status = 'open'
457
483
  AND EXISTS (SELECT 1 FROM done_when_criteria d WHERE d.goal_id = g.id)
458
484
  AND NOT EXISTS (SELECT 1 FROM done_when_criteria d WHERE d.goal_id = g.id AND NOT d.satisfied)
485
+ AND NOT EXISTS (SELECT 1 FROM tasks t WHERE t.goal_id = g.id
486
+ AND ${nonTerminalSql()}
487
+ AND ${SMOKE_NOT_LIKE.replace(/title/, 't.title')})
459
488
  RETURNING g.id, g.title, g.version_id, g.status`,
460
489
  [goalIds]
461
490
  );
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.557",
3
+ "version": "1.19.559",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.557",
9
+ "version": "1.19.559",
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.557",
3
+ "version": "1.19.559",
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",
@@ -276,6 +276,17 @@ function readBaselines() {
276
276
  }
277
277
 
278
278
  // Pure ratchet verdict — exported for the hermetic test.
279
+ // A metric whose name does not, by itself, tell the builder who tripped it what
280
+ // to do gets one line of guidance here. Appended to the regression message only
281
+ // — the ratchet-down warning already says exactly what to do.
282
+ const METRIC_HINTS = {
283
+ legacy_gds_literals:
284
+ ' — a NEW "GDS" literal entered the tree (goal 1000073 C5: the rename cannot come back). '
285
+ + 'Write Bongos instead. Run `node scripts/gds/gds-literal-scan.js` to see every counted file. '
286
+ + 'Version ids (GDS-V##), docs/adr, docs/session-logs, docs/audits, limitations and migrations '
287
+ + 'are excluded by construction — if your literal is history, it belongs in one of those.',
288
+ };
289
+
279
290
  function compareToBaselines(metrics, baselines) {
280
291
  const violations = [];
281
292
  const warnings = [];
@@ -285,7 +296,7 @@ function compareToBaselines(metrics, baselines) {
285
296
  if (floor === undefined) {
286
297
  warnings.push(`no baseline for "${name}" (value ${value}) — run --update-baselines`);
287
298
  } else if (value > floor) {
288
- violations.push(`${name} regressed: ${value} > baseline ${floor} — fix the regression; never raise the baseline to pass`);
299
+ violations.push(`${name} regressed: ${value} > baseline ${floor} — fix the regression; never raise the baseline to pass${METRIC_HINTS[name] || ''}`);
289
300
  } else if (value < floor) {
290
301
  warnings.push(`ratchet-down available: ${name} improved to ${value} (baseline ${floor}) — lower the baseline in config/fitness-baselines.json`);
291
302
  }
@@ -293,6 +304,19 @@ function compareToBaselines(metrics, baselines) {
293
304
  return { violations, warnings };
294
305
  }
295
306
 
307
+ // legacy_gds_literals — criterion C5 of goal 1000073, "the rename cannot come
308
+ // back" (task 1003669). A shrink-only count of the GDS acronym outside the
309
+ // frozen record, so a NEW literal reds the build the way any other regression
310
+ // does. It rides the ratchet machinery rather than being its own check on
311
+ // purpose: --update-baselines, the floors, and baseline-staleness then all
312
+ // apply to it for free, and a metric outside `metrics` would be silently
313
+ // dropped the next time anyone refreshed the file. Detector + the reasons for
314
+ // every exclusion: scripts/gds/gds-literal-scan.js.
315
+ function gdsMetrics() {
316
+ const { scanGdsLiterals } = require('./gds-literal-scan.js');
317
+ return { legacy_gds_literals: scanGdsLiterals().total };
318
+ }
319
+
296
320
  function computeMetrics({ withDocs = true } = {}) {
297
321
  const files = trackedJsFiles();
298
322
  const contents = new Map(files.map((f) => [f, readRepoFile(f)]));
@@ -301,6 +325,7 @@ function computeMetrics({ withDocs = true } = {}) {
301
325
  duplicate_window_count: duplicateWindowCount(contents),
302
326
  require_cycle_count: countCycles(requireGraph(contents)),
303
327
  console_call_count: consoleCallCount(contents),
328
+ ...gdsMetrics(),
304
329
  ...(withDocs ? docMetrics() : {}),
305
330
  };
306
331
  }
@@ -314,7 +339,7 @@ function checkQualityRatchets() {
314
339
  // FIRST so it is the headline rather than buried under per-metric noise.
315
340
  if (baselineError) violations.unshift(baselineError);
316
341
  return {
317
- name: 'quality ratchets — size, duplication, cycles, console, doc entropy only tighten (task 1003129)',
342
+ name: 'quality ratchets — size, duplication, cycles, console, doc entropy, legacy GDS literals only tighten (task 1003129)',
318
343
  hardFail: violations.length > 0,
319
344
  violations,
320
345
  warnings,
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // gds-literal-scan.js — the detector behind the GDS→Bongos rename ratchet
4
+ // (criterion C5 of goal 1000073, "the rename cannot come back"; task 1003669).
5
+ //
6
+ // WHY THIS EXISTS. The rename is landing criterion by criterion, and the goal's
7
+ // own sequencing says the ratchet lands SECOND — immediately after the prose
8
+ // pass — because "a rename without a ratchet reappears within a release, which
9
+ // is exactly how this term survived this long". This module is the measurement;
10
+ // the gate is the `legacy_gds_literals` metric in fitness-ratchets.js, which
11
+ // fails the build when the count RISES.
12
+ //
13
+ // WHY A COUNT AND NOT ZERO. Measured after C1 shipped (2026-09-06) there are
14
+ // still ~1,400 GDS literals outside the historical record: the `GDS_*` env
15
+ // names and the `scripts/gds/` + `/api/gds` paths are criterion C2's job, and
16
+ // C2 deliberately lands BEHIND this ratchet because it is the risky one (hooks,
17
+ // cron routines, .service files and CI all name those paths). A zero-tolerance
18
+ // check could not be landed today and would red every unrelated ship. So this
19
+ // holds the line instead: the number may fall, never rise.
20
+ //
21
+ // WHAT IS NEVER COUNTED. Four kinds of thing, for four different reasons:
22
+ //
23
+ // 1. THE FROZEN RECORD (path-excluded). ADRs, session logs, audits, shipped
24
+ // scope archives. The goal's rule is "rename what the project says about
25
+ // itself going forward; leave what it recorded about its past" — an ADR
26
+ // rewritten to today's vocabulary destroys the evidence that the words
27
+ // changed.
28
+ // 2. APPLIED MIGRATION FILENAMES (path-excluded). schema_migrations is keyed
29
+ // by filename stem, so renaming an applied migration makes it look
30
+ // unapplied and RE-RUNS it. This one is an outage, not an aesthetic.
31
+ // 3. VERSION IDS (token-excluded). `GDS-V3` / `GDS-V4` are row keys in the
32
+ // versions table, not vocabulary. They are stripped from every line before
33
+ // counting, so they stay legal even inside a file that is otherwise
34
+ // counted.
35
+ //
36
+ // Usage:
37
+ // node scripts/gds/gds-literal-scan.js # count + the top offending files
38
+ // node scripts/gds/gds-literal-scan.js --files # every file with its count
39
+ // node scripts/gds/gds-literal-scan.js --json
40
+
41
+ const fs = require('node:fs');
42
+ const path = require('node:path');
43
+ const { execFileSync } = require('node:child_process');
44
+
45
+ const ROOT = path.resolve(__dirname, '..', '..');
46
+
47
+ // The frozen record + applied migration filenames. Matched against the
48
+ // repo-relative path, at any depth, so an instance's own migrations/ and a
49
+ // module-owned migrations/ are covered by the same rule as the core's.
50
+ const GDS_EXCLUDED_PATHS = [
51
+ /(^|\/)docs\/adr\//,
52
+ /(^|\/)docs\/session-logs\//,
53
+ /(^|\/)docs\/audits\//,
54
+ /(^|\/)limitations\//,
55
+ /(^|\/)migrations\//,
56
+ ];
57
+
58
+ // Vendored / generated / third-party trees the budgets must not count — the
59
+ // same exclusion fitness-ratchets.js applies to its own metrics.
60
+ const GDS_EXCLUDED_TREES = /(^|\/)(node_modules|public-docs)\/|\.min\.js$|(^|\/)vendor\//;
61
+
62
+ // The files that STATE the rule. Meta-commentary about a banned word has to be
63
+ // able to name it, or the gate cannot explain itself — the same principle as
64
+ // VOCAB_ALLOW's "the manifest states the rule itself" entry in
65
+ // fitness-checks-vocabulary.js. Without this the detector counts its own
66
+ // explanation, and improving a comment here would red the build.
67
+ const GDS_RULE_FILES = new Set([
68
+ 'scripts/gds/gds-literal-scan.js', // this file
69
+ 'scripts/gds/fitness-ratchets.js', // the gate: names the metric and carries the builder-facing hint
70
+ 'tests/fitness_gds_ratchet.mjs', // its test, which must write the literal to prove the gate fires
71
+ 'config/fitness-baselines.json', // the baseline's own _comment explains what is counted
72
+ ]);
73
+
74
+ // A version id is a row key, not vocabulary. Stripped before matching so it is
75
+ // legal on any line in any counted file.
76
+ const GDS_VERSION_ID_RE = /GDS-V\d+/g;
77
+
78
+ // The literal itself: case-sensitive, so the lowercase path/env spellings that
79
+ // criterion C2 owns (`scripts/gds/`, `/api/gds`, `gds_session`) are counted
80
+ // only where they appear in their SHOUTING form. Case-sensitivity is the point
81
+ // — `GDS` is the acronym a newcomer trips over.
82
+ const GDS_LITERAL_RE = /GDS/g;
83
+
84
+ function isExcludedPath(relPath) {
85
+ if (GDS_RULE_FILES.has(relPath)) return true;
86
+ if (GDS_EXCLUDED_TREES.test(relPath)) return true;
87
+ return GDS_EXCLUDED_PATHS.some((re) => re.test(relPath));
88
+ }
89
+
90
+ // countInText — occurrences of the literal in one file's text, with version ids
91
+ // removed first. Exported so the test and any future sweep share ONE detector
92
+ // (the checkErrorEnvelope precedent: a gate whose detector drifts from its
93
+ // sweep is worse than no gate).
94
+ function countInText(text) {
95
+ const stripped = text.replace(GDS_VERSION_ID_RE, '');
96
+ return (stripped.match(GDS_LITERAL_RE) || []).length;
97
+ }
98
+
99
+ function trackedFiles() {
100
+ const out = execFileSync('git', ['ls-files'], { cwd: ROOT, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
101
+ return out.split('\n').map((s) => s.trim()).filter(Boolean);
102
+ }
103
+
104
+ // scanGdsLiterals — { total, byFile } across every tracked, countable file.
105
+ // Unreadable and binary files are skipped rather than guessed at: a NUL byte
106
+ // means the "occurrences" would be meaningless anyway.
107
+ //
108
+ // `files` and `readFile` are both injectable so the test can exercise the real
109
+ // aggregation — exclusions, counting, per-file totals — WITHOUT writing to the
110
+ // repo. That is not tidiness: the unit lane runs test files in parallel, and an
111
+ // earlier version of the test proved the point by temporarily editing
112
+ // docs/architecture.md and breaking whichever suite happened to be scanning the
113
+ // tree at that moment.
114
+ function scanGdsLiterals({ files = null, readFile = null } = {}) {
115
+ const read = readFile || ((relPath) => fs.readFileSync(path.join(ROOT, relPath), 'utf8'));
116
+ const byFile = {};
117
+ let total = 0;
118
+ for (const relPath of files || trackedFiles()) {
119
+ if (isExcludedPath(relPath)) continue;
120
+ let text;
121
+ try {
122
+ text = read(relPath);
123
+ } catch {
124
+ continue;
125
+ }
126
+ if (text.includes(String.fromCharCode(0))) continue; // binary (a NUL byte)
127
+ const n = countInText(text);
128
+ if (n) {
129
+ byFile[relPath] = n;
130
+ total += n;
131
+ }
132
+ }
133
+ return { total, byFile };
134
+ }
135
+
136
+ function main() {
137
+ const { total, byFile } = scanGdsLiterals();
138
+ const ranked = Object.entries(byFile).sort((a, b) => b[1] - a[1]);
139
+ if (process.argv.includes('--json')) {
140
+ process.stdout.write(`${JSON.stringify({ total, byFile }, null, 2)}\n`);
141
+ return;
142
+ }
143
+ const show = process.argv.includes('--files') ? ranked : ranked.slice(0, 20);
144
+ console.log(`legacy GDS literals: ${total} across ${ranked.length} file(s)`);
145
+ console.log('(version ids GDS-V## are not counted; docs/adr, docs/session-logs, docs/audits, limitations and migrations are excluded by construction)');
146
+ console.log('');
147
+ for (const [f, n] of show) console.log(` ${String(n).padStart(5)} ${f}`);
148
+ if (show.length < ranked.length) console.log(` … and ${ranked.length - show.length} more file(s) — pass --files for all`);
149
+ }
150
+
151
+ if (require.main === module) main();
152
+
153
+ module.exports = {
154
+ GDS_EXCLUDED_PATHS,
155
+ GDS_RULE_FILES,
156
+ GDS_EXCLUDED_TREES,
157
+ GDS_LITERAL_RE,
158
+ GDS_VERSION_ID_RE,
159
+ countInText,
160
+ isExcludedPath,
161
+ scanGdsLiterals,
162
+ };
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.557'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
52
+ const CORE_VERSION = '1.19.559'; // 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');
@@ -213,7 +213,6 @@ await test('closeCompletedWorkForShip: silent on the common no-op, and logs what
213
213
  assert.match(loud[0], /shipTask 9/);
214
214
  });
215
215
 
216
- summary();
217
216
 
218
217
  // ---- 4. the terminal vocabulary has exactly one spelling (task 1003588) ----
219
218
 
@@ -233,3 +232,56 @@ await test('VOCABULARY: no consumer re-types the terminal set behind the constan
233
232
  f + ' must read the terminal set from task-status.js, not spell it again');
234
233
  }
235
234
  });
235
+
236
+ // ---- 5. the cascade carries R08's rule too (BV1.R09, task 1003596) ---------
237
+ //
238
+ // The two achievement writers are a DELIBERATE duplicate — collapsing them makes
239
+ // a require cycle (db-goals requires done-when) — so the danger is that only one
240
+ // moves and a goal then closes correctly through the manual door and wrongly
241
+ // through the ship. These cases pin the third clause on the path that actually
242
+ // fires on every ship and every reconciler tick.
243
+
244
+ await test('CASCADE (R09): an unfinished task in the goal blocks the flip', async () => {
245
+ const exec = fakeExec({ rows: [CRIT] }, { rows: [] });
246
+ await autoSatisfyShippedCriteria(exec, { taskId: 5 });
247
+ const cascade = String(exec.calls[1].sql).replace(/\s+/g, ' ');
248
+ assert.match(cascade, /NOT EXISTS \(SELECT 1 FROM tasks t WHERE t\.goal_id = g\.id/,
249
+ 'satisfied criteria are necessary, not sufficient — ADR 0250 D2');
250
+ assert.match(cascade, /t\.status NOT IN \('shipped', 'abandoned'\)/,
251
+ 'the terminal set is R01\'s, so completed/confirmed hold the goal open');
252
+ assert.match(cascade, /t\.title NOT LIKE/,
253
+ 'a smoke fixture must not wedge a real goal open forever');
254
+ });
255
+
256
+ await test('CASCADE (R09): the rule matches db-goals.js — both writers or neither', () => {
257
+ // The duplicate is intentional; the DRIFT is the bug. Both copies must ask for
258
+ // the same terminal set and the same smoke exclusion.
259
+ const dbGoals = readFileSync(new URL('../modules/lifecycle/db-goals.js', import.meta.url), 'utf8');
260
+ const doneWhen = readFileSync(new URL('../modules/lifecycle/done-when.js', import.meta.url), 'utf8');
261
+ for (const [name, src] of [['db-goals.js', dbGoals], ['done-when.js', doneWhen]]) {
262
+ assert.match(src, /FROM tasks t/, name + ' must consult tasks at all');
263
+ assert.match(src, /\$\{nonTerminalSql\(\)\}/,
264
+ name + ' must read the terminal set from task-status.js, not re-type it');
265
+ assert.match(src, /SMOKE_NOT_LIKE\.replace/,
266
+ name + ' must exclude smoke rows the way version_progress does');
267
+ }
268
+ });
269
+
270
+ await test('CASCADE (R09): the check runs on the caller exec, never the module pool', async () => {
271
+ // Load-bearing for this clause specifically: the shipping task's flip to
272
+ // `shipped` is uncommitted here, so on the module pool the subquery would still
273
+ // read it as `confirmed` — non-terminal — and refuse to close a goal whose last
274
+ // task just landed.
275
+ const exec = fakeExec({ rows: [CRIT] }, { rows: [] });
276
+ await autoSatisfyShippedCriteria(exec, { taskId: 5 });
277
+ assert.equal(exec.calls.length, 2, 'both statements go through the passed exec');
278
+ assert.match(String(exec.calls[1].sql), /UPDATE goals g/,
279
+ 'the cascade UPDATE is the second, and it is the one carrying the task check');
280
+ const src = readFileSync(new URL('../modules/lifecycle/done-when.js', import.meta.url), 'utf8');
281
+ const start = src.indexOf('async function autoSatisfyShippedCriteria');
282
+ const body = src.slice(start, src.indexOf('describeAutoClose', start));
283
+ assert.equal(/\bpool\.query\(/.test(body), false,
284
+ 'nothing in this function may reach the module pool');
285
+ });
286
+
287
+ summary();
@@ -0,0 +1,152 @@
1
+ // tests/fitness_gds_ratchet.mjs — the GDS→Bongos rename ratchet (task 1003669,
2
+ // criterion C5 of goal 1000073, "the rename cannot come back").
3
+ //
4
+ // WHY. A gate nobody has watched FAIL is not a gate. The criterion's whole point
5
+ // is that the old name cannot creep back, so the load-bearing assertion here is
6
+ // the negative one: a newly-introduced literal must red the build, and the
7
+ // exclusions must NOT swallow it. The positive case (today's tree passes) is the
8
+ // cheap half — it would also pass if the detector were broken and returned zero.
9
+ //
10
+ // The detector is exercised directly rather than through the filesystem: it takes
11
+ // an explicit `files` list, so a case can name exactly the paths under test
12
+ // without writing into the repo or depending on what happens to be committed.
13
+ import assert from 'node:assert/strict';
14
+ import { test } from 'node:test';
15
+ import { createRequire } from 'node:module';
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+
20
+ const require = createRequire(import.meta.url);
21
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
22
+ const scan = require(path.join(ROOT, 'scripts', 'gds', 'gds-literal-scan.js'));
23
+ const ratchets = require(path.join(ROOT, 'scripts', 'gds', 'fitness-ratchets.js'));
24
+
25
+ const LITERAL = ['G', 'D', 'S'].join(''); // built, not written, so this file needs no exemption of its own
26
+
27
+ test('the detector COUNTS a bare literal — the gate is not a no-op', () => {
28
+ assert.equal(scan.countInText(`the ${LITERAL} is the old name`), 1);
29
+ assert.equal(scan.countInText(`${LITERAL} and ${LITERAL} again`), 2);
30
+ assert.equal(scan.countInText('Bongos all the way down'), 0);
31
+ });
32
+
33
+ test('a version id is NOT counted — it is a row key, not vocabulary', () => {
34
+ assert.equal(scan.countInText(`${LITERAL}-V3`), 0);
35
+ assert.equal(scan.countInText(`${LITERAL}-V4 shipped`), 0);
36
+ // …but a bare literal on the same line still counts: stripping the version id
37
+ // must not blind the detector to real drift beside it.
38
+ assert.equal(scan.countInText(`${LITERAL}-V3 and the ${LITERAL} acronym`), 1);
39
+ });
40
+
41
+ test('the frozen record is excluded — an ADR or session log may say the old name', () => {
42
+ for (const p of [
43
+ 'docs/adr/0064-rename.md',
44
+ 'docs/session-logs/2026-09-06-something.md',
45
+ 'docs/audits/some-audit.md',
46
+ 'limitations/v3-shipped.md',
47
+ 'migrations/047_something.sql',
48
+ 'modules/copy-desk/migrations/copy_desk_001_flags.sql', // a MODULE's migrations too
49
+ ]) {
50
+ assert.equal(scan.isExcludedPath(p), true, `expected excluded: ${p}`);
51
+ }
52
+ });
53
+
54
+ test('ordinary source and docs are NOT excluded — otherwise the gate guards nothing', () => {
55
+ for (const p of [
56
+ 'docs/architecture.md',
57
+ 'docs/recipes/self-host.md',
58
+ 'src/bongos/routes/live.js',
59
+ 'modules/discord/discord-bot.js',
60
+ '.claude/skills/builder-ship/SKILL.md',
61
+ ]) {
62
+ assert.equal(scan.isExcludedPath(p), false, `expected counted: ${p}`);
63
+ }
64
+ });
65
+
66
+ // A fake tree, so these cases touch no real file. See the note on
67
+ // scanGdsLiterals's `readFile` parameter: the unit lane runs test files in
68
+ // parallel, and a test that edits the repo to make its point breaks whichever
69
+ // suite is reading the repo at the time.
70
+ function fakeTree(contents) {
71
+ return {
72
+ files: Object.keys(contents),
73
+ readFile: (relPath) => contents[relPath],
74
+ };
75
+ }
76
+
77
+ test('a NEW literal in a counted file REDS the ratchet (the criterion itself)', () => {
78
+ const clean = fakeTree({ 'docs/architecture.md': 'The platform is Bongos.\n' });
79
+ const dirty = fakeTree({ 'docs/architecture.md': `The platform is Bongos.\nBut this line says ${LITERAL}.\n` });
80
+
81
+ const before = scan.scanGdsLiterals(clean).total;
82
+ const after = scan.scanGdsLiterals(dirty).total;
83
+ assert.equal(before, 0);
84
+ assert.equal(after, 1, 'a new literal must raise the count');
85
+
86
+ // …and a count above its baseline must be a VIOLATION, not a warning.
87
+ const { violations } = ratchets.compareToBaselines(
88
+ { legacy_gds_literals: after },
89
+ { legacy_gds_literals: before }
90
+ );
91
+ assert.equal(violations.length, 1, 'rising above the baseline must violate');
92
+ assert.match(violations[0], /legacy_gds_literals regressed/);
93
+ assert.match(violations[0], /rename cannot come back/, 'the message must tell the builder what rule they broke');
94
+ });
95
+
96
+ test('an edit to the FROZEN record never reds the build', () => {
97
+ const tree = fakeTree({
98
+ 'docs/adr/README.md': `The ${LITERAL} was renamed; this line is history.\n`,
99
+ 'docs/session-logs/2026-09-06-x.md': `A session that mentions ${LITERAL} twice: ${LITERAL}.\n`,
100
+ 'migrations/047_x.sql': `-- ${LITERAL} era migration\n`,
101
+ });
102
+ assert.equal(
103
+ scan.scanGdsLiterals(tree).total,
104
+ 0,
105
+ 'the frozen record may name the old name freely — that is the goal\'s stated rule'
106
+ );
107
+ });
108
+
109
+ test('the aggregation is per-file and additive across a mixed tree', () => {
110
+ const tree = fakeTree({
111
+ 'docs/architecture.md': `${LITERAL} and ${LITERAL}\n`, // 2, counted
112
+ 'src/bongos/routes/live.js': `// ${LITERAL}\n`, // 1, counted
113
+ 'docs/adr/0064-rename.md': `${LITERAL} ${LITERAL}\n`, // excluded
114
+ 'docs/recipes/self-host.md': 'Bongos only\n', // 0
115
+ });
116
+ const { total, byFile } = scan.scanGdsLiterals(tree);
117
+ assert.equal(total, 3);
118
+ assert.deepEqual(byFile, { 'docs/architecture.md': 2, 'src/bongos/routes/live.js': 1 });
119
+ });
120
+
121
+ test('the ratchet only tightens — a FALLING count is a warning, never a failure', () => {
122
+ const { violations, warnings } = ratchets.compareToBaselines(
123
+ { legacy_gds_literals: 10 },
124
+ { legacy_gds_literals: 20 }
125
+ );
126
+ assert.equal(violations.length, 0);
127
+ assert.equal(warnings.length, 1);
128
+ assert.match(warnings[0], /ratchet-down available/);
129
+ });
130
+
131
+ test('the metric is MANAGED — it survives --update-baselines and has a committed baseline', () => {
132
+ // A metric outside computeMetrics() would be silently dropped the next time
133
+ // anyone refreshed the file, which is the failure mode this asserts against.
134
+ const metrics = ratchets.computeMetrics({ withDocs: false });
135
+ assert.ok(
136
+ Object.prototype.hasOwnProperty.call(metrics, 'legacy_gds_literals'),
137
+ 'legacy_gds_literals must come from computeMetrics(), or --update-baselines will drop it'
138
+ );
139
+ assert.equal(typeof metrics.legacy_gds_literals, 'number');
140
+
141
+ const baselines = JSON.parse(fs.readFileSync(path.join(ROOT, 'config', 'fitness-baselines.json'), 'utf8'));
142
+ assert.equal(typeof baselines.metrics.legacy_gds_literals, 'number', 'the baseline must be committed');
143
+ });
144
+
145
+ test('the files that STATE the rule may name it', () => {
146
+ // Meta-commentary about a banned word has to be able to name the word, or the
147
+ // gate cannot explain itself to the person who tripped it.
148
+ for (const p of scan.GDS_RULE_FILES) {
149
+ assert.equal(scan.isExcludedPath(p), true, `rule-stating file must be exempt: ${p}`);
150
+ assert.ok(fs.existsSync(path.join(ROOT, p)), `rule-stating file must exist (stale exemption otherwise): ${p}`);
151
+ }
152
+ });