@bongos/core 1.19.809 → 1.19.811
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 +14 -14
- package/docs/module-api-changelog.md +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/ship-flow.js +27 -17
- package/src/bongos/module-scope-map.js +58 -1
- package/src/module-api.js +1 -1
- package/tests/cli_exit_no_abort.mjs +45 -0
- package/tests/module-scope-map.mjs +96 -0
- package/tests/session_caps.mjs +16 -1
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
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.811",
|
|
6
|
+
"core_contract": "1.19.811",
|
|
7
|
+
"source_commit": "8dc3b188ab249c84103920e82ce8021dc55487ed",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-17T06:20:04.579Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 487,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 2710,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "7f3e8838baaa79070aa7705d761e9613297767dc1391cbf4f2ba0492339cd9b1",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2532,7 +2532,7 @@
|
|
|
2532
2532
|
{
|
|
2533
2533
|
"path": "docs/module-api-changelog.md",
|
|
2534
2534
|
"mode": "0000644",
|
|
2535
|
-
"sha256": "
|
|
2535
|
+
"sha256": "bb4fbb214534d6224c13b6c6aa283ca18d762fc46482842169505bb7c58d6308"
|
|
2536
2536
|
},
|
|
2537
2537
|
{
|
|
2538
2538
|
"path": "docs/modules-contract.md",
|
|
@@ -7727,12 +7727,12 @@
|
|
|
7727
7727
|
{
|
|
7728
7728
|
"path": "package-lock.json",
|
|
7729
7729
|
"mode": "0000644",
|
|
7730
|
-
"sha256": "
|
|
7730
|
+
"sha256": "d922e9276bdb8d89f5998de6f4b29dbc98d897e5a7acfc11d0ceeb2b11e89178"
|
|
7731
7731
|
},
|
|
7732
7732
|
{
|
|
7733
7733
|
"path": "package.json",
|
|
7734
7734
|
"mode": "0000644",
|
|
7735
|
-
"sha256": "
|
|
7735
|
+
"sha256": "98f193e8dfee8bab41773622a373d2c19efafa8fb7bdeaf6e2c43f2032651d24"
|
|
7736
7736
|
},
|
|
7737
7737
|
{
|
|
7738
7738
|
"path": "public-docs/index.html",
|
|
@@ -8977,7 +8977,7 @@
|
|
|
8977
8977
|
{
|
|
8978
8978
|
"path": "scripts/gds/ship-flow.js",
|
|
8979
8979
|
"mode": "0000644",
|
|
8980
|
-
"sha256": "
|
|
8980
|
+
"sha256": "bb39ac6572410567ce0a2881f26b0512df817fd04cec9130792ca24c48e8d02a"
|
|
8981
8981
|
},
|
|
8982
8982
|
{
|
|
8983
8983
|
"path": "scripts/gds/ship-git.js",
|
|
@@ -9432,7 +9432,7 @@
|
|
|
9432
9432
|
{
|
|
9433
9433
|
"path": "src/bongos/module-scope-map.js",
|
|
9434
9434
|
"mode": "0000644",
|
|
9435
|
-
"sha256": "
|
|
9435
|
+
"sha256": "8d7b749edca694b4c9d8a563a4334c7fe3fc7be64c520c3d14c3b63febff132a"
|
|
9436
9436
|
},
|
|
9437
9437
|
{
|
|
9438
9438
|
"path": "src/bongos/module-submissions.js",
|
|
@@ -9622,7 +9622,7 @@
|
|
|
9622
9622
|
{
|
|
9623
9623
|
"path": "src/module-api.js",
|
|
9624
9624
|
"mode": "0000644",
|
|
9625
|
-
"sha256": "
|
|
9625
|
+
"sha256": "7cbce9c1f2bed1a8fe7a8cff1cfee09c702ee66fa719829ff347087bf86fb7bc"
|
|
9626
9626
|
},
|
|
9627
9627
|
{
|
|
9628
9628
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10327,7 +10327,7 @@
|
|
|
10327
10327
|
{
|
|
10328
10328
|
"path": "tests/cli_exit_no_abort.mjs",
|
|
10329
10329
|
"mode": "0000644",
|
|
10330
|
-
"sha256": "
|
|
10330
|
+
"sha256": "39b63a76b153426f486240e4c57d4fafb2b5b130fb9853c800d20d9f4e5acdde"
|
|
10331
10331
|
},
|
|
10332
10332
|
{
|
|
10333
10333
|
"path": "tests/cli_lib_formatters.mjs",
|
|
@@ -11942,7 +11942,7 @@
|
|
|
11942
11942
|
{
|
|
11943
11943
|
"path": "tests/module-scope-map.mjs",
|
|
11944
11944
|
"mode": "0000644",
|
|
11945
|
-
"sha256": "
|
|
11945
|
+
"sha256": "e277e0579dd6c8fea459cc5a3b4adf04dda12c95e69a415eeea5cfd2de52d760"
|
|
11946
11946
|
},
|
|
11947
11947
|
{
|
|
11948
11948
|
"path": "tests/module_api.mjs",
|
|
@@ -12857,7 +12857,7 @@
|
|
|
12857
12857
|
{
|
|
12858
12858
|
"path": "tests/session_caps.mjs",
|
|
12859
12859
|
"mode": "0000644",
|
|
12860
|
-
"sha256": "
|
|
12860
|
+
"sha256": "b08260c077dca811eec663b0f32618ef1a5e28c78a427b907f0c451ee68fc196"
|
|
12861
12861
|
},
|
|
12862
12862
|
{
|
|
12863
12863
|
"path": "tests/session_cost.mjs",
|
|
@@ -2092,5 +2092,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2092
2092
|
landed since 1.19.807 with no explicit bump. run 35185130913. (task 1002620)
|
|
2093
2093
|
1.19.809 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2094
2094
|
landed since 1.19.808 with no explicit bump. run 35187322516. (task 1002620)
|
|
2095
|
+
1.19.810 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2096
|
+
landed since 1.19.809 with no explicit bump. run 35188253911. (task 1002620)
|
|
2097
|
+
1.19.811 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2098
|
+
landed since 1.19.810 with no explicit bump. run 35189324772. (task 1002620)
|
|
2095
2099
|
---------------------------------------------------------------------------
|
|
2096
2100
|
```
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.811",
|
|
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.811",
|
|
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.811",
|
|
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",
|
package/scripts/gds/ship-flow.js
CHANGED
|
@@ -11,8 +11,7 @@
|
|
|
11
11
|
const { cliClient, requireSession, arg, detectWorktreeName, cardFlagsFromMe } = require('./cli-lib');
|
|
12
12
|
const { resolveProse, markShipProgress, smokeGateMessages, proseFlagPresent, regradeNotesRefusal, staleSummaryNotice } = require('./ship-honesty.js');
|
|
13
13
|
const { classifyEmptyShip } = require('../../modules/lifecycle/ship-preflight');
|
|
14
|
-
const { scopeForModule,
|
|
15
|
-
const { matchOne } = require('../../src/bongos/path-match');
|
|
14
|
+
const { isModuleKey, scopeForModule, moduleDriftFiles: computeModuleDrift } = require('../../src/bongos/module-scope-map');
|
|
16
15
|
const { runShipGate } = require('./sandbox-stage');
|
|
17
16
|
const { captureSurpriseLearning, promptSurprise } = require('./ship-surprise.js');
|
|
18
17
|
// task 1003109: the optional `--visual` leg of a ship (goal 1000072).
|
|
@@ -270,9 +269,18 @@ async function shipMain() {
|
|
|
270
269
|
// ---- Module drift check (BV1.R53, task 1422) ----
|
|
271
270
|
// If the task carries a module_key, verify the committed diff doesn't stray
|
|
272
271
|
// into a DIFFERENT module's territory. "You said game, you touched dev-box" is
|
|
273
|
-
// the semantic violation this catches. Files
|
|
274
|
-
//
|
|
275
|
-
//
|
|
272
|
+
// the semantic violation this catches. Files no module owns are allowed —
|
|
273
|
+
// they're cross-cutting — as is the territory of a CROSS-CUTTING module, which
|
|
274
|
+
// every craft legitimately writes into. Only contact with a different OWNING
|
|
275
|
+
// module's glob set is flagged.
|
|
276
|
+
//
|
|
277
|
+
// The rule itself lives in module-scope-map.js (moduleDriftFiles) next to the
|
|
278
|
+
// map it reads, so it can be tested without running a ship (task 1003770). It
|
|
279
|
+
// used to be inline here, and read "files outside all module scopes (docs,
|
|
280
|
+
// session logs, general scripts) are allowed" — a premise task 1002989 ended by
|
|
281
|
+
// giving `methodology` the whole docs/ subtree. Since every ship writes a
|
|
282
|
+
// session log, the advisory then fired for 25 of the 26 module keys on that log
|
|
283
|
+
// alone. See CROSS_CUTTING_KEYS for why the glob was not shrunk instead.
|
|
276
284
|
//
|
|
277
285
|
// The check is ADVISORY (warns but does not refuse) on the first cut because
|
|
278
286
|
// the module_key column is new and existing tasks may carry a broader diff scope
|
|
@@ -280,24 +288,26 @@ async function shipMain() {
|
|
|
280
288
|
// ac.module_key comes from getActiveClaimsForBuilder (t.module_key in the join).
|
|
281
289
|
const enforceModule = args.includes('--enforce-module');
|
|
282
290
|
if (ac.module_key && isModuleKey(ac.module_key) && committedFiles.length > 0) {
|
|
283
|
-
const
|
|
284
|
-
const { MODULE_SCOPE_MAP } = require('../../src/bongos/module-scope-map');
|
|
285
|
-
const moduleDriftFiles = committedFiles.filter((f) => {
|
|
286
|
-
if (declaredScope && matchOne(f, declaredScope.globs)) return false;
|
|
287
|
-
for (const [key, entry] of Object.entries(MODULE_SCOPE_MAP)) {
|
|
288
|
-
if (key === ac.module_key) continue;
|
|
289
|
-
if (matchOne(f, entry.globs)) return true;
|
|
290
|
-
}
|
|
291
|
-
return false;
|
|
292
|
-
});
|
|
291
|
+
const moduleDriftFiles = computeModuleDrift(ac.module_key, committedFiles);
|
|
293
292
|
if (moduleDriftFiles.length > 0) {
|
|
293
|
+
// Name the module's REAL territory rather than guessing 'modules/<key>/'.
|
|
294
|
+
// That guess is wrong for every module whose code has not been carved out
|
|
295
|
+
// yet (ADR 0083) — kernel lives in src/bongos/, methodology in docs/ — so
|
|
296
|
+
// the advisory used to tell those builders to move their work somewhere
|
|
297
|
+
// their module does not own. scopeForModule is the roster's own answer.
|
|
298
|
+
const declared = scopeForModule(ac.module_key);
|
|
299
|
+
const territory = declared && declared.globs.length
|
|
300
|
+
? declared.globs.slice(0, 4).join(', ') + (declared.globs.length > 4 ? `, +${declared.globs.length - 4} more` : '')
|
|
301
|
+
: `modules/${ac.module_key}/`;
|
|
294
302
|
const msg = [
|
|
295
303
|
`\n⚠ Module drift detected: task is scoped to '${ac.module_key}' but ${moduleDriftFiles.length} file(s) touch a different module:`,
|
|
296
304
|
...moduleDriftFiles.map((f) => ` - ${f}`),
|
|
297
305
|
'',
|
|
306
|
+
` '${ac.module_key}' owns: ${territory}`,
|
|
307
|
+
'',
|
|
298
308
|
enforceModule
|
|
299
|
-
?
|
|
300
|
-
: 'Advisory — ship proceeds, but consider confining your changes to
|
|
309
|
+
? `Refused (--enforce-module). Confine your changes to that territory, or clear the module_key label.`
|
|
310
|
+
: 'Advisory — ship proceeds, but consider confining your changes to that territory.',
|
|
301
311
|
].join('\n');
|
|
302
312
|
console.log(msg);
|
|
303
313
|
if (enforceModule) process.exit(1);
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
'use strict';
|
|
38
38
|
|
|
39
39
|
const { PROTECTED_GLOBS } = require('./permission-path-check');
|
|
40
|
-
const { touchesOverlap } = require('./path-match');
|
|
40
|
+
const { touchesOverlap, matchOne } = require('./path-match');
|
|
41
41
|
|
|
42
42
|
// key → the path globs that constitute the module's code (exact paths or
|
|
43
43
|
// trailing-'/' subtree prefixes). See the header for the auto-tighten contract.
|
|
@@ -486,6 +486,62 @@ function scopeIncludesProtected(keys) {
|
|
|
486
486
|
return (keys || []).some((k) => isProtectedModule(k) === true);
|
|
487
487
|
}
|
|
488
488
|
|
|
489
|
+
// ---- CROSS-CUTTING MODULES (task 1003770) ----
|
|
490
|
+
//
|
|
491
|
+
// A module whose territory EVERY craft legitimately writes into, whatever its own
|
|
492
|
+
// task is scoped to. Distinct from SHARED_GLOBS above: a shared glob is owned by
|
|
493
|
+
// nobody, while a cross-cutting module genuinely owns its paths — `methodology`
|
|
494
|
+
// really is the owner of docs/ — it is just that owning them cannot mean other
|
|
495
|
+
// modules are trespassing when they write there.
|
|
496
|
+
//
|
|
497
|
+
// WHAT THIS IS AND IS NOT FOR. This narrows ONE consumer: the ship-time module-
|
|
498
|
+
// drift advisory (moduleDriftFiles below), whose question is "you said game, did
|
|
499
|
+
// you touch dev-box?" — module-to-module confusion. It does NOT touch the goal
|
|
500
|
+
// scope wall, claim collisions (touchesOverlap), or the protected-path wall.
|
|
501
|
+
// Those keep counting methodology's territory as methodology's.
|
|
502
|
+
//
|
|
503
|
+
// WHY IT WAS NEEDED. Task 1002989 gave `methodology` the whole docs/ subtree.
|
|
504
|
+
// Since every ship writes a session log under docs/session-logs/, that made the
|
|
505
|
+
// advisory fire on 25 of the 26 module keys for the session log ALONE — it warned
|
|
506
|
+
// on essentially every module-keyed ship, which is not a signal, it is weather.
|
|
507
|
+
//
|
|
508
|
+
// WHY NOT SHRINK THE GLOB INSTEAD. The glob format has no wildcards (see the
|
|
509
|
+
// header), so "all of docs/ except session-logs" is inexpressible; enumerating
|
|
510
|
+
// docs/ subtree-by-subtree would rot on the next doc.
|
|
511
|
+
//
|
|
512
|
+
// THE BAR, deliberately high — "every task legitimately writes here as a normal
|
|
513
|
+
// part of shipping". `devsecops` is NOT on this list even though it owns
|
|
514
|
+
// package.json: a task that edits .github/, .husky/ or fitness.js while scoped to
|
|
515
|
+
// another module IS the confusion this advisory exists to surface, and lockfile
|
|
516
|
+
// churn is occasional rather than universal.
|
|
517
|
+
const CROSS_CUTTING_KEYS = Object.freeze(['methodology']);
|
|
518
|
+
|
|
519
|
+
function isCrossCuttingModule(key) {
|
|
520
|
+
return CROSS_CUTTING_KEYS.includes(key);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Pure: of `files`, the ones that stray into a DIFFERENT module's territory for a
|
|
524
|
+
// task scoped to `moduleKey`. Extracted from scripts/gds/ship-flow.js (BV1.R53,
|
|
525
|
+
// task 1422) so the rule is testable without running a ship.
|
|
526
|
+
//
|
|
527
|
+
// A file is drift only if it is (a) outside the declared module's own globs and
|
|
528
|
+
// (b) inside some OTHER module's globs — a path no module owns is cross-cutting
|
|
529
|
+
// and always fine. Cross-cutting modules are skipped in (b): their territory
|
|
530
|
+
// never makes a file someone else's. An unknown moduleKey yields no drift, since
|
|
531
|
+
// there is no declared scope to stray from.
|
|
532
|
+
function moduleDriftFiles(moduleKey, files) {
|
|
533
|
+
const declaredScope = scopeForModule(moduleKey);
|
|
534
|
+
if (!declaredScope) return [];
|
|
535
|
+
return (files || []).filter((file) => {
|
|
536
|
+
if (matchOne(file, declaredScope.globs)) return false;
|
|
537
|
+
for (const [key, entry] of Object.entries(MODULE_SCOPE_MAP)) {
|
|
538
|
+
if (key === moduleKey || isCrossCuttingModule(key)) continue;
|
|
539
|
+
if (matchOne(file, entry.globs)) return true;
|
|
540
|
+
}
|
|
541
|
+
return false;
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
489
545
|
module.exports = {
|
|
490
546
|
MODULE_GLOBS,
|
|
491
547
|
MODULE_SCOPE_MAP,
|
|
@@ -498,5 +554,6 @@ module.exports = {
|
|
|
498
554
|
isProtectedModule,
|
|
499
555
|
protectedModules,
|
|
500
556
|
globsForModules,
|
|
557
|
+
moduleDriftFiles,
|
|
501
558
|
scopeIncludesProtected,
|
|
502
559
|
};
|
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.811'; // 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');
|
|
@@ -493,3 +493,48 @@ test('main-audit.js exits 0 after a SUCCESSFUL roster request — the teardown t
|
|
|
493
493
|
proc.kill();
|
|
494
494
|
}
|
|
495
495
|
});
|
|
496
|
+
|
|
497
|
+
// ---- tests/session_caps.mjs (task 1003827) ----
|
|
498
|
+
//
|
|
499
|
+
// Not a CLI, but the same abort and the same cure. It printed "9 passed, 0
|
|
500
|
+
// failed" and then died 127 in 4 of 6 runs on Windows + Node 24. run-unit-tests.js
|
|
501
|
+
// reads the exit code, so a suite that fully passed was reported FAILED in most
|
|
502
|
+
// local baselines — which is worse than a plain red, because it teaches builders
|
|
503
|
+
// that a failing name is environmental noise.
|
|
504
|
+
const SESSION_CAPS = path.join(ROOT, 'tests', 'session_caps.mjs');
|
|
505
|
+
|
|
506
|
+
test('session_caps.mjs exits 0 after its server teardown, repeatedly (task 1003827)', () => {
|
|
507
|
+
// Three runs, because the abort was NONDETERMINISTIC — a single clean run
|
|
508
|
+
// proved nothing before the fix and would prove nothing after a regression.
|
|
509
|
+
for (let i = 0; i < 3; i++) {
|
|
510
|
+
const r = spawnSync(process.execPath, [SESSION_CAPS], { encoding: 'utf8', timeout: 60000 });
|
|
511
|
+
assert.notEqual(r.signal, 'SIGTERM', `run ${i + 1} timed out — a leaked handle, which is what removing the hard exit risks`);
|
|
512
|
+
assert.doesNotMatch(r.stderr || '', ABORT_RE, `run ${i + 1} aborted natively:\n${r.stderr}`);
|
|
513
|
+
assert.equal(r.status, 0, `run ${i + 1} exited ${r.status} (127 = the abort); stderr: ${r.stderr}`);
|
|
514
|
+
assert.match(r.stdout || '', /0 failed/, `run ${i + 1} did not report a passing suite`);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test('session_caps.mjs sets exitCode rather than calling process.exit (task 1003827)', () => {
|
|
519
|
+
// The deterministic half: the behavioural test above can pass by luck on a
|
|
520
|
+
// platform where the race does not fire, so the shape is pinned directly.
|
|
521
|
+
const src = fs.readFileSync(SESSION_CAPS, 'utf8');
|
|
522
|
+
// Comments stripped first: the fix's own comment explains what it does NOT do
|
|
523
|
+
// and names the call, so a raw scan matches the prose and reds a correct file.
|
|
524
|
+
// (Caught by this test on its first run — the guard found its own false positive.)
|
|
525
|
+
const code = src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
|
526
|
+
// Anti-vacuity: prove the pattern MATCHES something before trusting that it
|
|
527
|
+
// matches nothing here. A scanner that silently finds nothing looks identical
|
|
528
|
+
// to a scanner that works — and stripping comments is exactly the kind of step
|
|
529
|
+
// that can blank the haystack.
|
|
530
|
+
const HARD_EXIT = /process\.exit\s*\(/;
|
|
531
|
+
assert.match('process.exit(1)', HARD_EXIT, 'control: the pattern must detect a hard exit');
|
|
532
|
+
assert.ok(src.length > 1000, 'control: the suite was actually read');
|
|
533
|
+
assert.ok(code.length > src.length / 2, 'control: stripping comments did not blank the source');
|
|
534
|
+
assert.match(code, /server\.listen/, 'control: real code survived the strip');
|
|
535
|
+
assert.doesNotMatch(code, HARD_EXIT, 'session_caps.mjs must set process.exitCode and let the loop drain');
|
|
536
|
+
assert.match(code, /process\.exitCode\s*=/, 'and it must still report failure through the exit code');
|
|
537
|
+
// The other half of the cure: a hard exit MASKS leaked handles, so dropping it
|
|
538
|
+
// without releasing the keep-alive sockets trades the crash for a hang.
|
|
539
|
+
assert.match(code, /closeAllConnections/, 'sockets must be released before server.close()');
|
|
540
|
+
});
|
|
@@ -291,3 +291,99 @@ test('the gap this closed: an honest task declaring its own test is now authoriz
|
|
|
291
291
|
const r = gsc.check(['ideas'], ['modules/ideas/inbox.js', 'tests/idea_routing.mjs'], 'thetes');
|
|
292
292
|
assert.equal(r.ok, true, r.reason);
|
|
293
293
|
});
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
// ---- module drift advisory: cross-cutting territory (task 1003770) ----
|
|
297
|
+
|
|
298
|
+
const SESSION_LOG = 'docs/session-logs/2026-09-16-task-1003770-example.md';
|
|
299
|
+
|
|
300
|
+
test('THE BUG: a module-scoped task writing a session log is not drift (task 1003770)', () => {
|
|
301
|
+
// The row's own acceptance criterion. Task 1002989 gave `methodology` the whole
|
|
302
|
+
// docs/ subtree, and every ship writes a session log under it — so the advisory
|
|
303
|
+
// started firing on almost every module-keyed ship.
|
|
304
|
+
for (const key of scope.moduleKeys()) {
|
|
305
|
+
assert.deepEqual(
|
|
306
|
+
scope.moduleDriftFiles(key, [SESSION_LOG]), [],
|
|
307
|
+
`a '${key}' task writing a session log must not read as drift`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('the regression this replaces: the OLD rule flagged 25 of 26 keys on that one file', () => {
|
|
313
|
+
// Reproduce the pre-fix predicate — identical except that it does not skip
|
|
314
|
+
// cross-cutting modules — and prove it fails where the new one passes. Without
|
|
315
|
+
// this, the test above could pass for the wrong reason (e.g. nobody owning
|
|
316
|
+
// docs/) and go quietly vacuous if the glob moved again.
|
|
317
|
+
const { matchOne } = require(path.join(ROOT, 'src', 'bongos', 'path-match.js'));
|
|
318
|
+
const oldRule = (moduleKey, files) => {
|
|
319
|
+
const declared = scope.scopeForModule(moduleKey);
|
|
320
|
+
return files.filter((file) => {
|
|
321
|
+
if (declared && matchOne(file, declared.globs)) return false;
|
|
322
|
+
return Object.entries(scope.MODULE_SCOPE_MAP)
|
|
323
|
+
.some(([k, e]) => k !== moduleKey && matchOne(file, e.globs));
|
|
324
|
+
});
|
|
325
|
+
};
|
|
326
|
+
const flaggedBefore = scope.moduleKeys().filter((k) => oldRule(k, [SESSION_LOG]).length);
|
|
327
|
+
assert.ok(
|
|
328
|
+
flaggedBefore.length >= 20,
|
|
329
|
+
`expected the old rule to flag most keys on a session log, it flagged ${flaggedBefore.length}`,
|
|
330
|
+
);
|
|
331
|
+
assert.ok(flaggedBefore.includes('lifecycle'), 'sanity: lifecycle was among them');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('a cross-cutting module owns its paths everywhere EXCEPT the drift advisory', () => {
|
|
335
|
+
// The narrowness is the point: this must not become a hole in the scope wall.
|
|
336
|
+
// methodology still genuinely owns docs/ — only moduleDriftFiles looks away.
|
|
337
|
+
// Asserted through behaviour, not through the roster constant: the constant is
|
|
338
|
+
// internal (nothing outside the module reads it), and "docs/ stops counting as
|
|
339
|
+
// someone else's territory" is the property that actually matters.
|
|
340
|
+
assert.deepEqual(scope.moduleDriftFiles('lifecycle', ['docs/architecture.md']), []);
|
|
341
|
+
assert.ok(scope.scopeForModule('methodology').globs.includes('docs/'));
|
|
342
|
+
const owners = Object.entries(scope.MODULE_SCOPE_MAP)
|
|
343
|
+
.filter(([, e]) => e.globs.includes('docs/'))
|
|
344
|
+
.map(([k]) => k);
|
|
345
|
+
assert.deepEqual(owners, ['methodology'], 'docs/ is still owned, not orphaned');
|
|
346
|
+
// And the goal scope wall still places docs/ inside methodology, unchanged.
|
|
347
|
+
assert.equal(gsc.check(['methodology'], ['docs/architecture.md'], 'thetes').ok, true);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('real cross-module contact is still flagged — the signal the advisory exists for', () => {
|
|
351
|
+
// "You said lifecycle, you touched dev-box."
|
|
352
|
+
assert.deepEqual(
|
|
353
|
+
scope.moduleDriftFiles('lifecycle', ['modules/dev-box/app/src/box-controller.js']),
|
|
354
|
+
['modules/dev-box/app/src/box-controller.js'],
|
|
355
|
+
);
|
|
356
|
+
// Its own territory, and territory no module owns, both stay quiet.
|
|
357
|
+
assert.deepEqual(scope.moduleDriftFiles('lifecycle', ['modules/lifecycle/rot.js']), []);
|
|
358
|
+
assert.deepEqual(scope.moduleDriftFiles('lifecycle', ['some/unowned/path.txt']), []);
|
|
359
|
+
// A mixed diff reports only the straying file.
|
|
360
|
+
assert.deepEqual(
|
|
361
|
+
scope.moduleDriftFiles('lifecycle', ['modules/lifecycle/rot.js', SESSION_LOG, 'modules/discord/discord-bot.js']),
|
|
362
|
+
['modules/discord/discord-bot.js'],
|
|
363
|
+
);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('devsecops is deliberately NOT cross-cutting (task 1003770)', () => {
|
|
367
|
+
// The bar is "every task legitimately writes here as part of shipping". Editing
|
|
368
|
+
// .github/ or fitness.js while scoped elsewhere IS the confusion being surfaced,
|
|
369
|
+
// so this stays a signal. Widening the cross-cutting roster to devsecops would
|
|
370
|
+
// red these, which is the point: it should take a deliberate edit, not a shrug.
|
|
371
|
+
for (const f of ['.github/workflows/unit.yml', 'scripts/gds/fitness.js', 'package.json']) {
|
|
372
|
+
assert.deepEqual(scope.moduleDriftFiles('lifecycle', [f]), [f], `${f} must still read as drift`);
|
|
373
|
+
}
|
|
374
|
+
// And exactly one module is cross-cutting: every other key's territory still
|
|
375
|
+
// counts against a lifecycle-scoped task. Proven over the whole roster rather
|
|
376
|
+
// than by naming the constant, so adding a second cross-cutting module fails here.
|
|
377
|
+
const quiet = scope.moduleKeys().filter((k) => k !== 'lifecycle'
|
|
378
|
+
&& scope.moduleDriftFiles('lifecycle', scope.scopeForModule(k).globs).length === 0);
|
|
379
|
+
assert.deepEqual(quiet, ['methodology']);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test('moduleDriftFiles degrades safely on junk input', () => {
|
|
383
|
+
// ship-flow calls this with whatever the claim carries; it must never throw.
|
|
384
|
+
assert.deepEqual(scope.moduleDriftFiles('not-a-module', ['modules/dev-box/app.js']), [],
|
|
385
|
+
'an unknown key has no declared scope to stray from');
|
|
386
|
+
assert.deepEqual(scope.moduleDriftFiles('lifecycle', []), []);
|
|
387
|
+
assert.deepEqual(scope.moduleDriftFiles('lifecycle', null), []);
|
|
388
|
+
assert.deepEqual(scope.moduleDriftFiles(null, ['modules/dev-box/app.js']), []);
|
|
389
|
+
});
|
package/tests/session_caps.mjs
CHANGED
|
@@ -221,6 +221,10 @@ try {
|
|
|
221
221
|
assert.equal(fixture.upserts.length, 0);
|
|
222
222
|
});
|
|
223
223
|
} finally {
|
|
224
|
+
// Drop keep-alive sockets BEFORE close(). fetch()'s global agent holds them
|
|
225
|
+
// open, and server.close() only stops new connections — it waits on the live
|
|
226
|
+
// ones, so without this the process would linger instead of exiting.
|
|
227
|
+
if (typeof server.closeAllConnections === 'function') server.closeAllConnections();
|
|
224
228
|
await new Promise((resolve) => server.close(resolve));
|
|
225
229
|
auth.requireBuilder = orig.requireBuilder;
|
|
226
230
|
db.filterTaskIdsClaimedBy = orig.filterTaskIdsClaimedBy;
|
|
@@ -235,4 +239,15 @@ await t('the audit body cap constant is exported and sane', () => {
|
|
|
235
239
|
});
|
|
236
240
|
|
|
237
241
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
238
|
-
process.exit(
|
|
242
|
+
// NOT process.exit(). On Windows this suite aborted in roughly two runs of three
|
|
243
|
+
// — "Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c"
|
|
244
|
+
// — exiting 127 AFTER printing "9 passed, 0 failed", because the hard exit tore
|
|
245
|
+
// the loop down while fetch's handles were still closing. run-unit-tests.js reads
|
|
246
|
+
// the exit code, so a fully-passing suite reported FAILED in every Windows
|
|
247
|
+
// baseline, which trains builders to wave off real failures as environmental.
|
|
248
|
+
// Setting the code and letting the loop drain is the same cure scripts/gds/
|
|
249
|
+
// CLAUDE.md documents for the CLI scripts (cliExit) and tests/cli_exit_no_abort.mjs
|
|
250
|
+
// guards for api.js. The closeAllConnections() above is the other half: a hard
|
|
251
|
+
// exit MASKS a leaked handle, so removing it without releasing the sockets would
|
|
252
|
+
// have traded a crash for a hang.
|
|
253
|
+
process.exitCode = failed === 0 ? 0 : 1;
|