@bongos/core 1.19.323 → 1.19.324

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.323",
6
- "core_contract": "1.19.323",
7
- "source_commit": "6b1fb52bfb9b8d899ffc44c0b8a22fd08cab9379",
5
+ "core_version": "1.19.324",
6
+ "core_contract": "1.19.324",
7
+ "source_commit": "7448abdd2e984ab3e1d9acb627bb21e631f198c8",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-24T01:33:26.263Z",
9
+ "built_at": "2026-08-24T14:18:41.725Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 324,
13
13
  "agent_docs_stubbed": 22,
14
- "functional_verbatim": 1457,
14
+ "functional_verbatim": 1460,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1803,
20
- "tree_sha256": "6534c0e9c341ea8d670e2f26984693052bc41041c70f99e671d5abd7ca3448dc",
19
+ "file_count": 1806,
20
+ "tree_sha256": "170e8d01edc7c386adfef5d3c45b916a749225cd999f6e7aef6c65e8c1307c24",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -4652,12 +4652,12 @@
4652
4652
  {
4653
4653
  "path": "package-lock.json",
4654
4654
  "mode": "0000644",
4655
- "sha256": "670e665f41fbb4d1c4154b2e2a08dcb64383583a8162188101dd0807ae849caa"
4655
+ "sha256": "2bee07291c5d8ef2f9a3c4e0c7e10714c8e078d7a0e9bdc2321aecf4af7d9afe"
4656
4656
  },
4657
4657
  {
4658
4658
  "path": "package.json",
4659
4659
  "mode": "0000644",
4660
- "sha256": "27ab832e2bac7f7baac4016571d5536ac884b4b0f125a4b6f6a2dbae9b73112f"
4660
+ "sha256": "f7c47e473b34391b7357c799f632cfb8101bb723099baa3d41b2074deba44e37"
4661
4661
  },
4662
4662
  {
4663
4663
  "path": "public-docs/index.html",
@@ -5017,7 +5017,7 @@
5017
5017
  {
5018
5018
  "path": "scripts/gds/architect-audit.js",
5019
5019
  "mode": "0000644",
5020
- "sha256": "bb4d1723f6668116e5c4783bb39efbfe5249e3dd8ed9e1739f3488f92fd37b00"
5020
+ "sha256": "62c32c421cc02af44ca47aab58ff857dcf6791450acd5b36e83fe175906e21f6"
5021
5021
  },
5022
5022
  {
5023
5023
  "path": "scripts/gds/audit-authorship.sh",
@@ -5027,7 +5027,7 @@
5027
5027
  {
5028
5028
  "path": "scripts/gds/audit-deps.js",
5029
5029
  "mode": "0000644",
5030
- "sha256": "84275f8b3c8981717c1648f346954a55742a0fecd2f6617257c4f77894609e12"
5030
+ "sha256": "f0905213e5e8ba18ea4c3eab7e11bb7c00401a5653ac00990be15c65f361959d"
5031
5031
  },
5032
5032
  {
5033
5033
  "path": "scripts/gds/audit-exports.js",
@@ -5112,7 +5112,7 @@
5112
5112
  {
5113
5113
  "path": "scripts/gds/capture.js",
5114
5114
  "mode": "0000644",
5115
- "sha256": "950a65cf4766f795622c6780dcfa9a1b3f9aefe38f406ec6759b129028958aa9"
5115
+ "sha256": "946fa97d020eb481b7da559f0b5c99bfa16dc24b680a01e0ff3d3faeaf5bd701"
5116
5116
  },
5117
5117
  {
5118
5118
  "path": "scripts/gds/cf-tunnel.js",
@@ -5307,7 +5307,7 @@
5307
5307
  {
5308
5308
  "path": "scripts/gds/fitness.js",
5309
5309
  "mode": "0000644",
5310
- "sha256": "2de9f3b0e2c488ac1170aa90eab53c850b37c332676e36c4c8f214b219019352"
5310
+ "sha256": "b5432b114aa075f790d056bc8cb090d3ba83fb52498d9dbee4a81161ac5bc019"
5311
5311
  },
5312
5312
  {
5313
5313
  "path": "scripts/gds/gate-review.js",
@@ -5862,7 +5862,7 @@
5862
5862
  {
5863
5863
  "path": "scripts/gds/session-eval.js",
5864
5864
  "mode": "0000644",
5865
- "sha256": "6e236a05fa0a6459104bd16e8f2da5092c412b0c518511823eae0b39570eb678"
5865
+ "sha256": "9cdf1b5c6834568169cbe130cb2d28361baa747aa5bf7f358cc8e8631080790d"
5866
5866
  },
5867
5867
  {
5868
5868
  "path": "scripts/gds/session.js",
@@ -5892,7 +5892,7 @@
5892
5892
  {
5893
5893
  "path": "scripts/gds/ship-flow.js",
5894
5894
  "mode": "0000644",
5895
- "sha256": "efad88d4ead86356f27650b81c5ee28362bc94695dede92958e5398e9e71748c"
5895
+ "sha256": "c63d7bcdf4c25de60ad4106495e09bccfd98a19b223111988e4339a4460558b8"
5896
5896
  },
5897
5897
  {
5898
5898
  "path": "scripts/gds/ship-git.js",
@@ -5977,12 +5977,12 @@
5977
5977
  {
5978
5978
  "path": "scripts/gds/smoke-batch-claim.sh",
5979
5979
  "mode": "0000644",
5980
- "sha256": "d27deba8997676ed2224d2261316b3e60c0668bcde585f7efd0443d6ab150d89"
5980
+ "sha256": "53d3f4e28514aa6a3966e7a3db6223d4abe19074aaf44c39f61b920a0bdc35b0"
5981
5981
  },
5982
5982
  {
5983
5983
  "path": "scripts/gds/smoke-batch-release.sh",
5984
5984
  "mode": "0000644",
5985
- "sha256": "57e6f1ce66cd4e5cb7d6bbbd409f074b137344ba7c281a122f8ad42243d76029"
5985
+ "sha256": "68ea957b3e9e82917e1c1a2a870b4ebbd0e5ef80d101afa3cff39858c98a3a0f"
5986
5986
  },
5987
5987
  {
5988
5988
  "path": "scripts/gds/smoke-box.sh",
@@ -5992,7 +5992,7 @@
5992
5992
  {
5993
5993
  "path": "scripts/gds/smoke-claim-consistency.sh",
5994
5994
  "mode": "0000644",
5995
- "sha256": "55aa25f1d7143edec57833dcd582f05d1a1b7261bd73d51044fc48dff3532ee1"
5995
+ "sha256": "337d3f957cab32166b235a17c2604f9890d14067e31440a1400f345b21918eab"
5996
5996
  },
5997
5997
  {
5998
5998
  "path": "scripts/gds/smoke-dependencies.sh",
@@ -6007,7 +6007,7 @@
6007
6007
  {
6008
6008
  "path": "scripts/gds/smoke-write.sh",
6009
6009
  "mode": "0000644",
6010
- "sha256": "9c3a382def8c074a37bad66d7e3deef41c7f2bb9406f02f107be0e9a7dc7bfb3"
6010
+ "sha256": "5468e6fdf5330fa2a67aa9a027d747efdf09908f9d676c567f2e324fd55a5155"
6011
6011
  },
6012
6012
  {
6013
6013
  "path": "scripts/gds/spark.js",
@@ -6037,7 +6037,7 @@
6037
6037
  {
6038
6038
  "path": "scripts/gds/triage.js",
6039
6039
  "mode": "0000644",
6040
- "sha256": "5255bafa71699cadd345c5bb2ba6e99700cdd5d90fd165b949ef6595c5a83386"
6040
+ "sha256": "831de1914642a7dcf7c591c9cc69b37c917122e04428cb1e28fc00b07170b603"
6041
6041
  },
6042
6042
  {
6043
6043
  "path": "scripts/gds/update-channel.js",
@@ -6059,6 +6059,16 @@
6059
6059
  "mode": "0000644",
6060
6060
  "sha256": "32e0c1f2a955d4cabeae00d3749b283abd70f05f34b5fcbd84be3f35e2e655c8"
6061
6061
  },
6062
+ {
6063
+ "path": "scripts/gds/version-literal-guard.js",
6064
+ "mode": "0000644",
6065
+ "sha256": "7e27012820d6e2fb43c84140ae6f90d4ca9345658937aa75cc9f2c28cfdaf41e"
6066
+ },
6067
+ {
6068
+ "path": "scripts/gds/version-select.js",
6069
+ "mode": "0000644",
6070
+ "sha256": "be4fec619d5a0c76ba05bcc75042c1497f70da21b54a3d646884e2228f90b2ba"
6071
+ },
6062
6072
  {
6063
6073
  "path": "scripts/gds/worktree.js",
6064
6074
  "mode": "0000644",
@@ -6427,7 +6437,7 @@
6427
6437
  {
6428
6438
  "path": "src/module-api.js",
6429
6439
  "mode": "0000644",
6430
- "sha256": "8624156a5587eaca3430325ef66420fb96b9567ed19af27dce361dc8af08611f"
6440
+ "sha256": "cbeef525ef673bbd61cd645ea63a3cbc5bc7946d9a8f9e7b15457aaae0869220"
6431
6441
  },
6432
6442
  {
6433
6443
  "path": "src/module-loader/catalog.js",
@@ -8994,6 +9004,11 @@
8994
9004
  "mode": "0000644",
8995
9005
  "sha256": "372c80d42e122a70a4fae8fcfbaaa47459575d3e0a2b80fff8876dfc512c5aeb"
8996
9006
  },
9007
+ {
9008
+ "path": "tests/version_literals.mjs",
9009
+ "mode": "0000644",
9010
+ "sha256": "17e872131c17bae7a061debc1fd3ee354ab704678b475edd6a8b1dfdd63ceb2f"
9011
+ },
8997
9012
  {
8998
9013
  "path": "tests/versions_route.mjs",
8999
9014
  "mode": "0000644",
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.323",
3
+ "version": "1.19.324",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.323",
9
+ "version": "1.19.324",
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.323",
3
+ "version": "1.19.324",
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",
@@ -32,6 +32,7 @@ const fs = require('node:fs');
32
32
  const path = require('node:path');
33
33
  const { execFileSync } = require('node:child_process');
34
34
  const { apiCall, cliClient, arg } = require('./cli-lib');
35
+ const { resolveDefaultVersion } = require('./version-select.js');
35
36
  const grader = require('../../modules/grading/grader');
36
37
  const llmCache = require('../../src/bongos/llm-cache'); // System 5 (task 1360): cache identical audits over an unchanged tree
37
38
  const { branding } = require('../../src/branding');
@@ -183,7 +184,12 @@ async function main() {
183
184
  const args = process.argv.slice(2);
184
185
  const scope = arg('--scope', args);
185
186
  const paths = arg('--paths', args);
186
- const version = arg('--version', args) || 'GDS-V3';
187
+ // task 1003166 (G02): was 'GDS-V3', long since shipped.
188
+ const version = arg('--version', args) || await resolveDefaultVersion();
189
+ if (!version) {
190
+ console.error('architect-audit: no version is building; pass --version <id>.');
191
+ process.exit(1);
192
+ }
187
193
  const dryRun = args.includes('--dry-run');
188
194
 
189
195
  if (!scope || !paths) {
@@ -21,6 +21,7 @@
21
21
 
22
22
  const { touchesOverlap } = require('../../src/bongos/path-match.js');
23
23
  const { cliClient } = require('./cli-lib');
24
+ const { resolveDefaultVersion } = require('./version-select.js');
24
25
  // task 1003153 — the DEAD-edge detector core, shared with the claim refusal so
25
26
  // "abandoned dependency" means one thing across the CLI, the route and this audit.
26
27
  const { findStrandedTasks } = require('../../modules/lifecycle/dead-deps.js');
@@ -128,7 +129,16 @@ async function main() {
128
129
  process.exitCode = await reportStranded({ asJson: argv.includes('--json') });
129
130
  return;
130
131
  }
131
- const ver = argv.includes('--version') ? argv[argv.indexOf('--version') + 1] : 'GDS-V3';
132
+ // task 1003166 (G02): the default was 'GDS-V3', which has shipped — so a bare
133
+ // `audit-deps` audited a closed version and reported nothing useful.
134
+ const ver = argv.includes('--version')
135
+ ? argv[argv.indexOf('--version') + 1]
136
+ : await resolveDefaultVersion();
137
+ if (!ver) {
138
+ console.error('audit-deps: no version is building; pass --version <id>.');
139
+ process.exitCode = 1;
140
+ return;
141
+ }
132
142
  const minRank = argv.includes('--min') ? argv[argv.indexOf('--min') + 1] : 'low';
133
143
  const asJson = argv.includes('--json');
134
144
  const order = { high: 3, medium: 2, low: 1 };
@@ -54,22 +54,10 @@ const readline = require('node:readline');
54
54
  const { cliClient, requireSession, arg, hasFlag } = require('./cli-lib');
55
55
 
56
56
 
57
- async function resolveDefaultVersion() {
58
- const api = await cliClient();
59
- const r = await api.versions.getVersions();
60
- if (!r.ok) return null;
61
- const versions = r.data?.versions ?? [];
62
- // Most recent building version. Fall back to any building version if
63
- // started_at is null on all of them.
64
- const building = versions.filter((v) => v.status === 'building');
65
- if (building.length === 0) return null;
66
- building.sort((a, b) => {
67
- const ta = a.started_at ? new Date(a.started_at).getTime() : 0;
68
- const tb = b.started_at ? new Date(b.started_at).getTime() : 0;
69
- return tb - ta;
70
- });
71
- return building[0].id;
72
- }
57
+ // task 1003166 (G02): the selection rule moved to version-select.js so the nine
58
+ // scripts that used to hardcode a version id share ONE implementation. Kept as a
59
+ // local name so every call site below reads unchanged.
60
+ const { resolveDefaultVersion } = require('./version-select.js');
73
61
 
74
62
  // Full JSON, not console.error's depth-2 inspect — a validation 400's nested
75
63
  // details otherwise print as "[ [Object] ]" and hide which field failed.
@@ -1910,7 +1910,7 @@ const CHECKS = [
1910
1910
  checkWriteRoutesValidated, // Check 18 — BV1 R12 / task 1999: every body-reading write route validates (ADR 0118)
1911
1911
  require('./fitness-ratchets.js').checkQualityRatchets, // Check 20 — task 1003129: quality budgets only tighten (mechanics + knip CI step live in fitness-ratchets.js)
1912
1912
  require('./doc-cli-guard.js').checkDocNamesRealCli, // Check 22 — task 1003165 / G01: a doc that names a CLI invocation must match the real CLI
1913
- require('./skill-preflight.js').checkOpsSkillsDeclareMachine, // Check 23 — task 1003167 / G03: an ops skill declares the machine it needs
1913
+ require('./skill-preflight.js').checkOpsSkillsDeclareMachine, require('./version-literal-guard.js').checkNoVersionLiterals, // Checks 23 (task 1003167/G03: a skill declares its machine) + 24 (task 1003166/G02: no script hardcodes a version id) — one line because fitness.js is 500 over the excess-lines ratchet and a new check must not cost a line (idea 1000810)
1914
1914
  require('./control-manifest.js').checkControlsExist, // Check 21 — task 1003131: goal-1000076 controls assert their own existence (the ADR 0022 assurance-of-assurance)
1915
1915
  ];
1916
1916
 
@@ -19,6 +19,7 @@
19
19
  // Pure helper (no I/O) — resolves the per-rank wandering default so the
20
20
  // off-task signal below only fires for the CONSTRAINING ranks (task 1268).
21
21
  const wanderingPrefs = require('../../modules/builder-settings/wandering-prefs'); // carved → modules/builder-settings/ (BV1.R80 / ADR 0093 §1)
22
+ const { resolveDefaultVersion } = require('./version-select.js');
22
23
 
23
24
  // ---- classification vocabularies -----------------------------------------
24
25
 
@@ -547,6 +548,9 @@ if (require.main === module) {
547
548
  : []);
548
549
  let filed = 0;
549
550
  let skipped = 0;
551
+ // task 1003166 (G02): resolved ONCE — it is invariant for the run, and this
552
+ // is a network round trip. The first version of this called it per finding.
553
+ const suggestedVersion = await resolveDefaultVersion();
550
554
  for (const s of systemic) {
551
555
  const tmpl = FINDING_TEMPLATES[s.type] || { title: s.type, body: '' };
552
556
  const title = `${PREFIX} ${tmpl.title}`.slice(0, 200);
@@ -561,7 +565,8 @@ if (require.main === module) {
561
565
  body: {
562
566
  title,
563
567
  body_md: bodyMd,
564
- suggested_version: 'GDS-V4',
568
+ // task 1003166 (G02): was 'GDS-V4', which has shipped.
569
+ suggested_version: suggestedVersion,
565
570
  },
566
571
  }).catch(() => null);
567
572
  if (r && r.ok) filed++;
@@ -20,6 +20,10 @@ const { autoLinkifyChangedDocs, mergeMainIntoBranch, preflight } = require('./sh
20
20
  const { applyBaseOverride, computeChangedFiles, computeGraderBaseline, runSmokeAsync } = require('./ship-grade.js');
21
21
  const { buildPriorRoundContext, computeGradeResult, gradeAndRecord, printCreditsAndAchievements, printGradeOutcome } = require('./ship-grade-steps.js');
22
22
  const { apiErrorLine, normalizeApiError } = require('./ship-io.js');
23
+ // task 1003205's carve left pushVia() used at regradeMain but never imported, so
24
+ // EVERY resume of a 'completed' task died with ReferenceError before resolving.
25
+ const { pushVia } = require('./ship-deploy-target.js');
26
+
23
27
  const { diffstatFor, finishShip } = require('./ship-finish.js');
24
28
  const { evaluateRegradeEligibility, evaluateResumeAction, resumeMerge } = require('./ship-resume.js');
25
29
  const { noteServerReward } = require('./ship-state.js');
@@ -24,6 +24,14 @@ source "$(dirname "$0")/lib.sh"
24
24
  API="$(gds_api_base)"
25
25
  TOKEN="$(gds_resolve_token)" || exit $?
26
26
 
27
+ # task 1003166 (G02): resolve the version at RUNTIME rather than naming one. A
28
+ # literal works until that version ships, then `version_closed` refuses every
29
+ # create and the smoke fails as if the code broke (idea 1000823).
30
+ VERSION_ID=$(node "$(dirname "$0")/version-select.js") || {
31
+ echo "FAIL: could not resolve a building version — see version-select.js" >&2
32
+ exit 1
33
+ }
34
+
27
35
  ts=$(date +%Y%m%d-%H%M%S)
28
36
 
29
37
  # Wrapper: METHOD PATH [BODY] → echoes "HTTP_CODE\nPAYLOAD". Never exits.
@@ -49,7 +57,7 @@ mk_task() {
49
57
  import json,sys
50
58
  t = '${touches_csv}'.split(',')
51
59
  print(json.dumps({
52
- 'version_id': 'BONGOS-V1',
60
+ 'version_id': '${VERSION_ID}',
53
61
  'title': '__smoke__ batch-claim ${ts}-${suffix}',
54
62
  'description': 'smoke',
55
63
  'status': 'ready',
@@ -31,6 +31,14 @@ source "$(dirname "$0")/lib.sh"
31
31
  API="$(gds_api_base)"
32
32
  TOKEN="$(gds_resolve_token)" || exit $?
33
33
 
34
+ # task 1003166 (G02): resolve the version at RUNTIME rather than naming one. A
35
+ # literal works until that version ships, then `version_closed` refuses every
36
+ # create and the smoke fails as if the code broke (idea 1000823).
37
+ VERSION_ID=$(node "$(dirname "$0")/version-select.js") || {
38
+ echo "FAIL: could not resolve a building version — see version-select.js" >&2
39
+ exit 1
40
+ }
41
+
34
42
  ts=$(date +%Y%m%d-%H%M%S)
35
43
 
36
44
  # Wrapper: METHOD PATH [BODY] → echoes "PAYLOAD\nHTTP_CODE". Never exits.
@@ -55,7 +63,7 @@ mk_task() {
55
63
  import json,sys
56
64
  t = '${touches_csv}'.split(',')
57
65
  print(json.dumps({
58
- 'version_id': 'BONGOS-V1',
66
+ 'version_id': '${VERSION_ID}',
59
67
  'title': '__smoke__ batch-release ${ts}-${suffix}',
60
68
  'description': 'smoke',
61
69
  'status': 'ready',
@@ -86,7 +86,7 @@ create_task() {
86
86
  body=$(python3 -c "
87
87
  import json
88
88
  print(json.dumps({
89
- 'version_id': 'PMS-V2',
89
+ 'version_id': '${VERSION_ID}',
90
90
  'title': '${title}',
91
91
  'description': 'Synthetic smoke task. Auto-cleaned by smoke-claim-consistency.sh.',
92
92
  'status': 'ready',
@@ -124,6 +124,15 @@ echo "API: ${API}"
124
124
  echo "Titles: ${SMOKE_TITLE_A} | ${SMOKE_TITLE_B}"
125
125
  echo
126
126
 
127
+ # task 1003166 (G02): resolve the version at RUNTIME. This file used to hardcode
128
+ # one; that version shipped, `version_closed` refused every create, and the smoke
129
+ # could not pass while /merge-mode still advertised it green (idea 1000823).
130
+ VERSION_ID=$(node "$(dirname "$0")/version-select.js") || {
131
+ echo "FAIL: could not resolve a building version — see version-select.js" >&2
132
+ exit 1
133
+ }
134
+ echo "Version: ${VERSION_ID}"
135
+
127
136
  echo "--- 1. create 2 smoke tasks ---"
128
137
  TASK_A=$(create_task "${SMOKE_TITLE_A}" "A")
129
138
  TASK_B=$(create_task "${SMOKE_TITLE_B}" "B")
@@ -58,6 +58,15 @@ echo "API: ${API}"
58
58
  echo "Title: ${SMOKE_TITLE}"
59
59
  echo
60
60
 
61
+ # task 1003166 (G02): resolve the version at RUNTIME. This file used to hardcode
62
+ # one; that version shipped, `version_closed` refused every create, and the smoke
63
+ # could not pass while /merge-mode still advertised it green (idea 1000823).
64
+ VERSION_ID=$(node "$(dirname "$0")/version-select.js") || {
65
+ echo "FAIL: could not resolve a building version — see version-select.js" >&2
66
+ exit 1
67
+ }
68
+ echo "Version: ${VERSION_ID}"
69
+
61
70
  echo "--- 0. snapshot progress before ---"
62
71
  PROGRESS_BEFORE=$(snapshot_progress)
63
72
  echo "${PROGRESS_BEFORE}"
@@ -67,7 +76,7 @@ echo "--- 1. create task ---"
67
76
  CREATE_BODY=$(python3 -c "
68
77
  import json
69
78
  print(json.dumps({
70
- 'version_id': 'PMS-V1',
79
+ 'version_id': '${VERSION_ID}',
71
80
  'title': '${SMOKE_TITLE}',
72
81
  'description': 'Synthetic smoke task. Auto-shipped by smoke-write.sh.',
73
82
  'status': 'ready',
@@ -38,6 +38,7 @@
38
38
  const readline = require('node:readline');
39
39
  const fs = require('node:fs');
40
40
  const { cliClient, requireSession } = require('./cli-lib');
41
+ const { resolveDefaultVersion } = require('./version-select.js');
41
42
 
42
43
  function prompt(rl, question) {
43
44
  return new Promise((resolve) => rl.question(question, (a) => resolve(a)));
@@ -384,7 +385,9 @@ async function main() {
384
385
 
385
386
  if (ans === 'p' || ans === 'promote') {
386
387
  // Promote: version, priority, title, goal — each defaulted from the idea.
387
- const defaultVersion = idea.suggested_version || 'PMS-V2';
388
+ // task 1003166 (G02): the fallback was 'PMS-V2', which has shipped —
389
+ // promoting into it would have been refused as version_closed.
390
+ const defaultVersion = idea.suggested_version || await resolveDefaultVersion();
388
391
  const versionAns = (await prompt(rl, ` version [${defaultVersion}]: `)).trim();
389
392
  const versionId = versionAns || defaultVersion;
390
393
 
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/version-literal-guard.js — no ops script names a version id
3
+ // (task 1003166 / G02, goal 1000077; criterion guards-no-hardcoded-version).
4
+ //
5
+ // THE INCIDENT. smoke-write.sh hardcoded 'PMS-V1'. That version shipped, so
6
+ // `version_closed` refused every create and the write-path smoke COULD NOT PASS
7
+ // — while /merge-mode Step 5 went on telling the operator "4/4 expected". The
8
+ // failure reads as a broken smoke, not a stale constant, which is why it sat
9
+ // (idea 1000823). Nine scripts carried the same bomb; four of the versions they
10
+ // named (PMS-V1, PMS-V2, GDS-V3, GDS-V4) had ALREADY shipped when this ran.
11
+ //
12
+ // WHAT IT CHECKS, narrowly: a version-shaped literal ASSIGNED TO A VERSION FIELD
13
+ // or used as a version DEFAULT, in an executable script. Not any mention of a
14
+ // version anywhere — smoke-claim-consistency.sh legitimately says "PMS-V2 P6 not
15
+ // live yet" in a comment and an echo, and flagging that would be the over-broad
16
+ // source assertion this repo has learned to avoid (learning 1000196). Comment
17
+ // lines are stripped before matching for the same reason.
18
+ //
19
+ // EXCLUDED BY PATH, not by allowlist: seeders and tests. A seeder's whole subject
20
+ // IS a specific historical version — `seed-v3-tasks.js` naming GDS-V3 is correct,
21
+ // and asking it to resolve one at runtime would break it.
22
+
23
+ 'use strict';
24
+
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+ const { execFileSync } = require('node:child_process');
28
+
29
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
30
+
31
+ // Any prefix, so a version scheme invented next year is covered too — better
32
+ // than enumerating PMS/GDS/BONGOS/CB and finding out later that OTB-V2 slipped.
33
+ const VERSION_LITERAL = String.raw`['"][A-Z][A-Z0-9]*-V\d+['"]`;
34
+ // A version FIELD or a version DEFAULT — the two shapes that actually rot.
35
+ const VERSION_FIELD = String.raw`version_id|versionId|suggested_version|target_version|DEFAULT_VERSION|VERSION_ID`;
36
+ const ASSIGNED = new RegExp(`(?:${VERSION_FIELD})\\s*['"]?\\s*[:=][^,;)]*${VERSION_LITERAL}`);
37
+ const DEFAULTED = new RegExp(`--version[^\\n]*?\\|\\|\\s*${VERSION_LITERAL}`);
38
+
39
+ // path::reason. A justified entry, one line, like every other guard here.
40
+ const VERSION_LITERAL_ALLOW = new Map([
41
+ ['scripts/gds/grade-smoke.js',
42
+ 'GOOD_TASK / BAD_TASK are in-memory fixtures handed to grader.gradeSubagent; they are never POSTed, so no version_closed can refuse them and the literal cannot rot into a failure.'],
43
+ ['scripts/gds/status.js',
44
+ 'DEFAULT_VERSION is an owner-facing dashboard default with an env override, and TWO versions are currently building — so "most recent building" is not a derivable answer here. Retargeting what /status shows by default is an owner decision, not a refactor.'],
45
+ ]);
46
+
47
+ function isExempt(rel) {
48
+ return /(^|\/)seed-[^/]*\.js$/.test(rel) // a seeder's subject IS that version
49
+ || /\.test\.js$/.test(rel) // fixtures pinned to a historical version
50
+ || /(^|\/)fixtures\//.test(rel)
51
+ || /(^|\/)version-literal-guard\.js$/.test(rel); // this file names the shapes it hunts
52
+ }
53
+
54
+ // Drop comment lines only — never inline content, which is where real code lives.
55
+ function stripComments(src, rel) {
56
+ const lineComment = rel.endsWith('.sh') ? /^\s*#/ : /^\s*(\/\/|\*|\/\*)/;
57
+ return String(src).split('\n').map((l) => (lineComment.test(l) ? '' : l));
58
+ }
59
+
60
+ function scanScripts(files, read) {
61
+ const violations = [];
62
+ let scanned = 0;
63
+ for (const rel of files) {
64
+ if (isExempt(rel)) continue;
65
+ const src = read(rel);
66
+ if (src === null) continue;
67
+ scanned += 1;
68
+ if (VERSION_LITERAL_ALLOW.has(rel)) continue;
69
+ stripComments(src, rel).forEach((line, i) => {
70
+ if (ASSIGNED.test(line) || DEFAULTED.test(line)) {
71
+ violations.push(
72
+ `${rel}:${i + 1} names a version id where one should be RESOLVED: ${line.trim().slice(0, 90)}`
73
+ + ' — a literal works until that version ships, then version_closed refuses every write and the'
74
+ + ' failure reads as broken code. Use resolveDefaultVersion() from version-select.js'
75
+ + ' (bash: VERSION_ID=$(node scripts/gds/version-select.js)), or allowlist it with a reason.'
76
+ );
77
+ }
78
+ });
79
+ }
80
+ return { violations, scanned };
81
+ }
82
+
83
+ function trackedScripts() {
84
+ const out = execFileSync('git', ['ls-files', 'scripts/*.js', 'scripts/*.sh', 'scripts/**/*.js', 'scripts/**/*.sh'], {
85
+ cwd: REPO_ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
86
+ });
87
+ return String(out || '').split('\n').map((f) => f.trim()).filter(Boolean);
88
+ }
89
+
90
+ const NAME = 'no ops script hardcodes a version id (G02)';
91
+
92
+ function checkNoVersionLiterals({ files = null, read = null } = {}) {
93
+ const list = files || trackedScripts();
94
+ if (!list.length) {
95
+ return { name: NAME, ok: false, hardFail: true,
96
+ violations: ['scan defect — enumerated 0 tracked scripts; a broken enumeration, not a clean result.'],
97
+ warnings: [], note: 'static scan of scripts/ for hardcoded version ids.' };
98
+ }
99
+ const reader = read || ((rel) => {
100
+ try { return fs.readFileSync(path.join(REPO_ROOT, rel), 'utf8'); } catch { return null; }
101
+ });
102
+ const { violations, scanned } = scanScripts(list, reader);
103
+ return {
104
+ name: NAME,
105
+ ok: violations.length === 0,
106
+ hardFail: violations.length > 0,
107
+ violations,
108
+ warnings: [],
109
+ note: `${scanned} executable script(s) under scripts/ scanned: none may assign a version-shaped literal to a version field or use one as a --version default. Seeders, *.test.js and fixtures are exempt by path (their subject IS a historical version); ${VERSION_LITERAL_ALLOW.size} justified allowlist entr${VERSION_LITERAL_ALLOW.size === 1 ? 'y' : 'ies'}.`,
110
+ };
111
+ }
112
+
113
+ function main() {
114
+ const r = checkNoVersionLiterals();
115
+ for (const v of r.violations) console.error(`✗ ${v}`);
116
+ console.log(`${r.hardFail ? 'FAIL' : 'PASS'} ${r.note}`);
117
+ process.exit(r.hardFail ? 1 : 0);
118
+ }
119
+
120
+ if (require.main === module) main();
121
+
122
+ module.exports = { checkNoVersionLiterals, scanScripts, stripComments, isExempt, VERSION_LITERAL_ALLOW, ASSIGNED, DEFAULTED };
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/version-select.js — resolve "the version to use" at RUNTIME
3
+ // (task 1003166 / G02, goal 1000077; criterion guards-no-hardcoded-version).
4
+ //
5
+ // THE BUG THIS ENDS. scripts/gds/smoke-write.sh hardcoded 'PMS-V1'. That version
6
+ // shipped, so `version_closed` correctly refuses the create — meaning the
7
+ // write-path smoke CANNOT pass — while /merge-mode Step 5 still tells the
8
+ // operator "4/4 expected". A literal version id is a time bomb with a long fuse:
9
+ // it works until the version ships, and then it fails in a way that reads as a
10
+ // broken smoke rather than a stale constant (idea 1000823).
11
+ //
12
+ // Nine scripts carried one. The rule they now share is capture.js's, which was
13
+ // already right: the most recent BUILDING version. Extracted here so there is one
14
+ // implementation rather than nine, and exposed as a CLI so the bash smokes get
15
+ // the same answer as the JS callers instead of reimplementing the sort in python.
16
+ //
17
+ // const { resolveDefaultVersion } = require('./version-select.js'); // JS
18
+ // VERSION_ID=$(node scripts/gds/version-select.js) || exit 1 # bash
19
+
20
+ 'use strict';
21
+
22
+ const { cliExit } = require('./cli-lib');
23
+
24
+ // PURE, so the selection rule is unit-tested without a session or a network.
25
+ // Returns a version id, or null when nothing is building — never a guess.
26
+ function pickDefaultVersion(versions) {
27
+ if (!Array.isArray(versions)) return null;
28
+ const building = versions.filter((v) => v && v.status === 'building');
29
+ if (building.length === 0) return null;
30
+ // Most recent by started_at. A null started_at sorts last rather than
31
+ // throwing, so a half-filled row cannot decide the answer (capture.js's
32
+ // original behaviour, kept deliberately).
33
+ building.sort((a, b) => {
34
+ const ta = a.started_at ? new Date(a.started_at).getTime() : 0;
35
+ const tb = b.started_at ? new Date(b.started_at).getTime() : 0;
36
+ return tb - ta;
37
+ });
38
+ return building[0].id;
39
+ }
40
+
41
+ async function resolveDefaultVersion(deps = {}) {
42
+ const client = deps.cliClient || require('./cli-lib').cliClient;
43
+ const api = await client();
44
+ const r = await api.versions.getVersions();
45
+ if (!r.ok) return null;
46
+ return pickDefaultVersion(r.data?.versions ?? []);
47
+ }
48
+
49
+ // A version id is interpolated by bash callers into a quoted Python string, so
50
+ // the ONE place that can guarantee it is safe is here, at the source, rather than
51
+ // four escaping fixes downstream. Ids are `[A-Za-z][A-Za-z0-9._-]*` in practice;
52
+ // anything else is refused rather than printed. Defense in depth: writing a
53
+ // versions.id is already a privileged operation.
54
+ const SAFE_ID = /^[A-Za-z][A-Za-z0-9._-]*$/;
55
+
56
+ async function main() {
57
+ const id = await resolveDefaultVersion();
58
+ if (!id) {
59
+ // Exit non-zero rather than printing something a caller would interpolate:
60
+ // a smoke that creates a task against the empty string is worse than one
61
+ // that stops and says why.
62
+ console.error('version-select: no version is currently `building` — cannot resolve a default.');
63
+ console.error(' Open one, or pass an explicit --version to the caller.');
64
+ return cliExit(1);
65
+ }
66
+ if (!SAFE_ID.test(id)) {
67
+ console.error(`version-select: refusing to emit ${JSON.stringify(id)} — not a plain version id.`);
68
+ console.error(' Callers splice this into a quoted string; an id with a quote or backslash could break out.');
69
+ return cliExit(1);
70
+ }
71
+ process.stdout.write(id);
72
+ return cliExit(0);
73
+ }
74
+
75
+ if (require.main === module) {
76
+ main().catch((err) => { console.error(`version-select: ${err && err.message}`); cliExit(1); });
77
+ }
78
+
79
+ module.exports = { pickDefaultVersion, resolveDefaultVersion, SAFE_ID };
package/src/module-api.js CHANGED
@@ -1073,8 +1073,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
1073
1073
  // landed since 1.19.321 with no explicit bump. run 32679604360. (task 1002620)
1074
1074
  // 1.19.323 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1075
1075
  // landed since 1.19.322 with no explicit bump. run 32680138776. (task 1002620)
1076
+ // 1.19.324 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1077
+ // landed since 1.19.323 with no explicit bump. run 32737868129. (task 1002620)
1076
1078
  // ---------------------------------------------------------------------------
1077
- const CORE_VERSION = '1.19.323'; // CI auto-patch carrier (ADR 0161); see changelog above
1079
+ const CORE_VERSION = '1.19.324'; // CI auto-patch carrier (ADR 0161); see changelog above
1078
1080
 
1079
1081
  // A namespaced logger so a module's log lines are attributable + consistent.
1080
1082
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,201 @@
1
+ // tests/version_literals.mjs — no ops script hardcodes a version id
2
+ // (task 1003166 / G02, goal 1000077, criterion guards-no-hardcoded-version).
3
+ //
4
+ // THE INCIDENT: smoke-write.sh named 'PMS-V1'. That version shipped, so
5
+ // version_closed refused every create and the write-path smoke could not pass —
6
+ // while /merge-mode Step 5 still said "4/4 expected". FOUR of the versions the
7
+ // nine offending scripts named had already shipped when this was written.
8
+ //
9
+ // The tests care as much about what the guard must NOT flag as what it must: a
10
+ // scan that fires on a comment mentioning an old version would be the over-broad
11
+ // source assertion this repo keeps relearning (learning 1000196).
12
+ import { strict as assert } from 'node:assert';
13
+ import { test } from 'node:test';
14
+ import { createRequire } from 'node:module';
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ const require = createRequire(import.meta.url);
20
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
21
+ const g = require(path.join(ROOT, 'scripts', 'gds', 'version-literal-guard.js'));
22
+ const { pickDefaultVersion } = require(path.join(ROOT, 'scripts', 'gds', 'version-select.js'));
23
+ const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), 'utf8');
24
+ const scan = (files) => g.scanScripts(Object.keys(files), (f) => files[f]);
25
+
26
+ // ---- the selection rule ---------------------------------------------------
27
+ test('the newest BUILDING version wins; shipped ones are never chosen', () => {
28
+ const vs = [
29
+ { id: 'PMS-V1', status: 'shipped', started_at: '2026-05-09' },
30
+ { id: 'GDS-V4', status: 'shipped', started_at: '2026-05-30' },
31
+ { id: 'BONGOS-V1', status: 'building', started_at: '2026-06-20' },
32
+ { id: 'CB-V1', status: 'building', started_at: '2026-06-21' },
33
+ ];
34
+ assert.equal(pickDefaultVersion(vs), 'CB-V1');
35
+ });
36
+
37
+ test('no building version yields NULL, never a guess', () => {
38
+ // The caller must stop and say so; interpolating '' into a create is worse.
39
+ assert.equal(pickDefaultVersion([{ id: 'X', status: 'shipped', started_at: '2026-01-01' }]), null);
40
+ assert.equal(pickDefaultVersion([]), null);
41
+ assert.equal(pickDefaultVersion(null), null);
42
+ });
43
+
44
+ test('a null started_at cannot decide the answer', () => {
45
+ const vs = [
46
+ { id: 'OLD', status: 'building', started_at: null },
47
+ { id: 'NEW', status: 'building', started_at: '2026-06-21' },
48
+ ];
49
+ assert.equal(pickDefaultVersion(vs), 'NEW', 'a half-filled row sorts last, it does not win');
50
+ });
51
+
52
+ // ---- what the guard MUST catch -------------------------------------------
53
+ test('THE INCIDENT: a version field assigned a literal is caught', () => {
54
+ const r = scan({ 'scripts/gds/smoke.sh': " 'version_id': 'PMS-V1'," });
55
+ assert.equal(r.violations.length, 1);
56
+ assert.match(r.violations[0], /RESOLVED/);
57
+ assert.match(r.violations[0], /version-select/, 'and it names the remedy');
58
+ });
59
+
60
+ test('a --version default is caught', () => {
61
+ const r = scan({ 'scripts/gds/a.js': " const v = arg('--version', args) || 'GDS-V3';" });
62
+ assert.equal(r.violations.length, 1);
63
+ });
64
+
65
+ test('a DEFAULT_VERSION constant is caught', () => {
66
+ const r = scan({ 'scripts/gds/b.js': "const DEFAULT_VERSION = process.env.X || 'BONGOS-V1';" });
67
+ assert.equal(r.violations.length, 1);
68
+ });
69
+
70
+ test('an unknown future prefix is caught too', () => {
71
+ // Enumerating PMS/GDS/BONGOS/CB would let the next scheme through.
72
+ const r = scan({ 'scripts/gds/c.js': " version_id: 'ACME-V7'," });
73
+ assert.equal(r.violations.length, 1, 'the shape is matched, not a fixed list');
74
+ });
75
+
76
+ // ---- what the guard must NOT catch ---------------------------------------
77
+ test('a COMMENT naming an old version is not a violation', () => {
78
+ // smoke-claim-consistency.sh legitimately says "PMS-V2 P6 not live yet".
79
+ const r = scan({
80
+ 'scripts/gds/d.sh': '# TODO: once PMS-V2 P6 ships, drop this',
81
+ 'scripts/gds/e.js': "// version_id: 'GDS-V3' used to live here",
82
+ });
83
+ assert.deepEqual(r.violations, []);
84
+ });
85
+
86
+ test('prose printed to the operator is not a violation', () => {
87
+ const r = scan({ 'scripts/gds/f.sh': ' echo "SKIP — PMS-V2 P6 not live yet"' });
88
+ assert.deepEqual(r.violations, []);
89
+ });
90
+
91
+ test('a RESOLVED assignment is not a violation', () => {
92
+ const r = scan({ 'scripts/gds/g.js': ' version_id: await resolveDefaultVersion(),' });
93
+ assert.deepEqual(r.violations, []);
94
+ });
95
+
96
+ test('seeders and tests are exempt BY PATH, not by allowlist', () => {
97
+ // A seeder's whole subject IS a historical version; asking it to resolve one
98
+ // at runtime would break it.
99
+ for (const p of ['scripts/gds/seed-v3-tasks.js', 'scripts/gds/newcomer-floor.test.js', 'scripts/hall-preview/fixtures/x.js']) {
100
+ assert.equal(g.isExempt(p), true, `${p} must be exempt`);
101
+ }
102
+ assert.equal(g.isExempt('scripts/gds/smoke-write.sh'), false, 'an ops script is not exempt');
103
+ });
104
+
105
+ // ---- the live tree --------------------------------------------------------
106
+ test('the live tree passes', () => {
107
+ const r = g.checkNoVersionLiterals();
108
+ assert.equal(r.hardFail, false, `unexpected:\n ${r.violations.join('\n ')}`);
109
+ });
110
+
111
+ test('an empty enumeration FAILS rather than reporting all-clear', () => {
112
+ const r = g.checkNoVersionLiterals({ files: [] });
113
+ assert.equal(r.hardFail, true);
114
+ assert.match(r.violations[0], /scan defect/);
115
+ });
116
+
117
+ test('every allowlist entry carries a real justification', () => {
118
+ assert.ok(g.VERSION_LITERAL_ALLOW.size > 0);
119
+ for (const [k, why] of g.VERSION_LITERAL_ALLOW) {
120
+ assert.match(k, /^scripts\//, `${k} is a script path`);
121
+ assert.ok(String(why).length > 60, `${k} needs a reason, not a label`);
122
+ }
123
+ });
124
+
125
+ // ---- the reported bug is actually fixed ----------------------------------
126
+ test('smoke-write.sh resolves its version instead of naming one', () => {
127
+ const sh = read('scripts', 'gds', 'smoke-write.sh');
128
+ assert.ok(!/'version_id': '[A-Z]/.test(sh), 'no literal remains');
129
+ // The invocation is `$(node "$(dirname "$0")/version-select.js")`, so the
130
+ // closing paren is preceded by a quote AND the inner $(dirname "$0") contains
131
+ // its own ')', so neither `.js)` nor [^)]* matches. Match to end-of-line.
132
+ assert.match(sh, /VERSION_ID=\$\(node[^\n]*version-select\.js/, 'it resolves at runtime');
133
+ assert.match(sh, /exit 1/, 'and stops if it cannot');
134
+ });
135
+
136
+ test('all four bash smokes resolve, and none names a version', () => {
137
+ for (const f of ['smoke-write.sh', 'smoke-claim-consistency.sh', 'smoke-batch-release.sh', 'smoke-batch-claim.sh']) {
138
+ const sh = read('scripts', 'gds', f);
139
+ assert.match(sh, /version-select\.js/, `${f} resolves`);
140
+ assert.match(sh, /'version_id': '\$\{VERSION_ID\}'/, `${f} uses the resolved id`);
141
+ }
142
+ });
143
+
144
+ test('capture.js no longer keeps its own copy of the rule', () => {
145
+ // It was the precedent; now it is a caller, so there is ONE implementation.
146
+ const src = read('scripts', 'gds', 'capture.js');
147
+ assert.match(src, /require\('\.\/version-select\.js'\)/);
148
+ assert.ok(!/async function resolveDefaultVersion/.test(src), 'the private copy is gone');
149
+ });
150
+
151
+ // ---- the four grader findings from the first pass -------------------------
152
+ test('an unsafe version id is REFUSED, not printed', () => {
153
+ // Bash callers splice this into a quoted Python string, so the one place that
154
+ // can guarantee safety is the source. Ids with a quote or backslash could
155
+ // otherwise break out of the literal (the grader's defense-in-depth finding).
156
+ const { SAFE_ID } = require(path.join(ROOT, 'scripts', 'gds', 'version-select.js'));
157
+ for (const bad of ["CB-V1'; import os", 'CB\\V1', "'", '', '1-V1', 'a b']) {
158
+ assert.equal(SAFE_ID.test(bad), false, `${JSON.stringify(bad)} must be refused`);
159
+ }
160
+ for (const good of ['CB-V1', 'BONGOS-V1', 'GDS-V3', 'v2.1_beta']) {
161
+ assert.equal(SAFE_ID.test(good), true, `${good} must be accepted`);
162
+ }
163
+ });
164
+
165
+ test('session-eval resolves the version ONCE, not per finding', () => {
166
+ // It was called inside `for (const s of systemic)` — a network round trip per
167
+ // finding, for a value invariant across the run.
168
+ const src = read('scripts', 'gds', 'session-eval.js');
169
+ // Anchored on the CALL COUNT rather than on a loop position: the file has TWO
170
+ // `for (const s of systemic)` loops, so an indexOf-based test picked the wrong
171
+ // one and passed vacuously. Exactly one call site is the real invariant.
172
+ const calls = src.match(/await resolveDefaultVersion\(\)/g) || [];
173
+ assert.equal(calls.length, 1, `resolved once per run, not per finding (found ${calls.length})`);
174
+ assert.match(src, /const suggestedVersion = await resolveDefaultVersion\(\)/, 'hoisted to a const');
175
+ assert.match(src, /suggested_version: suggestedVersion,/, 'and the filing uses it');
176
+ });
177
+
178
+ test('version-select exits through cliExit, not process.exit', () => {
179
+ // CLAUDE.md: cliExit is the always-correct move for handle closure.
180
+ const src = read('scripts', 'gds', 'version-select.js');
181
+ assert.match(src, /require\('\.\/cli-lib'\)/);
182
+ assert.ok(!/process\.exit\(/.test(src), 'no raw process.exit remains');
183
+ });
184
+
185
+ test('ship-flow.js imports every cross-module function it calls', () => {
186
+ // task 1003205's carve left pushVia() called in regradeMain but never imported,
187
+ // so EVERY resume of a task at 'completed' died with ReferenceError before the
188
+ // claim resolved — found by hitting it while shipping this task. Guarding the
189
+ // one name is not enough; assert the import exists for anything it calls from
190
+ // its sibling deploy-target module.
191
+ const src = read('scripts', 'gds', 'ship-flow.js');
192
+ const deployTargetExports = ['pushVia', 'resolveDeployMode', 'mainIsProtected', 'resolveDeployTarget'];
193
+ const imported = /require\('\.\/ship-deploy-target\.js'\)/.test(src)
194
+ ? (src.match(/const \{([^}]*)\} = require\('\.\/ship-deploy-target\.js'\)/) || [, ''])[1]
195
+ : '';
196
+ for (const name of deployTargetExports) {
197
+ if (new RegExp(`(?<![\\w.])${name}\\(`).test(src)) {
198
+ assert.ok(imported.includes(name), `ship-flow.js calls ${name}() but does not import it`);
199
+ }
200
+ }
201
+ });