@bongos/core 1.19.767 → 1.19.769
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 +27 -17
- 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/provision.js +2 -3
- 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/provision.mjs +12 -0
- 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.769",
|
|
6
|
+
"core_contract": "1.19.769",
|
|
7
|
+
"source_commit": "5552e7252b67cc3192446a9c48ca2cd4faf10368",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-16T14:
|
|
9
|
+
"built_at": "2026-09-16T14:54:06.807Z",
|
|
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": 2125,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2633,
|
|
20
|
+
"tree_sha256": "8c9135931a565436af92fb7fc38953f0bf6b9ca8c6b608b91e9751c88dd62fe0",
|
|
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": "ffaeae011cd55895b3c16a2ee903e0593aa42a22cfceb3307c5228452c737948"
|
|
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": "17b6a246e7e14d3949244aff29ed7e63dc5bc5d65a7aeff2a463f8283fa5b4cc"
|
|
7581
7581
|
},
|
|
7582
7582
|
{
|
|
7583
7583
|
"path": "package.json",
|
|
7584
7584
|
"mode": "0000644",
|
|
7585
|
-
"sha256": "
|
|
7585
|
+
"sha256": "df0b9ff8dceb4cafd08db0a28bcbe92b1cbee04fefe4f811574257af0929901c"
|
|
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",
|
|
@@ -8442,7 +8442,7 @@
|
|
|
8442
8442
|
{
|
|
8443
8443
|
"path": "scripts/gds/provision.js",
|
|
8444
8444
|
"mode": "0000644",
|
|
8445
|
-
"sha256": "
|
|
8445
|
+
"sha256": "bf403052607f7cce560633fc11d03f51ecf1e9fc1d32e4d6c601c6406787a143"
|
|
8446
8446
|
},
|
|
8447
8447
|
{
|
|
8448
8448
|
"path": "scripts/gds/publish-credential-check.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": "3237d6ea069eae19ed9baf41a528984e21010defcfc5a0d96d34fd8eeca2fb4a"
|
|
9406
9411
|
},
|
|
9407
9412
|
{
|
|
9408
9413
|
"path": "src/module-loader/catalog.js",
|
|
@@ -12062,7 +12067,7 @@
|
|
|
12062
12067
|
{
|
|
12063
12068
|
"path": "tests/provision.mjs",
|
|
12064
12069
|
"mode": "0000644",
|
|
12065
|
-
"sha256": "
|
|
12070
|
+
"sha256": "5f7ec2483f8564dc9561f0becdfc36166bca5021b7f964db6d6e40d62108e041"
|
|
12066
12071
|
},
|
|
12067
12072
|
{
|
|
12068
12073
|
"path": "tests/provision_restart.mjs",
|
|
@@ -12979,6 +12984,11 @@
|
|
|
12979
12984
|
"mode": "0000644",
|
|
12980
12985
|
"sha256": "61b50103967187dbff82f957d2faf6cc9ca5079b83531a5925fdeae61eb3c921"
|
|
12981
12986
|
},
|
|
12987
|
+
{
|
|
12988
|
+
"path": "tests/tree_preflight.mjs",
|
|
12989
|
+
"mode": "0000644",
|
|
12990
|
+
"sha256": "d8382f7a608fc7511cd6e00e3a9efcb0283b8128b39cbe229fb07ae590b20c4b"
|
|
12991
|
+
},
|
|
12982
12992
|
{
|
|
12983
12993
|
"path": "tests/triage_development.mjs",
|
|
12984
12994
|
"mode": "0000644",
|
|
@@ -2008,5 +2008,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2008
2008
|
landed since 1.19.765 with no explicit bump. run 35104246900. (task 1002620)
|
|
2009
2009
|
1.19.767 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2010
2010
|
landed since 1.19.766 with no explicit bump. run 35109111074. (task 1002620)
|
|
2011
|
+
1.19.768 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
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)
|
|
2011
2015
|
---------------------------------------------------------------------------
|
|
2012
2016
|
```
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.769",
|
|
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.769",
|
|
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.769",
|
|
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/provision.js
CHANGED
|
@@ -1033,12 +1033,11 @@ async function teardownInstance(inst, deps) {
|
|
|
1033
1033
|
if (inst.hosting_shape !== 'dedicated') {
|
|
1034
1034
|
boxExec(`${sudoP}systemctl disable --now ${inst.slug}`, { allowFail: true });
|
|
1035
1035
|
boxExec(`${sudoP}rm -f ${serviceUnitPath(inst)}`, { allowFail: true });
|
|
1036
|
-
// Stop + remove the nightly backup units too (task 2213)
|
|
1037
|
-
// dumps are left in place — like the DB below, they are precious data a teardown
|
|
1038
|
-
// must not silently destroy.
|
|
1036
|
+
// Stop + remove the nightly backup units too (task 2213; the dir + dumps stay — precious data, like the DB below), then reset-failed (task 1003940): a removed unit's FAILED state outlives its file, so `systemctl --failed` listed torn-down ghosts for weeks.
|
|
1039
1037
|
boxExec(`${sudoP}systemctl disable --now ${backupUnitName(inst)}.timer`, { allowFail: true });
|
|
1040
1038
|
boxExec(`${sudoP}rm -f ${backupServicePath(inst)} ${backupTimerPath(inst)}`, { allowFail: true });
|
|
1041
1039
|
boxExec(`${sudoP}systemctl daemon-reload`, { allowFail: true });
|
|
1040
|
+
boxExec(`${sudoP}systemctl reset-failed ${inst.slug}.service ${backupUnitName(inst)}.service`, { allowFail: true });
|
|
1042
1041
|
// Left for an operator (data is precious), naming ONLY what this row created —
|
|
1043
1042
|
// teardown now takes 'requested'/'error'. `port` + `domain` stay held: ADR 0181.
|
|
1044
1043
|
const dumps = inst.db_name ? [`dropdb ${dbName(inst)}`, `prune ${CONFIG.backupDir}/${dbName(inst)}-*.sql.gz`] : [];
|
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.769'; // 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/provision.mjs
CHANGED
|
@@ -1485,6 +1485,18 @@ await ta('teardownInstance(privileged): systemctl + rm-under-/etc are sudo-prefi
|
|
|
1485
1485
|
// task 2213: the backup units come down too (the dumps + dir are left in place)
|
|
1486
1486
|
assert.match(joined, /sudo systemctl disable --now mercury-db-backup\.timer/);
|
|
1487
1487
|
assert.match(joined, /sudo rm -f \/etc\/systemd\/system\/mercury-db-backup\.service \/etc\/systemd\/system\/mercury-db-backup\.timer/);
|
|
1488
|
+
// task 1003940: a removed unit's FAILED state outlives its file — clear it, AFTER the rm + reload,
|
|
1489
|
+
// for both the instance unit and its backup unit (the two ghosts task 1003930 found on the box).
|
|
1490
|
+
assert.match(joined, /sudo systemctl reset-failed mercury\.service mercury-db-backup\.service/);
|
|
1491
|
+
const idx = (re) => cmds.findIndex((c) => re.test(c));
|
|
1492
|
+
assert.ok(idx(/rm -f .*mercury-db-backup\.service/) < idx(/daemon-reload/), 'rm before reload');
|
|
1493
|
+
assert.ok(idx(/daemon-reload/) < idx(/reset-failed/), 'reset-failed after the reload that forgets the file — reset first and the stopped unit could fail again');
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
await ta('teardownInstance(dedicated): no systemd steps at all, so no reset-failed either (its own droplet)', async () => {
|
|
1497
|
+
const { deps, cmds } = recorder();
|
|
1498
|
+
await P.teardownInstance({ ...coTenant, hosting_shape: 'dedicated', droplet_id: 9 }, deps);
|
|
1499
|
+
assert.ok(!cmds.some((c) => /reset-failed|daemon-reload|disable --now/.test(c)), cmds.join(' | '));
|
|
1488
1500
|
});
|
|
1489
1501
|
|
|
1490
1502
|
await ta('teardownInstance(default): NO sudo (bare, unchanged)', async () => {
|
|
@@ -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);
|