@bongos/core 1.19.768 → 1.19.770
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 +33 -18
- package/docs/module-api-changelog.md +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/build-cli-package.js +5 -0
- package/scripts/gds/claim.js +15 -0
- package/scripts/gds/ship.js +15 -0
- package/scripts/gds/start.js +15 -0
- package/scripts/gds/tree-preflight.js +241 -0
- package/src/module-api.js +1 -1
- package/tests/help_requests.mjs +2 -4
- package/tests/helpers.mjs +36 -0
- package/tests/sql_column_lines.mjs +87 -0
- package/tests/task_recommendations.mjs +2 -4
- package/tests/tree_preflight.mjs +243 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.770",
|
|
6
|
+
"core_contract": "1.19.770",
|
|
7
|
+
"source_commit": "6860da8b241f99c69f179a82908b848215eaf46d",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-16T15:11:28.421Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 483,
|
|
13
13
|
"agent_docs_stubbed": 25,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2126,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2634,
|
|
20
|
+
"tree_sha256": "d7c0492f1f78fb62ba8de96723bfc2b4caffb4083a4e98d39c23fab5de7c6270",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2512,7 +2512,7 @@
|
|
|
2512
2512
|
{
|
|
2513
2513
|
"path": "docs/module-api-changelog.md",
|
|
2514
2514
|
"mode": "0000644",
|
|
2515
|
-
"sha256": "
|
|
2515
|
+
"sha256": "6c9191a42934b1fc862c9075812ead3451eacd71278bdcfb652c01b02ed7e083"
|
|
2516
2516
|
},
|
|
2517
2517
|
{
|
|
2518
2518
|
"path": "docs/modules-contract.md",
|
|
@@ -7577,12 +7577,12 @@
|
|
|
7577
7577
|
{
|
|
7578
7578
|
"path": "package-lock.json",
|
|
7579
7579
|
"mode": "0000644",
|
|
7580
|
-
"sha256": "
|
|
7580
|
+
"sha256": "43473b034badae11d9d613079d8ea502102bd9349a5b601c89ec4d094f0d21bf"
|
|
7581
7581
|
},
|
|
7582
7582
|
{
|
|
7583
7583
|
"path": "package.json",
|
|
7584
7584
|
"mode": "0000644",
|
|
7585
|
-
"sha256": "
|
|
7585
|
+
"sha256": "ca671dc476d503ca56328e84fa7f0e3712b60657cfbfd84f1790be4bc93a5b1f"
|
|
7586
7586
|
},
|
|
7587
7587
|
{
|
|
7588
7588
|
"path": "public-docs/index.html",
|
|
@@ -7812,7 +7812,7 @@
|
|
|
7812
7812
|
{
|
|
7813
7813
|
"path": "scripts/gds/build-cli-package.js",
|
|
7814
7814
|
"mode": "0000644",
|
|
7815
|
-
"sha256": "
|
|
7815
|
+
"sha256": "f3c15da424b2652ecb511d9a794cda1c037d500b8d810fb71bab2e31f41a0ca6"
|
|
7816
7816
|
},
|
|
7817
7817
|
{
|
|
7818
7818
|
"path": "scripts/gds/bump-version.js",
|
|
@@ -7847,7 +7847,7 @@
|
|
|
7847
7847
|
{
|
|
7848
7848
|
"path": "scripts/gds/claim.js",
|
|
7849
7849
|
"mode": "0000644",
|
|
7850
|
-
"sha256": "
|
|
7850
|
+
"sha256": "2d225377a6ab3bdc8e8cb195d5e6588517275179fe49100c42362c88a3465324"
|
|
7851
7851
|
},
|
|
7852
7852
|
{
|
|
7853
7853
|
"path": "scripts/gds/classify-discipline.js",
|
|
@@ -8847,7 +8847,7 @@
|
|
|
8847
8847
|
{
|
|
8848
8848
|
"path": "scripts/gds/ship.js",
|
|
8849
8849
|
"mode": "0000644",
|
|
8850
|
-
"sha256": "
|
|
8850
|
+
"sha256": "9c279a8852e46758e498334fbeb25e94be2895930a6c65b400c2c1baaac1c270"
|
|
8851
8851
|
},
|
|
8852
8852
|
{
|
|
8853
8853
|
"path": "scripts/gds/skill-lint.js",
|
|
@@ -8912,7 +8912,7 @@
|
|
|
8912
8912
|
{
|
|
8913
8913
|
"path": "scripts/gds/start.js",
|
|
8914
8914
|
"mode": "0000644",
|
|
8915
|
-
"sha256": "
|
|
8915
|
+
"sha256": "8cf839a0db44d7b647cdd0ea394bdd31db924e54d4840a5f407f515c098c9de7"
|
|
8916
8916
|
},
|
|
8917
8917
|
{
|
|
8918
8918
|
"path": "scripts/gds/status.js",
|
|
@@ -8939,6 +8939,11 @@
|
|
|
8939
8939
|
"mode": "0000644",
|
|
8940
8940
|
"sha256": "d3e542cf977f9aadad8f856defa6d809f3604e184cd261fbee7738aa1da736ea"
|
|
8941
8941
|
},
|
|
8942
|
+
{
|
|
8943
|
+
"path": "scripts/gds/tree-preflight.js",
|
|
8944
|
+
"mode": "0000644",
|
|
8945
|
+
"sha256": "0655b96b842ba4073fbc72ca76204591b3339cc649f2f76986816ddfbe88ff6d"
|
|
8946
|
+
},
|
|
8942
8947
|
{
|
|
8943
8948
|
"path": "scripts/gds/triage.js",
|
|
8944
8949
|
"mode": "0000644",
|
|
@@ -9402,7 +9407,7 @@
|
|
|
9402
9407
|
{
|
|
9403
9408
|
"path": "src/module-api.js",
|
|
9404
9409
|
"mode": "0000644",
|
|
9405
|
-
"sha256": "
|
|
9410
|
+
"sha256": "a4707be144d7581356575936d364de82a6b1dabd5310dc6df70de1954282deb4"
|
|
9406
9411
|
},
|
|
9407
9412
|
{
|
|
9408
9413
|
"path": "src/module-loader/catalog.js",
|
|
@@ -11162,12 +11167,12 @@
|
|
|
11162
11167
|
{
|
|
11163
11168
|
"path": "tests/help_requests.mjs",
|
|
11164
11169
|
"mode": "0000644",
|
|
11165
|
-
"sha256": "
|
|
11170
|
+
"sha256": "e1e1075e2f8ba4b52acba29804e5fd62443b823cfd15d4c50f0a93fcea0d13bd"
|
|
11166
11171
|
},
|
|
11167
11172
|
{
|
|
11168
11173
|
"path": "tests/helpers.mjs",
|
|
11169
11174
|
"mode": "0000644",
|
|
11170
|
-
"sha256": "
|
|
11175
|
+
"sha256": "2527e761e86d289bfaf1f0c5bc416762b02c0784f0307fd0f445da656d8db549"
|
|
11171
11176
|
},
|
|
11172
11177
|
{
|
|
11173
11178
|
"path": "tests/hierarchy-config.mjs",
|
|
@@ -12779,6 +12784,11 @@
|
|
|
12779
12784
|
"mode": "0000644",
|
|
12780
12785
|
"sha256": "905b7b9147fa35f74c2c5f4118175ebf96fa36107379207a7a2c59afc5a2967d"
|
|
12781
12786
|
},
|
|
12787
|
+
{
|
|
12788
|
+
"path": "tests/sql_column_lines.mjs",
|
|
12789
|
+
"mode": "0000644",
|
|
12790
|
+
"sha256": "c68bf3aa9cd886de435293f0a536df8153fcc99187c48097de12c5482b1ee430"
|
|
12791
|
+
},
|
|
12782
12792
|
{
|
|
12783
12793
|
"path": "tests/sso_checkin_identity_spoof.mjs",
|
|
12784
12794
|
"mode": "0000644",
|
|
@@ -12902,7 +12912,7 @@
|
|
|
12902
12912
|
{
|
|
12903
12913
|
"path": "tests/task_recommendations.mjs",
|
|
12904
12914
|
"mode": "0000644",
|
|
12905
|
-
"sha256": "
|
|
12915
|
+
"sha256": "1c5d35d58371cbde290a3e71e023febeebf350b54031d458c2ebe58f25962aa4"
|
|
12906
12916
|
},
|
|
12907
12917
|
{
|
|
12908
12918
|
"path": "tests/task_visual_route_gate.mjs",
|
|
@@ -12979,6 +12989,11 @@
|
|
|
12979
12989
|
"mode": "0000644",
|
|
12980
12990
|
"sha256": "61b50103967187dbff82f957d2faf6cc9ca5079b83531a5925fdeae61eb3c921"
|
|
12981
12991
|
},
|
|
12992
|
+
{
|
|
12993
|
+
"path": "tests/tree_preflight.mjs",
|
|
12994
|
+
"mode": "0000644",
|
|
12995
|
+
"sha256": "d8382f7a608fc7511cd6e00e3a9efcb0283b8128b39cbe229fb07ae590b20c4b"
|
|
12996
|
+
},
|
|
12982
12997
|
{
|
|
12983
12998
|
"path": "tests/triage_development.mjs",
|
|
12984
12999
|
"mode": "0000644",
|
|
@@ -2010,5 +2010,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2010
2010
|
landed since 1.19.766 with no explicit bump. run 35109111074. (task 1002620)
|
|
2011
2011
|
1.19.768 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2012
2012
|
landed since 1.19.767 with no explicit bump. run 35110800163. (task 1002620)
|
|
2013
|
+
1.19.769 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2014
|
+
landed since 1.19.768 with no explicit bump. run 35111616379. (task 1002620)
|
|
2015
|
+
1.19.770 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2016
|
+
landed since 1.19.769 with no explicit bump. run 35113578542. (task 1002620)
|
|
2013
2017
|
---------------------------------------------------------------------------
|
|
2014
2018
|
```
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.770",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.770",
|
|
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.
|
|
3
|
+
"version": "1.19.770",
|
|
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",
|
|
@@ -56,6 +56,11 @@ const FILES = [
|
|
|
56
56
|
'scripts/gds/install-git-hooks.js', // lazy dep of setup.js
|
|
57
57
|
'scripts/gds/preflight.js',
|
|
58
58
|
'scripts/gds/surface.js', // who the output is for: agent vs terminal (task 1003680)
|
|
59
|
+
// The partial-checkout preflight (task 1003098). start/claim/ship require it
|
|
60
|
+
// ABOVE their first module require, so an absent file here breaks every verb in
|
|
61
|
+
// the published package -- which is exactly how tests/cli_package.mjs caught it.
|
|
62
|
+
// Pure: node builtins only, no repo, no network.
|
|
63
|
+
'scripts/gds/tree-preflight.js',
|
|
59
64
|
|
|
60
65
|
// Read the board and move your own work along.
|
|
61
66
|
'scripts/gds/start.js',
|
package/scripts/gds/claim.js
CHANGED
|
@@ -7,6 +7,21 @@
|
|
|
7
7
|
// On success: prints the claim id, the task title, and the touches[] so the
|
|
8
8
|
// builder knows the parallel-safety boundary they're working within.
|
|
9
9
|
|
|
10
|
+
// TREE PREFLIGHT (task 1003098). A task-scoped dev box carries scripts/ and
|
|
11
|
+
// src/bongos/ but not every modules/ directory, so the requires below can fail —
|
|
12
|
+
// and Node's loader names only the FIRST missing one, which turned recovery into
|
|
13
|
+
// guess-read-fix-repeat. This enumerates every missing requirement in one message
|
|
14
|
+
// and exits before the first such require runs. It must stay ABOVE them.
|
|
15
|
+
//
|
|
16
|
+
// A PRE-REQUEST hard exit, like the usage banner and worktree guard: nothing has
|
|
17
|
+
// been sent yet, so there is no in-flight request for cliExit to drain.
|
|
18
|
+
const { action: _surfaceAction } = require('./surface.js');
|
|
19
|
+
const { checkTree: _checkTree } = require('./tree-preflight.js');
|
|
20
|
+
{
|
|
21
|
+
const _fault = _checkTree(__filename, { command: _surfaceAction("claim") });
|
|
22
|
+
if (_fault) { console.error(_fault); process.exit(1); }
|
|
23
|
+
}
|
|
24
|
+
|
|
10
25
|
const { execSync } = require('node:child_process');
|
|
11
26
|
const fs = require('node:fs');
|
|
12
27
|
const path = require('node:path');
|
package/scripts/gds/ship.js
CHANGED
|
@@ -77,6 +77,21 @@
|
|
|
77
77
|
|
|
78
78
|
'use strict';
|
|
79
79
|
|
|
80
|
+
// TREE PREFLIGHT (task 1003098). A task-scoped dev box carries scripts/ and
|
|
81
|
+
// src/bongos/ but not every modules/ directory, so the requires below can fail —
|
|
82
|
+
// and Node's loader names only the FIRST missing one, which turned recovery into
|
|
83
|
+
// guess-read-fix-repeat. This enumerates every missing requirement in one message
|
|
84
|
+
// and exits before the first such require runs. It must stay ABOVE them.
|
|
85
|
+
//
|
|
86
|
+
// A PRE-REQUEST hard exit, like the usage banner and worktree guard: nothing has
|
|
87
|
+
// been sent yet, so there is no in-flight request for cliExit to drain.
|
|
88
|
+
const { action: _surfaceAction } = require('./surface.js');
|
|
89
|
+
const { checkTree: _checkTree } = require('./tree-preflight.js');
|
|
90
|
+
{
|
|
91
|
+
const _fault = _checkTree(__filename, { command: _surfaceAction("ship") });
|
|
92
|
+
if (_fault) { console.error(_fault); process.exit(1); }
|
|
93
|
+
}
|
|
94
|
+
|
|
80
95
|
const { buildMergeArgv, gitArgvOk, sanitizeGitHubRefs } = require('./ship-git.js');
|
|
81
96
|
const { apiErrorLine, normalizeApiError, postLandShipOutcome } = require('./ship-io.js');
|
|
82
97
|
const { deriveTagsFromPaths } = require('./ship-surprise.js');
|
package/scripts/gds/start.js
CHANGED
|
@@ -52,6 +52,21 @@
|
|
|
52
52
|
//
|
|
53
53
|
// Returns JSON if --json is passed (for the slash command to parse cleanly).
|
|
54
54
|
|
|
55
|
+
// TREE PREFLIGHT (task 1003098). A task-scoped dev box carries scripts/ and
|
|
56
|
+
// src/bongos/ but not every modules/ directory, so the requires below can fail —
|
|
57
|
+
// and Node's loader names only the FIRST missing one, which turned recovery into
|
|
58
|
+
// guess-read-fix-repeat. This enumerates every missing requirement in one message
|
|
59
|
+
// and exits before the first such require runs. It must stay ABOVE them.
|
|
60
|
+
//
|
|
61
|
+
// A PRE-REQUEST hard exit, like the usage banner and worktree guard: nothing has
|
|
62
|
+
// been sent yet, so there is no in-flight request for cliExit to drain.
|
|
63
|
+
const { action: _surfaceAction } = require('./surface.js');
|
|
64
|
+
const { checkTree: _checkTree } = require('./tree-preflight.js');
|
|
65
|
+
{
|
|
66
|
+
const _fault = _checkTree(__filename, { command: _surfaceAction("start") });
|
|
67
|
+
if (_fault) { console.error(_fault); process.exit(1); }
|
|
68
|
+
}
|
|
69
|
+
|
|
55
70
|
const { cliClient, requireSession, fmtUsd, printOnboardingNudge, printCostWarning, printNeedsNudge, hallBase, API_BASE, rewardPolicy, costPlusOnlyNote, isDispatcherIncompatError } = require('./cli-lib');
|
|
56
71
|
const { contextPack, formatContextPackCompact } = require('./context-pack');
|
|
57
72
|
const { depsFinding } = require('./preflight');
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// scripts/gds/tree-preflight.js — does this checkout contain the code the command
|
|
4
|
+
// is about to require? (task 1003098)
|
|
5
|
+
//
|
|
6
|
+
// THE FAILURE THIS REPLACES. On a TASK-SCOPED dev box — a supported, server-driven
|
|
7
|
+
// configuration — `GET /box/source-access` returns a sparse path list that carries
|
|
8
|
+
// `scripts`, `src/bongos`, `tests`, `clients`, `config` and NOT `modules/`, except
|
|
9
|
+
// the one module the claimed task is scoped to. So the platform itself produces a
|
|
10
|
+
// checkout in which its own entry-point commands cannot run, and they said so like
|
|
11
|
+
// this:
|
|
12
|
+
//
|
|
13
|
+
// $ node scripts/gds/ship.js
|
|
14
|
+
// Error: Cannot find module '../../modules/lifecycle/ship-preflight'
|
|
15
|
+
// Require stack: scripts/gds/ship-state.js
|
|
16
|
+
//
|
|
17
|
+
// A raw stack trace, naming ONE missing thing. Fix that one and the next run names
|
|
18
|
+
// the next — the filer lost a whole session to guess-read-fix-repeat, and their
|
|
19
|
+
// workaround (`git sparse-checkout disable` plus commenting out the re-scope cron)
|
|
20
|
+
// is not something a builder should have to discover.
|
|
21
|
+
//
|
|
22
|
+
// ENUMERATION IS THE WHOLE POINT. Node's loader is lazy by construction: it fails
|
|
23
|
+
// at the first unresolvable specifier and cannot tell you about the second, because
|
|
24
|
+
// it never got there. That is correct behaviour for a module loader and useless as
|
|
25
|
+
// a diagnosis. This runs BEFORE the first such require and reports every missing
|
|
26
|
+
// requirement it can see in ONE message.
|
|
27
|
+
//
|
|
28
|
+
// THE REQUIRED LIST IS COMPUTED, NEVER DECLARED. A hand-kept list of "what ship.js
|
|
29
|
+
// needs" is a second copy of the require graph, and it would be wrong within a
|
|
30
|
+
// month — ship.js alone fans out across sixteen ship-*.js phase files. So the
|
|
31
|
+
// preflight WALKS the graph the same way the loader would: read the entry file,
|
|
32
|
+
// find its `require('./x')` siblings, follow them, and collect every
|
|
33
|
+
// `require('../../modules/...')` it passes. Pure string work over the file system;
|
|
34
|
+
// nothing is executed, so a missing module cannot throw during the check that
|
|
35
|
+
// exists to report it.
|
|
36
|
+
//
|
|
37
|
+
// WHY IT DOES NOT SIMPLY TRY/CATCH EACH REQUIRE. Individual guards are what the
|
|
38
|
+
// codebase already does where a module is genuinely OPTIONAL (context-pack.js's
|
|
39
|
+
// classifier, cli-lib.js's reward policy) and they are right for that: the command
|
|
40
|
+
// still works, slightly diminished. These requires are not optional — ship.js
|
|
41
|
+
// cannot ship without `modules/lifecycle/ship-preflight`. Wrapping them would turn
|
|
42
|
+
// a loud failure into a confusing one.
|
|
43
|
+
//
|
|
44
|
+
// PURE + INJECTED (`readFile`, `exists`) so the whole thing is testable with a fake
|
|
45
|
+
// file system and no box — which is the layer task 1003098 asks for the test at.
|
|
46
|
+
|
|
47
|
+
const fsMod = require('node:fs');
|
|
48
|
+
const pathMod = require('node:path');
|
|
49
|
+
|
|
50
|
+
const REPO_ROOT = pathMod.resolve(__dirname, '..', '..');
|
|
51
|
+
|
|
52
|
+
// A require of this repo's OWN module code, which is what a scoped checkout drops.
|
|
53
|
+
// Matches the two spellings the CLI uses; the quote style is captured so a require
|
|
54
|
+
// written with double quotes is not silently invisible to the walk.
|
|
55
|
+
const MODULE_REQUIRE_RE = /require\(\s*['"](\.\.\/\.\.\/modules\/[^'"]+)['"]\s*\)/g;
|
|
56
|
+
// A sibling require inside scripts/gds/ — the edges the walk follows.
|
|
57
|
+
const LOCAL_REQUIRE_RE = /require\(\s*['"](\.\/[^'"]+)['"]\s*\)/g;
|
|
58
|
+
|
|
59
|
+
// Comments are stripped before scanning. A header that QUOTES a require to explain
|
|
60
|
+
// why it was removed would otherwise be walked as a live edge — the defect filed as
|
|
61
|
+
// task 1003832 against a test that scanned raw source, and one this file would hit
|
|
62
|
+
// immediately, because its own header above quotes the exact failing require.
|
|
63
|
+
function stripComments(src) {
|
|
64
|
+
return String(src)
|
|
65
|
+
.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
66
|
+
.replace(/^\s*\/\/.*$/gm, ' ');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Is the character at `index` inside a try{} block?
|
|
70
|
+
//
|
|
71
|
+
// THE DISTINCTION THIS DRAWS IS THE WHOLE CORRECTNESS OF THE PREFLIGHT. Several of
|
|
72
|
+
// the CLI's module requires are deliberately guarded — context-pack.js's classifier
|
|
73
|
+
// and cli-lib.js's reward policy both sit in a try and fall back honestly, so those
|
|
74
|
+
// commands RUN on a scoped box. Reporting them as missing requirements would refuse
|
|
75
|
+
// a command that works, which is a worse failure than the one being fixed: a builder
|
|
76
|
+
// who could have shipped is told they cannot.
|
|
77
|
+
//
|
|
78
|
+
// A brace walk, not a parser — this repo ships none (the route-rank-check /
|
|
79
|
+
// exec-path-guard precedent). It tracks a stack of block kinds and answers whether
|
|
80
|
+
// any enclosing block was opened by `try`. Comments are already stripped by the
|
|
81
|
+
// caller; string and regex literals containing braces are the known blind spot, and
|
|
82
|
+
// the failure direction is benign: a miscount can only mis-scope a block, and the
|
|
83
|
+
// classification is proved against the real files by tests/tree_preflight.mjs, which
|
|
84
|
+
// pins that start.js's requires read optional and ship.js's read required.
|
|
85
|
+
function insideTry(src, index) {
|
|
86
|
+
const stack = [];
|
|
87
|
+
for (let i = 0; i < index && i < src.length; i++) {
|
|
88
|
+
const c = src[i];
|
|
89
|
+
if (c === '{') {
|
|
90
|
+
// Look back past whitespace for the keyword that opened this block.
|
|
91
|
+
const before = src.slice(Math.max(0, i - 200), i);
|
|
92
|
+
stack.push(/\btry\s*$/.test(before) ? 'try' : 'block');
|
|
93
|
+
} else if (c === '}') {
|
|
94
|
+
stack.pop();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return stack.includes('try');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function matchAll(src, re) {
|
|
101
|
+
const out = [];
|
|
102
|
+
let m;
|
|
103
|
+
re.lastIndex = 0;
|
|
104
|
+
while ((m = re.exec(src)) !== null) out.push({ spec: m[1], index: m.index });
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Resolve a relative specifier the way require would, to a repo-relative posix path.
|
|
109
|
+
function resolveRel(fromFile, spec, { root = REPO_ROOT } = {}) {
|
|
110
|
+
const abs = pathMod.resolve(pathMod.dirname(fromFile), spec);
|
|
111
|
+
const rel = pathMod.relative(root, abs).split(pathMod.sep).join('/');
|
|
112
|
+
return rel;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Add the `.js` a bare specifier omits. Only ever appended — a spec that already
|
|
116
|
+
// names an extension is left exactly as written, so a `.json` require still points
|
|
117
|
+
// at the file it names.
|
|
118
|
+
function withJsExt(rel) {
|
|
119
|
+
return /\.[a-z]+$/i.test(rel) ? rel : `${rel}.js`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Every module-code path the entry point's require graph reaches. PURE.
|
|
124
|
+
*
|
|
125
|
+
* Follows `./sibling` edges inside scripts/gds/ transitively and collects the
|
|
126
|
+
* `../../modules/...` leaves. A file it cannot read is skipped rather than thrown
|
|
127
|
+
* on: this runs on a checkout that is by definition incomplete, and a preflight
|
|
128
|
+
* that crashes while diagnosing a partial tree is worse than the trace it replaces.
|
|
129
|
+
*
|
|
130
|
+
* @returns {string[]} repo-relative posix paths, deduped, stable order
|
|
131
|
+
*/
|
|
132
|
+
function requiredModulePaths(entryFile, { readFile, root = REPO_ROOT } = {}) {
|
|
133
|
+
const read = readFile || ((f) => fsMod.readFileSync(f, 'utf8'));
|
|
134
|
+
const seen = new Set();
|
|
135
|
+
const found = new Set();
|
|
136
|
+
const queue = [pathMod.resolve(entryFile)];
|
|
137
|
+
|
|
138
|
+
while (queue.length) {
|
|
139
|
+
const file = queue.shift();
|
|
140
|
+
if (seen.has(file)) continue;
|
|
141
|
+
seen.add(file);
|
|
142
|
+
let src;
|
|
143
|
+
try { src = stripComments(read(file)); } catch (_) { continue; }
|
|
144
|
+
|
|
145
|
+
for (const { spec, index } of matchAll(src, MODULE_REQUIRE_RE)) {
|
|
146
|
+
// A guarded require is not a requirement — see insideTry.
|
|
147
|
+
if (insideTry(src, index)) continue;
|
|
148
|
+
found.add(withJsExt(resolveRel(file, spec, { root })));
|
|
149
|
+
}
|
|
150
|
+
for (const { spec } of matchAll(src, LOCAL_REQUIRE_RE)) {
|
|
151
|
+
const abs = pathMod.resolve(pathMod.dirname(file), spec);
|
|
152
|
+
queue.push(/\.[a-z]+$/i.test(abs) ? abs : `${abs}.js`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return [...found].sort();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Which of those paths are not on disk. PURE. Returns repo-relative paths.
|
|
160
|
+
*/
|
|
161
|
+
function missingPaths(paths, { exists, root = REPO_ROOT } = {}) {
|
|
162
|
+
const has = exists || ((rel) => fsMod.existsSync(pathMod.join(root, rel)));
|
|
163
|
+
return paths.filter((rel) => !has(rel));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// The distinct top-level module directories a set of missing paths falls under —
|
|
167
|
+
// what the builder actually has to get back, and what box-sync speaks in.
|
|
168
|
+
function missingModuleDirs(paths) {
|
|
169
|
+
const dirs = new Set();
|
|
170
|
+
for (const p of paths) {
|
|
171
|
+
const m = String(p).match(/^modules\/([^/]+)\//);
|
|
172
|
+
if (m) dirs.add(`modules/${m[1]}`);
|
|
173
|
+
}
|
|
174
|
+
return [...dirs].sort();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* ONE actionable message, or null when the tree is complete. PURE.
|
|
179
|
+
*
|
|
180
|
+
* Names the CONDITION (a scoped checkout, not a broken install), EVERY missing
|
|
181
|
+
* requirement, and BOTH remedies — re-scope, or take the whole tree. Two are given
|
|
182
|
+
* because they solve different problems: box-sync re-reads the server's path list
|
|
183
|
+
* and is right when the claim moved, while sparse-checkout disable is the escape
|
|
184
|
+
* hatch when the scope itself is what is wrong.
|
|
185
|
+
*
|
|
186
|
+
* It does not promise which one applies, because that is exactly what this layer
|
|
187
|
+
* cannot know — it can see that the code is absent, not why.
|
|
188
|
+
*/
|
|
189
|
+
function preflightMessage(missing, { command = 'this command' } = {}) {
|
|
190
|
+
if (!missing || !missing.length) return null;
|
|
191
|
+
const dirs = missingModuleDirs(missing);
|
|
192
|
+
const lines = [
|
|
193
|
+
`${command} needs code this checkout does not contain.`,
|
|
194
|
+
'',
|
|
195
|
+
`Missing (${missing.length}):`,
|
|
196
|
+
...missing.map((p) => ` - ${p}`),
|
|
197
|
+
'',
|
|
198
|
+
'This is normal on a TASK-SCOPED dev box: the box carries scripts/, src/bongos/,',
|
|
199
|
+
'tests/ and the module your claimed task is scoped to — not every module. It is',
|
|
200
|
+
'not a broken install, and re-running will not fix it.',
|
|
201
|
+
'',
|
|
202
|
+
'Fix it with ONE of:',
|
|
203
|
+
' node scripts/gds/box-sync.js re-scope this box to what you hold now',
|
|
204
|
+
' git sparse-checkout disable take the whole tree instead',
|
|
205
|
+
];
|
|
206
|
+
if (dirs.length) {
|
|
207
|
+
lines.splice(lines.indexOf('') + 1, 0, `Whole directories absent: ${dirs.join(', ')}`);
|
|
208
|
+
}
|
|
209
|
+
return lines.join('\n');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The one call an entry point makes. Returns null when the tree is complete, or the
|
|
214
|
+
* message to print before exiting non-zero.
|
|
215
|
+
*
|
|
216
|
+
* Every dependency is injectable so the test drives it with a fake file system —
|
|
217
|
+
* and so a failure inside the preflight can never be the reason a command dies.
|
|
218
|
+
*/
|
|
219
|
+
function checkTree(entryFile, { readFile, exists, root = REPO_ROOT, command } = {}) {
|
|
220
|
+
try {
|
|
221
|
+
const required = requiredModulePaths(entryFile, { readFile, root });
|
|
222
|
+
const missing = missingPaths(required, { exists, root });
|
|
223
|
+
return preflightMessage(missing, { command: command || pathMod.basename(entryFile) });
|
|
224
|
+
} catch (_) {
|
|
225
|
+
// A preflight that throws is a preflight that replaced a clear stack trace with
|
|
226
|
+
// a worse one. Silence here means the command proceeds and fails the old way,
|
|
227
|
+
// which is the outcome this file improves on — never the one it causes.
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = {
|
|
233
|
+
REPO_ROOT,
|
|
234
|
+
requiredModulePaths,
|
|
235
|
+
missingPaths,
|
|
236
|
+
missingModuleDirs,
|
|
237
|
+
preflightMessage,
|
|
238
|
+
checkTree,
|
|
239
|
+
stripComments,
|
|
240
|
+
insideTry,
|
|
241
|
+
};
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.770'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/help_requests.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { test } from 'node:test';
|
|
|
10
10
|
import { createRequire } from 'node:module';
|
|
11
11
|
import fs from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
|
+
import { sqlColumnLines } from './helpers.mjs';
|
|
13
14
|
import { fileURLToPath } from 'node:url';
|
|
14
15
|
|
|
15
16
|
const require = createRequire(import.meta.url);
|
|
@@ -127,10 +128,7 @@ test('sub-speciality is deliberately NOT modelled yet', () => {
|
|
|
127
128
|
// "speciality" anywhere outside a `--` comment and flagged the migration's own
|
|
128
129
|
// COMMENT ON TABLE, which says it is deliberately not modelled. Same
|
|
129
130
|
// over-broad-source-assertion reflex as learning 1000196.
|
|
130
|
-
const
|
|
131
|
-
const columns = body.split('\n')
|
|
132
|
-
.map((l) => l.replace(/--.*$/, '').trim())
|
|
133
|
-
.filter((l) => l && !/^(CONSTRAINT|CHECK|\))/.test(l));
|
|
131
|
+
const columns = sqlColumnLines(sql);
|
|
134
132
|
assert.ok(!columns.some((l) => /special/i.test(l)), `no speciality column: ${columns.filter((l) => /special/i.test(l))}`);
|
|
135
133
|
});
|
|
136
134
|
|
package/tests/helpers.mjs
CHANGED
|
@@ -385,3 +385,39 @@ export function parseInto(host, html) {
|
|
|
385
385
|
return null;
|
|
386
386
|
});
|
|
387
387
|
}
|
|
388
|
+
|
|
389
|
+
// The COLUMN LINES of a CREATE TABLE, with SQL line comments removed (task 1003759).
|
|
390
|
+
//
|
|
391
|
+
// Two tests assert "this table has no such column" and must read the COLUMN LIST,
|
|
392
|
+
// not the file: a migration routinely EXPLAINS in a comment that some column is
|
|
393
|
+
// deliberately not modelled, and matching that prose as a violation of the rule it
|
|
394
|
+
// states is the over-broad-source-assertion reflex of learning 1000196.
|
|
395
|
+
//
|
|
396
|
+
// Both had the same strip, and the same bug: `l.replace(/--.*$/, '')`. In JS `.`
|
|
397
|
+
// does not match `\r`, and `$` without the `m` flag matches ONLY end-of-string —
|
|
398
|
+
// so on a CRLF checkout (every Windows clone) the trailing `\r` sits between `.*`
|
|
399
|
+
// and `$`, the match fails, and the comment is NOT stripped. The comment line then
|
|
400
|
+
// reaches the column scan and trips the very assertion it was written to explain.
|
|
401
|
+
//
|
|
402
|
+
// tests/task_recommendations.mjs failed exactly that way on Windows and nowhere
|
|
403
|
+
// else, so CI never saw it and it sat in the local failure baseline being read as
|
|
404
|
+
// environmental noise. tests/help_requests.mjs carried the identical strip and
|
|
405
|
+
// passed only by luck — no comment inside its CREATE TABLE body happened to
|
|
406
|
+
// contain its scanned word.
|
|
407
|
+
//
|
|
408
|
+
// Dropping the `$` is the whole fix: `.*` still stops at the `\r`, the match now
|
|
409
|
+
// succeeds, and the existing `.trim()` removes what is left.
|
|
410
|
+
export function sqlColumnLines(sql) {
|
|
411
|
+
const text = String(sql || '');
|
|
412
|
+
const start = text.indexOf('CREATE TABLE');
|
|
413
|
+
if (start === -1) return [];
|
|
414
|
+
// End on `\n);` rather than the first `);` — a bare `);` can appear inside a
|
|
415
|
+
// comment or an earlier statement, which would cut the body short.
|
|
416
|
+
const endAt = text.indexOf('\n);', start);
|
|
417
|
+
const body = text.slice(start, endAt === -1 ? undefined : endAt);
|
|
418
|
+
return body.split('\n')
|
|
419
|
+
.map((l) => l.replace(/--.*/, '').trim())
|
|
420
|
+
// CREATE TABLE is dropped too: the slice starts at the header line, so a table
|
|
421
|
+
// whose NAME contained a scanned word would otherwise read as a column.
|
|
422
|
+
.filter((l) => l && !/^(CREATE TABLE|CONSTRAINT|CHECK|\))/.test(l));
|
|
423
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// tests/sql_column_lines.mjs — the CRLF-safe SQL column extractor (task 1003759).
|
|
2
|
+
//
|
|
3
|
+
// THE BUG IT REPLACES. Two tests assert "this table has no such column", and both
|
|
4
|
+
// read the COLUMN LIST rather than the file, because a migration routinely EXPLAINS
|
|
5
|
+
// in a comment that some column is deliberately not modelled — matching that prose
|
|
6
|
+
// as a violation of the rule it states is the over-broad-source-assertion reflex of
|
|
7
|
+
// learning 1000196.
|
|
8
|
+
//
|
|
9
|
+
// Both stripped comments with `l.replace(/--.*$/, '')`, and that is wrong on a CRLF
|
|
10
|
+
// checkout. In JS `.` does not match `\r`, and `$` without the `m` flag matches ONLY
|
|
11
|
+
// end-of-string, so the trailing `\r` sits between `.*` and `$`, the match fails,
|
|
12
|
+
// and the comment survives into the column scan.
|
|
13
|
+
//
|
|
14
|
+
// tests/task_recommendations.mjs failed exactly that way — on Windows only, so CI
|
|
15
|
+
// never saw it and it sat in the local failure baseline being read as environmental
|
|
16
|
+
// noise for weeks. tests/help_requests.mjs had the identical strip and passed purely
|
|
17
|
+
// by luck: no comment inside its CREATE TABLE body happened to contain its scanned
|
|
18
|
+
// word. A guard that works by accident is the thing this file exists to stop.
|
|
19
|
+
//
|
|
20
|
+
// Run: node --test tests/sql_column_lines.mjs
|
|
21
|
+
|
|
22
|
+
import { test } from 'node:test';
|
|
23
|
+
import assert from 'node:assert/strict';
|
|
24
|
+
import { sqlColumnLines } from './helpers.mjs';
|
|
25
|
+
|
|
26
|
+
// The shape that broke: a comment INSIDE the table body naming the very word a
|
|
27
|
+
// caller scans for. Written with explicit \r\n — the whole point is the line ending.
|
|
28
|
+
const CRLF = [
|
|
29
|
+
'CREATE TABLE lifecycle_task_recommendations (',
|
|
30
|
+
' id BIGSERIAL PRIMARY KEY,',
|
|
31
|
+
" -- There is NO 'accepted'. Accepting a recommendation is CLAIMING THE TASK,",
|
|
32
|
+
' -- so "accepted" would be a second, weaker record of holding work.',
|
|
33
|
+
' status TEXT NOT NULL DEFAULT \'open\',',
|
|
34
|
+
' CONSTRAINT one_open_pair UNIQUE (from_builder_id, to_builder_id)',
|
|
35
|
+
');',
|
|
36
|
+
].join('\r\n');
|
|
37
|
+
|
|
38
|
+
const LF = CRLF.replace(/\r\n/g, '\n');
|
|
39
|
+
|
|
40
|
+
test('CRLF: a comment naming the scanned word is stripped, not returned as a column', () => {
|
|
41
|
+
const cols = sqlColumnLines(CRLF);
|
|
42
|
+
assert.ok(!cols.some((l) => /accepted/i.test(l)),
|
|
43
|
+
`comments must not reach the column list, got: ${JSON.stringify(cols)}`);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('CRLF and LF produce identical column lists — the platform must not matter', () => {
|
|
47
|
+
assert.deepEqual(sqlColumnLines(CRLF), sqlColumnLines(LF));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('the real columns survive on both line endings', () => {
|
|
51
|
+
for (const [label, sql] of [['CRLF', CRLF], ['LF', LF]]) {
|
|
52
|
+
const cols = sqlColumnLines(sql);
|
|
53
|
+
assert.ok(cols.some((l) => l.startsWith('id ')), `${label}: id column kept`);
|
|
54
|
+
assert.ok(cols.some((l) => l.startsWith('status ')), `${label}: status column kept`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('CONSTRAINT / CHECK lines and the closing paren are filtered out', () => {
|
|
59
|
+
const cols = sqlColumnLines(CRLF);
|
|
60
|
+
assert.ok(!cols.some((l) => /^CONSTRAINT/.test(l)), 'constraints are not columns');
|
|
61
|
+
assert.ok(!cols.some((l) => /^\)/.test(l)), 'the closing paren is not a column');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('a trailing comment on a real column line is stripped, the column kept', () => {
|
|
65
|
+
const sql = 'CREATE TABLE t (\r\n id BIGINT, -- accepted is not modelled here\r\n);';
|
|
66
|
+
const cols = sqlColumnLines(sql);
|
|
67
|
+
assert.deepEqual(cols, ['id BIGINT,']);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('the body ends at the closing \\n); — a bare ); inside a comment does not cut it short', () => {
|
|
71
|
+
// `indexOf(');')` would stop at the comment and hide every column after it.
|
|
72
|
+
const sql = [
|
|
73
|
+
'CREATE TABLE t (',
|
|
74
|
+
' -- see fn(a); for why',
|
|
75
|
+
' keeper TEXT,',
|
|
76
|
+
');',
|
|
77
|
+
].join('\r\n');
|
|
78
|
+
assert.ok(sqlColumnLines(sql).some((l) => l.startsWith('keeper')),
|
|
79
|
+
'a column after a comment containing "};" must still be seen');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('no CREATE TABLE, empty, and non-string inputs are safe', () => {
|
|
83
|
+
assert.deepEqual(sqlColumnLines('ALTER TABLE t ADD COLUMN x INT;'), []);
|
|
84
|
+
assert.deepEqual(sqlColumnLines(''), []);
|
|
85
|
+
assert.deepEqual(sqlColumnLines(null), []);
|
|
86
|
+
assert.deepEqual(sqlColumnLines(undefined), []);
|
|
87
|
+
});
|
|
@@ -18,6 +18,7 @@ import { test } from 'node:test';
|
|
|
18
18
|
import { createRequire } from 'node:module';
|
|
19
19
|
import fs from 'node:fs';
|
|
20
20
|
import path from 'node:path';
|
|
21
|
+
import { sqlColumnLines } from './helpers.mjs';
|
|
21
22
|
import { fileURLToPath } from 'node:url';
|
|
22
23
|
|
|
23
24
|
const require = createRequire(import.meta.url);
|
|
@@ -79,10 +80,7 @@ test('the schema has NO accepted status — accepting is claiming, which lives e
|
|
|
79
80
|
});
|
|
80
81
|
|
|
81
82
|
test('the schema carries no transfer column', () => {
|
|
82
|
-
const
|
|
83
|
-
const columns = body.split('\n')
|
|
84
|
-
.map((l) => l.replace(/--.*$/, '').trim())
|
|
85
|
-
.filter((l) => l && !/^(CONSTRAINT|CHECK|\))/.test(l));
|
|
83
|
+
const columns = sqlColumnLines(SQL);
|
|
86
84
|
const suspect = columns.filter((l) => /transferred|claimed_by|assigned_to|accepted/i.test(l));
|
|
87
85
|
assert.deepEqual(suspect, [], `no transfer-shaped column: ${suspect}`);
|
|
88
86
|
});
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// tests/tree_preflight.mjs — the partial-checkout preflight (task 1003098).
|
|
2
|
+
//
|
|
3
|
+
// THE BUG. On a task-scoped dev box — a supported, server-driven configuration —
|
|
4
|
+
// `scripts/gds/ship.js` died with a raw stack trace naming ONE missing module. Fix
|
|
5
|
+
// that one, run again, learn the next. The filer lost a session to it, and their
|
|
6
|
+
// workaround (`git sparse-checkout disable` plus disabling the re-scope cron) is
|
|
7
|
+
// not something a builder should have to discover. Node's loader cannot do better:
|
|
8
|
+
// it stops at the first unresolvable specifier and never reaches the second.
|
|
9
|
+
//
|
|
10
|
+
// THE TWO FAILURE DIRECTIONS, and both are tested, because only one is obvious:
|
|
11
|
+
//
|
|
12
|
+
// under-reporting — the original bug: name one missing thing, hide the other six.
|
|
13
|
+
// OVER-reporting — the regression a naive fix causes: several of the CLI's
|
|
14
|
+
// module requires are deliberately try-guarded, so those
|
|
15
|
+
// commands RUN on a scoped box. Refusing them would tell a
|
|
16
|
+
// builder who could have shipped that they cannot, which is
|
|
17
|
+
// worse than the trace it replaces.
|
|
18
|
+
//
|
|
19
|
+
// Pure: a fake file system throughout, per the task's own DONE-WHEN. The cases at
|
|
20
|
+
// the end are the empirical anchor — they run the classifier against the REAL
|
|
21
|
+
// scripts/gds files and pin it to behaviour observed on a simulated scoped tree:
|
|
22
|
+
// start.js survives, claim.js dies on one module, ship.js on seven.
|
|
23
|
+
//
|
|
24
|
+
// Run: node tests/tree_preflight.mjs
|
|
25
|
+
|
|
26
|
+
import { strict as assert } from 'node:assert';
|
|
27
|
+
import { createRequire } from 'node:module';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
import { dirname, join } from 'node:path';
|
|
30
|
+
|
|
31
|
+
const require = createRequire(import.meta.url);
|
|
32
|
+
const tp = require('../scripts/gds/tree-preflight.js');
|
|
33
|
+
|
|
34
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
const repoRoot = join(__dirname, '..');
|
|
36
|
+
|
|
37
|
+
let passed = 0;
|
|
38
|
+
let failed = 0;
|
|
39
|
+
const t = (name, fn) => {
|
|
40
|
+
try { fn(); passed += 1; console.log(` PASS ${name}`); }
|
|
41
|
+
catch (e) { failed += 1; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// A fake scripts/gds tree. Keys are absolute-ish paths as the walker resolves them.
|
|
45
|
+
function fakeFs(files) {
|
|
46
|
+
return {
|
|
47
|
+
readFile: (f) => {
|
|
48
|
+
const key = String(f).replace(/\\/g, '/');
|
|
49
|
+
const hit = Object.keys(files).find((k) => key.endsWith(k));
|
|
50
|
+
if (hit === undefined) { const e = new Error('ENOENT'); e.code = 'ENOENT'; throw e; }
|
|
51
|
+
return files[hit];
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log('\nthe walk — what the command actually needs:');
|
|
57
|
+
|
|
58
|
+
t('collects a direct module require', () => {
|
|
59
|
+
const fs = fakeFs({ 'scripts/gds/a.js': "const x = require('../../modules/lifecycle/dead-deps.js');" });
|
|
60
|
+
assert.deepEqual(
|
|
61
|
+
tp.requiredModulePaths('/repo/scripts/gds/a.js', { ...fs, root: '/repo' }),
|
|
62
|
+
['modules/lifecycle/dead-deps.js'],
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
t('FOLLOWS sibling requires — the whole reason a declared list would be wrong', () => {
|
|
67
|
+
// ship.js reaches its modules through sixteen ship-*.js phase files; a hand-kept
|
|
68
|
+
// list of "what ship.js needs" is a second copy of the require graph.
|
|
69
|
+
const fs = fakeFs({
|
|
70
|
+
'scripts/gds/entry.js': "const p = require('./phase.js');",
|
|
71
|
+
'scripts/gds/phase.js': "const g = require('../../modules/grading/grader.js');",
|
|
72
|
+
});
|
|
73
|
+
assert.deepEqual(
|
|
74
|
+
tp.requiredModulePaths('/repo/scripts/gds/entry.js', { ...fs, root: '/repo' }),
|
|
75
|
+
['modules/grading/grader.js'],
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
t('a require cycle terminates', () => {
|
|
80
|
+
const fs = fakeFs({
|
|
81
|
+
'scripts/gds/a.js': "require('./b.js'); require('../../modules/lifecycle/one.js');",
|
|
82
|
+
'scripts/gds/b.js': "require('./a.js'); require('../../modules/lifecycle/two.js');",
|
|
83
|
+
});
|
|
84
|
+
assert.deepEqual(
|
|
85
|
+
tp.requiredModulePaths('/repo/scripts/gds/a.js', { ...fs, root: '/repo' }),
|
|
86
|
+
['modules/lifecycle/one.js', 'modules/lifecycle/two.js'],
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
t('an unreadable file is skipped, never thrown on', () => {
|
|
91
|
+
// This runs on a checkout that is BY DEFINITION incomplete. A preflight that
|
|
92
|
+
// crashes while diagnosing a partial tree is worse than the trace it replaces.
|
|
93
|
+
const fs = fakeFs({ 'scripts/gds/a.js': "require('./gone.js'); require('../../modules/lifecycle/one.js');" });
|
|
94
|
+
assert.deepEqual(
|
|
95
|
+
tp.requiredModulePaths('/repo/scripts/gds/a.js', { ...fs, root: '/repo' }),
|
|
96
|
+
['modules/lifecycle/one.js'],
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
t('a bare specifier gains .js; one that names an extension is left alone', () => {
|
|
101
|
+
const fs = fakeFs({
|
|
102
|
+
'scripts/gds/a.js': "require('../../modules/lifecycle/ship-preflight');require('../../modules/lifecycle/d.json');",
|
|
103
|
+
});
|
|
104
|
+
assert.deepEqual(
|
|
105
|
+
tp.requiredModulePaths('/repo/scripts/gds/a.js', { ...fs, root: '/repo' }),
|
|
106
|
+
['modules/lifecycle/d.json', 'modules/lifecycle/ship-preflight.js'],
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
console.log('\nOVER-reporting — the regression a naive fix causes:');
|
|
111
|
+
|
|
112
|
+
t('a try-guarded require is NOT a requirement', () => {
|
|
113
|
+
// context-pack.js and cli-lib.js both guard theirs and fall back honestly, which
|
|
114
|
+
// is why /builder-start works on a scoped box today. Reporting them would refuse
|
|
115
|
+
// a command that runs.
|
|
116
|
+
const fs = fakeFs({
|
|
117
|
+
'scripts/gds/a.js': "let c=null; try { c = require('../../modules/lifecycle/task-classifier'); } catch (_) {}",
|
|
118
|
+
});
|
|
119
|
+
assert.deepEqual(tp.requiredModulePaths('/repo/scripts/gds/a.js', { ...fs, root: '/repo' }), []);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
t('a require AFTER a closed try is still a requirement', () => {
|
|
123
|
+
// The brace walk must pop. If it did not, one guarded require near the top would
|
|
124
|
+
// silently excuse every require below it in the file.
|
|
125
|
+
const fs = fakeFs({
|
|
126
|
+
'scripts/gds/a.js': "try { require('../../modules/lifecycle/opt.js'); } catch (_) {}\nrequire('../../modules/lifecycle/need.js');",
|
|
127
|
+
});
|
|
128
|
+
assert.deepEqual(
|
|
129
|
+
tp.requiredModulePaths('/repo/scripts/gds/a.js', { ...fs, root: '/repo' }),
|
|
130
|
+
['modules/lifecycle/need.js'],
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
t('a require quoted inside a COMMENT is not walked', () => {
|
|
135
|
+
// tree-preflight.js's own header quotes the failing require verbatim to explain
|
|
136
|
+
// the bug. A raw-text scan would read that prose as a live edge — the defect
|
|
137
|
+
// filed as task 1003832 against a test that scanned raw source.
|
|
138
|
+
const fs = fakeFs({
|
|
139
|
+
'scripts/gds/a.js': "// require('../../modules/grading/nope.js')\n/* require('../../modules/grading/also.js') */\nrequire('../../modules/lifecycle/real.js');",
|
|
140
|
+
});
|
|
141
|
+
assert.deepEqual(
|
|
142
|
+
tp.requiredModulePaths('/repo/scripts/gds/a.js', { ...fs, root: '/repo' }),
|
|
143
|
+
['modules/lifecycle/real.js'],
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
console.log('\nthe message — one run, every missing thing:');
|
|
148
|
+
|
|
149
|
+
t('nothing missing → null, so a complete checkout is a silent no-op', () => {
|
|
150
|
+
assert.equal(tp.preflightMessage([], { command: '/builder-ship' }), null);
|
|
151
|
+
assert.equal(tp.preflightMessage(null, { command: '/builder-ship' }), null);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
t('EVERY missing requirement is listed, not the first', () => {
|
|
155
|
+
// The whole ask. Seven for ship.js is seven guess-read-fix cycles avoided.
|
|
156
|
+
const missing = ['modules/a/one.js', 'modules/b/two.js', 'modules/b/three.js'];
|
|
157
|
+
const msg = tp.preflightMessage(missing, { command: '/builder-ship' });
|
|
158
|
+
for (const m of missing) assert.ok(msg.includes(m), `names ${m}`);
|
|
159
|
+
assert.match(msg, /Missing \(3\)/, 'and says how many');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
t('it names the CONDITION and BOTH remedies', () => {
|
|
163
|
+
const msg = tp.preflightMessage(['modules/a/one.js'], { command: '/builder-ship' });
|
|
164
|
+
assert.match(msg, /task-scoped/i, 'names why the code is absent');
|
|
165
|
+
assert.match(msg, /not a broken install/i, 'and rules out the wrong diagnosis');
|
|
166
|
+
assert.match(msg, /box-sync\.js/, 're-scope');
|
|
167
|
+
assert.match(msg, /sparse-checkout disable/, 'or take the whole tree');
|
|
168
|
+
assert.match(msg, /\/builder-ship/, 'and says which command was refused');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
t('it rolls the missing files up to whole directories', () => {
|
|
172
|
+
const msg = tp.preflightMessage(['modules/lifecycle/a.js', 'modules/lifecycle/b.js', 'modules/grading/c.js']);
|
|
173
|
+
assert.match(msg, /modules\/grading, modules\/lifecycle/, 'deduped and sorted');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
console.log('\ncheckTree — what the entry points call:');
|
|
177
|
+
|
|
178
|
+
t('a complete tree returns null', () => {
|
|
179
|
+
const fs = fakeFs({ 'scripts/gds/a.js': "require('../../modules/lifecycle/one.js');" });
|
|
180
|
+
assert.equal(tp.checkTree('/repo/scripts/gds/a.js', { ...fs, root: '/repo', exists: () => true }), null);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
t('a partial tree returns the message', () => {
|
|
184
|
+
const fs = fakeFs({ 'scripts/gds/a.js': "require('../../modules/lifecycle/one.js');" });
|
|
185
|
+
const msg = tp.checkTree('/repo/scripts/gds/a.js', { ...fs, root: '/repo', exists: () => false, command: '/builder-x' });
|
|
186
|
+
assert.match(msg, /modules\/lifecycle\/one\.js/);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
t('a preflight that THROWS returns null rather than replacing one bad trace with another', () => {
|
|
190
|
+
const boom = { readFile: () => { throw new Error('boom'); }, exists: () => { throw new Error('boom'); } };
|
|
191
|
+
assert.equal(tp.checkTree('/repo/scripts/gds/a.js', { ...boom, root: '/repo' }), null);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
console.log('\nthe empirical anchor — the REAL CLI files:');
|
|
195
|
+
|
|
196
|
+
t('start.js needs NO unguarded module code, matching that it runs on a scoped box', () => {
|
|
197
|
+
// Observed: with modules/ hidden, start.js prints its card normally. If this
|
|
198
|
+
// list is ever non-empty, /builder-start starts refusing on a box where it works.
|
|
199
|
+
const req = tp.requiredModulePaths(join(repoRoot, 'scripts/gds/start.js'), { root: repoRoot });
|
|
200
|
+
assert.deepEqual(req, [], `start.js must survive a scoped box; got ${req.join(', ')}`);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
t('claim.js and ship.js DO need module code, matching the traces they died with', () => {
|
|
204
|
+
const claim = tp.requiredModulePaths(join(repoRoot, 'scripts/gds/claim.js'), { root: repoRoot });
|
|
205
|
+
assert.ok(claim.includes('modules/lifecycle/dead-deps.js'),
|
|
206
|
+
'the exact module claim.js died on');
|
|
207
|
+
const ship = tp.requiredModulePaths(join(repoRoot, 'scripts/gds/ship.js'), { root: repoRoot });
|
|
208
|
+
assert.ok(ship.includes('modules/lifecycle/ship-preflight.js'),
|
|
209
|
+
'the exact module ship.js died on');
|
|
210
|
+
assert.ok(ship.length >= 5,
|
|
211
|
+
`ship.js fans out across phase files; the walk found ${ship.length}, so the one-per-run cost was real`);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
t('the three entry points actually CALL the preflight, above their first require', () => {
|
|
215
|
+
// The check is worthless if it runs after the require that dies. Asserted by
|
|
216
|
+
// position, because that is the property — not merely that the call exists.
|
|
217
|
+
for (const f of ['start.js', 'claim.js', 'ship.js']) {
|
|
218
|
+
const src = require('node:fs').readFileSync(join(repoRoot, 'scripts/gds', f), 'utf8');
|
|
219
|
+
const call = src.indexOf('_checkTree(__filename');
|
|
220
|
+
assert.ok(call > 0, `${f} calls the preflight`);
|
|
221
|
+
const firstModuleRequire = src.search(/require\(\s*['"]\.\.\/\.\.\/modules\//);
|
|
222
|
+
if (firstModuleRequire > 0) {
|
|
223
|
+
assert.ok(call < firstModuleRequire, `${f}: the preflight must run BEFORE the first modules/ require`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
t('the preflight names the command for the READER, never a hard-coded slash command', () => {
|
|
229
|
+
// The message this preflight prints is reader-facing, and `/builder-ship` does not exist
|
|
230
|
+
// outside Claude Code — a person running the public `bongos` CLI would be handed a command
|
|
231
|
+
// they cannot type. tests/cli_surface.mjs scans start.js for exactly this, but nothing
|
|
232
|
+
// scanned claim.js or ship.js, and the first draft of this task hard-coded all three.
|
|
233
|
+
for (const f of ['start.js', 'claim.js', 'ship.js']) {
|
|
234
|
+
const src = require('node:fs').readFileSync(join(repoRoot, 'scripts/gds', f), 'utf8');
|
|
235
|
+
const line = src.split('\n').find((l) => l.includes('_checkTree(__filename'));
|
|
236
|
+
assert.ok(line, `${f} calls the preflight`);
|
|
237
|
+
assert.ok(!/\/builder-/.test(line), `${f}: hard-coded slash command in the preflight message`);
|
|
238
|
+
assert.match(line, /_surfaceAction\(/, `${f}: the command name must come from the surface switch`);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
console.log(`\ntree_preflight: ${passed} passed, ${failed} failed`);
|
|
243
|
+
if (failed) process.exit(1);
|