@bongos/core 1.19.945 → 1.19.946
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 +13 -13
- package/docs/module-api-changelog.md +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +1 -1
- package/scripts/gds/scope-map-guard.js +181 -1
- package/src/bongos/module-scope-map.js +67 -0
- package/src/module-api.js +1 -1
- package/tests/scope_map_guard.mjs +101 -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.946",
|
|
6
|
+
"core_contract": "1.19.946",
|
|
7
|
+
"source_commit": "427f912951779422ac61981f8d918086a2f0cc65",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-21T23:
|
|
9
|
+
"built_at": "2026-09-21T23:49:20.517Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 512,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 2878,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "3a0e9a1d401c4698c1d6c8fd6a429f0f65a2beabf547c47a910f336148776ac6",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2682,7 +2682,7 @@
|
|
|
2682
2682
|
{
|
|
2683
2683
|
"path": "docs/module-api-changelog.md",
|
|
2684
2684
|
"mode": "0000644",
|
|
2685
|
-
"sha256": "
|
|
2685
|
+
"sha256": "3be3dcca6eabd22bc94ad8c59edb8fcc56bc14cf46cbd6d315807b2247d5a636"
|
|
2686
2686
|
},
|
|
2687
2687
|
{
|
|
2688
2688
|
"path": "docs/modules-contract.md",
|
|
@@ -8007,12 +8007,12 @@
|
|
|
8007
8007
|
{
|
|
8008
8008
|
"path": "package-lock.json",
|
|
8009
8009
|
"mode": "0000644",
|
|
8010
|
-
"sha256": "
|
|
8010
|
+
"sha256": "d9bcde780bfceb4b3f8c2c818db7552a5b21cd0280e123f35a81f71032234e8f"
|
|
8011
8011
|
},
|
|
8012
8012
|
{
|
|
8013
8013
|
"path": "package.json",
|
|
8014
8014
|
"mode": "0000644",
|
|
8015
|
-
"sha256": "
|
|
8015
|
+
"sha256": "289ec4343c26cd76b78320bdacd76382bfd5e4ef75670c307ff9fb400e866de3"
|
|
8016
8016
|
},
|
|
8017
8017
|
{
|
|
8018
8018
|
"path": "public-docs/index.html",
|
|
@@ -8597,7 +8597,7 @@
|
|
|
8597
8597
|
{
|
|
8598
8598
|
"path": "scripts/gds/fitness.js",
|
|
8599
8599
|
"mode": "0000644",
|
|
8600
|
-
"sha256": "
|
|
8600
|
+
"sha256": "35e377253453c975ce7a8e420d72af718a4f8ab6d3837d39fd38c1ebd5658ddc"
|
|
8601
8601
|
},
|
|
8602
8602
|
{
|
|
8603
8603
|
"path": "scripts/gds/gate-review.js",
|
|
@@ -9127,7 +9127,7 @@
|
|
|
9127
9127
|
{
|
|
9128
9128
|
"path": "scripts/gds/scope-map-guard.js",
|
|
9129
9129
|
"mode": "0000644",
|
|
9130
|
-
"sha256": "
|
|
9130
|
+
"sha256": "39554a1c45f9d4c507df8d782cf2d928adae312110ba9a057421354d5999dc69"
|
|
9131
9131
|
},
|
|
9132
9132
|
{
|
|
9133
9133
|
"path": "scripts/gds/seam-wiring-guard.js",
|
|
@@ -9822,7 +9822,7 @@
|
|
|
9822
9822
|
{
|
|
9823
9823
|
"path": "src/bongos/module-scope-map.js",
|
|
9824
9824
|
"mode": "0000644",
|
|
9825
|
-
"sha256": "
|
|
9825
|
+
"sha256": "36da288c8087ebd653c0b208a02414ea55d09da53df22cb4fc3d281941756ec7"
|
|
9826
9826
|
},
|
|
9827
9827
|
{
|
|
9828
9828
|
"path": "src/bongos/module-submissions.js",
|
|
@@ -10017,7 +10017,7 @@
|
|
|
10017
10017
|
{
|
|
10018
10018
|
"path": "src/module-api.js",
|
|
10019
10019
|
"mode": "0000644",
|
|
10020
|
-
"sha256": "
|
|
10020
|
+
"sha256": "4301e3d88d415bed2d9146b4bd1749be2341355554f78e441793ebca16d9e24a"
|
|
10021
10021
|
},
|
|
10022
10022
|
{
|
|
10023
10023
|
"path": "src/module-loader/catalog.js",
|
|
@@ -13472,7 +13472,7 @@
|
|
|
13472
13472
|
{
|
|
13473
13473
|
"path": "tests/scope_map_guard.mjs",
|
|
13474
13474
|
"mode": "0000644",
|
|
13475
|
-
"sha256": "
|
|
13475
|
+
"sha256": "9722a12a61597d86f033d3f3a8cd93a7e99c701da54f79c8f6ac49afdea62ad3"
|
|
13476
13476
|
},
|
|
13477
13477
|
{
|
|
13478
13478
|
"path": "tests/scouting_owner_access_and_reach.mjs",
|
|
@@ -2364,5 +2364,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2364
2364
|
landed since 1.19.943 with no explicit bump. run 35665131778. (task 1002620)
|
|
2365
2365
|
1.19.945 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2366
2366
|
landed since 1.19.944 with no explicit bump. run 35668642462. (task 1002620)
|
|
2367
|
+
1.19.946 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2368
|
+
landed since 1.19.945 with no explicit bump. run 35669244197. (task 1002620)
|
|
2367
2369
|
---------------------------------------------------------------------------
|
|
2368
2370
|
```
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.946",
|
|
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.946",
|
|
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.946",
|
|
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/fitness.js
CHANGED
|
@@ -1277,7 +1277,7 @@ const CHECKS = [
|
|
|
1277
1277
|
// one (ADR 0289). Rationale + the seam→consumer table live in that file.
|
|
1278
1278
|
require('./seam-wiring-guard.js').checkSeamWiring,
|
|
1279
1279
|
require('./home-isolation-guard.js').checkHomeIsolation, // Check 34 — task 1003763: a test that sets HOME for a child without USERPROFILE isolates NOTHING on win32, so it silently stops testing what its comment says. Three tasks fixed this one site at a time. Rationale in that file.
|
|
1280
|
-
require('./scope-map-guard.js')
|
|
1280
|
+
...['checkDevsecopsOwnsItsChecks', 'checkEveryModuleDirIsScoped', 'checkNoNewUnownedRoot'].map((k) => require('./scope-map-guard.js')[k]), // Check 35 — task 1003949: a check file no MODULE_SCOPE_MAP key owns cannot be authorized by the goal scoped to own it, and the list drifts once per new check. Check 36 — task 1004132: the same defect one level up, a carved module whose own directory no key claims (modules/specialities/ shipped that way). Sharing the line per the ratchet convention above. Rationale in that file.
|
|
1281
1281
|
require('./adr-namespace.js').checkAdrNumbering, require('./exec-path-guard.js').checkExecPathsStatic, // Check 32 — task 1003371 / B50: what the server EXECUTES, out of whose tree — the blind spot all three 2026-08-29 CRITICALs shared. Rationale + reviewed exemptions live in that file. Sharing Check 31's line (the 19c/19d + 23/24 precedent above): fitness.js is AT the 1,500 ratchet and a new check cannot afford a line until task 1003825 buys it back.
|
|
1282
1282
|
];
|
|
1283
1283
|
|
|
@@ -117,4 +117,184 @@ function checkDevsecopsOwnsItsChecks() {
|
|
|
117
117
|
+ 'unless CI says otherwise (task 1003949).',
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
|
-
module
|
|
120
|
+
// PURE — every module directory on disk, by key. A module is a modules/<key>/ dir
|
|
121
|
+
// carrying module.json (the ADR 0083 manifest), which is the same structural test
|
|
122
|
+
// the loader's discoverModules() uses: a key is what has a manifest, never a
|
|
123
|
+
// hand-kept roster. Exported so the scan is testable against a fixture tree —
|
|
124
|
+
// an empty result here looks identical to a clean repo, which is the failure the
|
|
125
|
+
// sibling check above exists to refuse.
|
|
126
|
+
function moduleDirsOnDisk(modulesRoot) {
|
|
127
|
+
let entries = [];
|
|
128
|
+
try { entries = fs.readdirSync(modulesRoot, { withFileTypes: true }); } catch { return []; }
|
|
129
|
+
return entries
|
|
130
|
+
.filter((e) => e.isDirectory() && fs.existsSync(path.join(modulesRoot, e.name, 'module.json')))
|
|
131
|
+
.map((e) => e.name)
|
|
132
|
+
.sort();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// A carved module whose own directory no MODULE_SCOPE_MAP key claims (task 1004132).
|
|
136
|
+
//
|
|
137
|
+
// THE DRIFT THIS STOPS, and it is the sibling of the check above rather than a new
|
|
138
|
+
// idea: modules/specialities/ shipped a full module — manifest, db slice, routes, two
|
|
139
|
+
// migrations — with no key here at all. scopeSubset() therefore resolved every file in
|
|
140
|
+
// it to no territory, so a goal scoped to `specialities` was refused out_of_scope for
|
|
141
|
+
// touching the module it is named after. It was the ONLY on-disk module in that state,
|
|
142
|
+
// which is exactly why nobody noticed: 21 of 22 were fine.
|
|
143
|
+
//
|
|
144
|
+
// The rule is deliberately narrow. It does NOT require every repo path to have an
|
|
145
|
+
// owner — src/, scripts/ and bin/ are unowned ON PURPOSE (ADR 0086: a goal grants
|
|
146
|
+
// authority BOUNDED to module territory, and POST /tasks stays Archon-only for
|
|
147
|
+
// ungoverned creation), so a broader "every root is owned" check would encode the
|
|
148
|
+
// opposite of the design. What must hold is narrower and absolute: if a directory
|
|
149
|
+
// declares itself a module, a goal must be able to be scoped to it.
|
|
150
|
+
function checkEveryModuleDirIsScoped() {
|
|
151
|
+
const name = 'every carved module owns its own directory (task 1004132)';
|
|
152
|
+
const dirs = moduleDirsOnDisk(path.join(REPO_ROOT, 'modules'));
|
|
153
|
+
|
|
154
|
+
// Same anti-vacuity rule as the check above: this repo HAS modules, so an empty
|
|
155
|
+
// scan means the manifest test broke, not that the tree is clean.
|
|
156
|
+
if (dirs.length === 0) {
|
|
157
|
+
return {
|
|
158
|
+
name,
|
|
159
|
+
ok: false,
|
|
160
|
+
hardFail: true,
|
|
161
|
+
violations: ['modules/: found NO directory carrying a module.json. That is a broken scan, '
|
|
162
|
+
+ 'not a clean result — moduleDirsOnDisk() in scope-map-guard.js needs looking at.'],
|
|
163
|
+
warnings: [],
|
|
164
|
+
note: 'the scan produced an empty set, which cannot be right.',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const { MODULE_SCOPE_MAP } = require('../../src/bongos/module-scope-map.js');
|
|
169
|
+
const globsOf = (v) => (Array.isArray(v) ? v : (v && v.globs) || []);
|
|
170
|
+
const claimed = new Set();
|
|
171
|
+
for (const v of Object.values(MODULE_SCOPE_MAP)) {
|
|
172
|
+
// A key may claim another module's dir (a cross-cutting owner), so collect every
|
|
173
|
+
// modules/<key>/ glob from EVERY key rather than only from the same-named one.
|
|
174
|
+
for (const g of globsOf(v)) {
|
|
175
|
+
const m = /^modules\/([^/]+)\/$/.exec(g);
|
|
176
|
+
if (m) claimed.add(m[1]);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const unscoped = dirs.filter((k) => !claimed.has(k));
|
|
181
|
+
const violations = unscoped.map((k) => `modules/${k}/ carries a module.json but no MODULE_SCOPE_MAP `
|
|
182
|
+
+ `key claims it — scopeSubset() resolves the whole module to no territory, so a goal scoped to `
|
|
183
|
+
+ `'${k}' is denied out_of_scope for touching the module it is named after. Add a '${k}' key `
|
|
184
|
+
+ `owning 'modules/${k}/'.`);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
name,
|
|
188
|
+
ok: unscoped.length === 0,
|
|
189
|
+
hardFail: unscoped.length > 0,
|
|
190
|
+
violations,
|
|
191
|
+
warnings: [],
|
|
192
|
+
note: `${dirs.length} carved module(s) on disk, each claimed by a MODULE_SCOPE_MAP key.`,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// PURE — every tracked top-level root, as `name` or `name/`. Exported so the scan
|
|
197
|
+
// is testable against a fixture list rather than only against this repo.
|
|
198
|
+
function trackedRoots(files) {
|
|
199
|
+
const out = new Set();
|
|
200
|
+
for (const f of files || []) {
|
|
201
|
+
if (!f) continue;
|
|
202
|
+
const i = f.indexOf('/');
|
|
203
|
+
out.add(i === -1 ? f : `${f.slice(0, i)}/`);
|
|
204
|
+
}
|
|
205
|
+
return [...out].sort();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// PURE — does ANY glob reach into this root? A root is "carved" the moment one
|
|
209
|
+
// does; src/ and scripts/ are partially owned and that is the normal in-progress
|
|
210
|
+
// state ADR 0086 §4 describes ("auto-tightens as ADR 0083 moves code into
|
|
211
|
+
// modules/<key>/"). What this answers is the narrower question: is this root
|
|
212
|
+
// territory the scope map has never heard of at all?
|
|
213
|
+
function rootIsTouched(root, globs) {
|
|
214
|
+
return (globs || []).some((g) => g === root || g.startsWith(root) || root.startsWith(g));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// New unowned territory, and stale entries in the record of the old (task 1004132).
|
|
218
|
+
//
|
|
219
|
+
// THE GAP THIS CLOSES, and it is the one the grade round caught me leaving open.
|
|
220
|
+
// I had argued that because the core is unowned BY DESIGN (ADR 0086: authority is
|
|
221
|
+
// bounded to module territory, POST /tasks stays Archon-only for ungoverned
|
|
222
|
+
// creation), no general check should exist. That is half right and wholly
|
|
223
|
+
// insufficient: the design says WHICH roots may be unowned, it does not stop the
|
|
224
|
+
// set growing. Without this, a new top-level directory lands tomorrow, no module
|
|
225
|
+
// claims it, and nothing anywhere fails — the exact shape that let
|
|
226
|
+
// modules/specialities/ sit unowned for its whole life.
|
|
227
|
+
//
|
|
228
|
+
// So the decision is recorded as DATA (UNOWNED_ROOTS in module-scope-map.js) and
|
|
229
|
+
// this check enforces it in BOTH directions, the discipline
|
|
230
|
+
// tests/host_topology_skips.mjs uses for its own allowlist:
|
|
231
|
+
// - a root that is neither carved nor listed -> new territory, fail
|
|
232
|
+
// - a listed root that is now carved or gone -> stale entry, fail
|
|
233
|
+
// A one-directional check would let the list rot into a rubber stamp.
|
|
234
|
+
function checkNoNewUnownedRoot(fileList) {
|
|
235
|
+
const name = 'no new unowned top-level root (task 1004132)';
|
|
236
|
+
let files = fileList;
|
|
237
|
+
if (!files) {
|
|
238
|
+
files = require('node:child_process')
|
|
239
|
+
.execFileSync('git', ['ls-files'], { cwd: REPO_ROOT, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 })
|
|
240
|
+
.trim().split('\n').filter(Boolean);
|
|
241
|
+
}
|
|
242
|
+
const roots = trackedRoots(files);
|
|
243
|
+
|
|
244
|
+
// Same anti-vacuity rule as its two siblings: this repo HAS tracked files, so an
|
|
245
|
+
// empty scan is a broken `git ls-files`, not a clean tree.
|
|
246
|
+
if (roots.length === 0) {
|
|
247
|
+
return {
|
|
248
|
+
name,
|
|
249
|
+
ok: false,
|
|
250
|
+
hardFail: true,
|
|
251
|
+
violations: ['git ls-files returned NO tracked files. That is a broken scan, not a clean '
|
|
252
|
+
+ 'result — trackedRoots() in scope-map-guard.js needs looking at.'],
|
|
253
|
+
warnings: [],
|
|
254
|
+
note: 'the scan produced an empty set, which cannot be right.',
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const map = require('../../src/bongos/module-scope-map.js');
|
|
259
|
+
const globs = [...map.globsForModules(map.moduleKeys()), ...map.SHARED_GLOBS];
|
|
260
|
+
const listed = map.UNOWNED_ROOTS || {};
|
|
261
|
+
|
|
262
|
+
const violations = [];
|
|
263
|
+
for (const r of roots) {
|
|
264
|
+
if (rootIsTouched(r, globs)) {
|
|
265
|
+
if (Object.prototype.hasOwnProperty.call(listed, r)) {
|
|
266
|
+
violations.push(`'${r}' is listed in UNOWNED_ROOTS but a module glob now reaches it — the `
|
|
267
|
+
+ 'entry is stale. Delete it; a root leaving that list is the map tightening as intended.');
|
|
268
|
+
}
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (!Object.prototype.hasOwnProperty.call(listed, r)) {
|
|
272
|
+
violations.push(`'${r}' is tracked but NO module glob and no SHARED_GLOBS entry reaches it, and `
|
|
273
|
+
+ 'it is not in UNOWNED_ROOTS. New unowned territory: no goal can ever authorize a task that '
|
|
274
|
+
+ `touches it. Give it an owning module, or add it to UNOWNED_ROOTS in module-scope-map.js `
|
|
275
|
+
+ 'with the reason it has none.');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
for (const r of Object.keys(listed)) {
|
|
279
|
+
if (!roots.includes(r)) {
|
|
280
|
+
violations.push(`UNOWNED_ROOTS lists '${r}', which is not a tracked root any more — stale entry, `
|
|
281
|
+
+ 'delete it so the list keeps meaning something.');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
name,
|
|
287
|
+
ok: violations.length === 0,
|
|
288
|
+
hardFail: violations.length > 0,
|
|
289
|
+
violations,
|
|
290
|
+
warnings: [],
|
|
291
|
+
note: `${roots.length} tracked root(s); ${Object.keys(listed).length} recorded as deliberately `
|
|
292
|
+
+ 'unowned (ADR 0086 — core is Archon territory). Shrink-only: an entry may leave, not appear silently.',
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
module.exports = {
|
|
297
|
+
checkDevsecopsOwnsItsChecks, siblingRequires, unownedBy, FITNESS,
|
|
298
|
+
checkEveryModuleDirIsScoped, moduleDirsOnDisk,
|
|
299
|
+
checkNoNewUnownedRoot, trackedRoots, rootIsTouched,
|
|
300
|
+
};
|
|
@@ -145,6 +145,16 @@ const MODULE_GLOBS = {
|
|
|
145
145
|
'modules/ideas/',
|
|
146
146
|
'scripts/gds/capture.js', 'scripts/gds/triage.js', 'scripts/gds/render-ideas.js',
|
|
147
147
|
],
|
|
148
|
+
// Role sub-specialities (task 1003855, ADR 0296). Carved into modules/specialities/
|
|
149
|
+
// but never given a key here, so every file it owns — module.json, db.js, routes,
|
|
150
|
+
// its two migrations — resolved to NO module territory: a goal scoped to
|
|
151
|
+
// `specialities` was denied out_of_scope for touching the module it is named after
|
|
152
|
+
// (found by task 1004132). Every OTHER on-disk module owns its own directory; this
|
|
153
|
+
// was the only one that did not, and checkEveryModuleDirIsScoped in
|
|
154
|
+
// scripts/gds/scope-map-guard.js now fails CI if a new carve repeats it.
|
|
155
|
+
specialities: [
|
|
156
|
+
'modules/specialities/',
|
|
157
|
+
],
|
|
148
158
|
// --- builder prefs (wandering/skill/sound/render) + builder-needs ---
|
|
149
159
|
// Carved into modules/builder-settings/ (BV1.R80 / ADR 0093 §1): the five pref
|
|
150
160
|
// libs + builder-needs + their db slice + the `/me/*-prefs` routes + the
|
|
@@ -521,6 +531,62 @@ const SHARED_GLOBS = Object.freeze(
|
|
|
521
531
|
SHARED_GLOBS_RAW.filter((g) => !touchesOverlap([g], PROTECTED_GLOBS)),
|
|
522
532
|
);
|
|
523
533
|
|
|
534
|
+
// ── Roots no module glob touches, and why (task 1004132) ────────────────────────
|
|
535
|
+
//
|
|
536
|
+
// This is the RECORDED DECISION behind "the core is owned by nobody", not a note
|
|
537
|
+
// about it. `checkNoNewUnownedRoot` in scripts/gds/scope-map-guard.js reads this
|
|
538
|
+
// map and fails CI BOTH ways: a tracked root that appears here-less is new unowned
|
|
539
|
+
// territory, and an entry whose root became owned (or stopped existing) is stale.
|
|
540
|
+
//
|
|
541
|
+
// WHY THESE ARE ALLOWED TO BE UNOWNED. Per ADR 0086 a goal grants authority
|
|
542
|
+
// BOUNDED to module territory, and `POST /tasks` stays Archon-only for ungoverned
|
|
543
|
+
// creation. So a path with no module owner is not homeless — it is reachable only
|
|
544
|
+
// through the Archon door, which is the safety model, not a gap in it. What must
|
|
545
|
+
// not happen is that set GROWING silently, which is what this freezes.
|
|
546
|
+
//
|
|
547
|
+
// SHRINK-ONLY, like the fitness ratchets. ADR 0086 §4 expects the map to
|
|
548
|
+
// "auto-tighten as ADR 0083 moves code into modules/<key>/" — so an entry leaving
|
|
549
|
+
// this list is the system working. Adding one needs a stated reason, here, in the
|
|
550
|
+
// same commit that adds the root.
|
|
551
|
+
//
|
|
552
|
+
// A `review` reason is an HONEST placeholder, not an endorsement: it marks a root
|
|
553
|
+
// that probably does have a natural owner but whose assignment is its own decision.
|
|
554
|
+
// Assigning nineteen roots in one pass is the one-at-a-time churn task 1004132 was
|
|
555
|
+
// filed to avoid; freezing the set and naming the arguable ones is the cheap half.
|
|
556
|
+
const UNOWNED_ROOTS = Object.freeze({
|
|
557
|
+
// Generated packaging manifests — written by package-core.js, never hand-edited.
|
|
558
|
+
'.bongos-core.json': 'generated package manifest (scripts/gds/package-core.js)',
|
|
559
|
+
'.cloudbongos-core.json': 'generated package manifest (scripts/gds/package-core.js)',
|
|
560
|
+
// Container build + repo-level dotfiles. review: devsecops already owns the
|
|
561
|
+
// neighbouring .npmrc / .gitattributes / package.json, so these plausibly belong
|
|
562
|
+
// to it — but that is a scope-wall change and wants its own decision.
|
|
563
|
+
'.dockerignore': 'review: container build, plausibly devsecops',
|
|
564
|
+
'Dockerfile': 'review: container build, plausibly devsecops',
|
|
565
|
+
'docker-compose.yml': 'review: container build, plausibly devsecops',
|
|
566
|
+
'docker-entrypoint.sh': 'review: container build, plausibly devsecops',
|
|
567
|
+
'.gitignore': 'review: repo-level dotfile, plausibly devsecops',
|
|
568
|
+
'.env.local.example': 'review: repo-level dotfile, plausibly devsecops',
|
|
569
|
+
// Design tooling checked into the tree. review: plausibly ui-design.
|
|
570
|
+
'.impeccable/': 'review: design tooling, plausibly ui-design',
|
|
571
|
+
// Governance + legal. Deliberately not methodology: methodology owns docs/, and
|
|
572
|
+
// these are the project's constitutional documents rather than its handbook.
|
|
573
|
+
'LICENSE': 'project legal text',
|
|
574
|
+
'CODE_OF_CONDUCT.md': 'project governance text',
|
|
575
|
+
'CONTRIBUTING.md': 'project governance text',
|
|
576
|
+
'GOVERNANCE.md': 'project governance text',
|
|
577
|
+
// Top-level product/design statements, read by humans before any module matters.
|
|
578
|
+
'README.md': 'repo front door',
|
|
579
|
+
'DESIGN.md': 'top-level design statement',
|
|
580
|
+
'PRODUCT.md': 'top-level product statement',
|
|
581
|
+
// The core itself. bin/ is the CLI entrypoint and clients/ is the GENERATED API
|
|
582
|
+
// client (scripts/gds/gen-api-client.js); public-docs/ is the published site.
|
|
583
|
+
// These are core, and core is Archon territory by ADR 0086 — the primary case
|
|
584
|
+
// this list exists to record.
|
|
585
|
+
'bin/': 'core CLI entrypoint — Archon territory (ADR 0086)',
|
|
586
|
+
'clients/': 'generated API client (scripts/gds/gen-api-client.js)',
|
|
587
|
+
'public-docs/': 'published docs site — core',
|
|
588
|
+
});
|
|
589
|
+
|
|
524
590
|
// A CLI ENTRYPOINT — a single executable script under scripts/, e.g.
|
|
525
591
|
// 'scripts/gds/setup.js'. Structural, not a list: it is a path test, so no roster
|
|
526
592
|
// is hand-maintained here and nothing has to be kept in step with PROTECTED_GLOBS.
|
|
@@ -676,6 +742,7 @@ function moduleDriftFiles(moduleKey, files) {
|
|
|
676
742
|
}
|
|
677
743
|
|
|
678
744
|
module.exports = {
|
|
745
|
+
UNOWNED_ROOTS,
|
|
679
746
|
MODULE_GLOBS,
|
|
680
747
|
MODULE_SCOPE_MAP,
|
|
681
748
|
SHARED_GLOBS,
|
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.946'; // 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');
|
|
@@ -17,7 +17,11 @@ import assert from 'node:assert/strict';
|
|
|
17
17
|
import { createRequire } from 'node:module';
|
|
18
18
|
|
|
19
19
|
const require = createRequire(import.meta.url);
|
|
20
|
-
const { checkDevsecopsOwnsItsChecks, siblingRequires
|
|
20
|
+
const { checkDevsecopsOwnsItsChecks, siblingRequires, checkEveryModuleDirIsScoped, moduleDirsOnDisk,
|
|
21
|
+
checkNoNewUnownedRoot, trackedRoots, rootIsTouched } = require('../scripts/gds/scope-map-guard.js');
|
|
22
|
+
const fs = require('node:fs');
|
|
23
|
+
const os = require('node:os');
|
|
24
|
+
const path = require('node:path');
|
|
21
25
|
|
|
22
26
|
test('every file fitness.js requires is owned by some MODULE_SCOPE_MAP key', () => {
|
|
23
27
|
const r = checkDevsecopsOwnsItsChecks();
|
|
@@ -80,3 +84,99 @@ test('the PASS note carries the count, so the check cannot look green while doin
|
|
|
80
84
|
const n = Number(r.note.match(/^(\d+) file/)[1]);
|
|
81
85
|
assert.ok(n > 14, `expected more than the fourteen the filing task listed, saw ${n}`);
|
|
82
86
|
});
|
|
87
|
+
|
|
88
|
+
// ── the same defect one level up: a carved module nobody can scope to (task 1004132) ──
|
|
89
|
+
//
|
|
90
|
+
// modules/specialities/ shipped a whole module — manifest, db slice, routes, two
|
|
91
|
+
// migrations — with no MODULE_SCOPE_MAP key, so a goal scoped to `specialities` was
|
|
92
|
+
// refused out_of_scope for touching the module it is named after. 21 of the 22 modules
|
|
93
|
+
// were fine, which is exactly why it went unseen.
|
|
94
|
+
|
|
95
|
+
test('every carved module on disk is claimed by some MODULE_SCOPE_MAP key', () => {
|
|
96
|
+
const r = checkEveryModuleDirIsScoped();
|
|
97
|
+
assert.equal(r.ok, true, r.violations.join('\n'));
|
|
98
|
+
assert.equal(r.hardFail, false);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('specialities specifically — the module this check was written for', () => {
|
|
102
|
+
const { MODULE_SCOPE_MAP } = require('../src/bongos/module-scope-map.js');
|
|
103
|
+
const globsOf = (v) => (Array.isArray(v) ? v : (v && v.globs) || []);
|
|
104
|
+
const all = Object.values(MODULE_SCOPE_MAP).flatMap(globsOf);
|
|
105
|
+
assert.ok(all.includes('modules/specialities/'),
|
|
106
|
+
'modules/specialities/ lost its owner again — a goal scoped to specialities cannot touch it');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('moduleDirsOnDisk counts a manifest, not a directory', () => {
|
|
110
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'bongos-modules-'));
|
|
111
|
+
fs.mkdirSync(path.join(root, 'real'));
|
|
112
|
+
fs.writeFileSync(path.join(root, 'real', 'module.json'), '{"key":"real"}');
|
|
113
|
+
fs.mkdirSync(path.join(root, 'not-a-module')); // no manifest
|
|
114
|
+
fs.writeFileSync(path.join(root, 'stray.txt'), 'x'); // not a directory
|
|
115
|
+
assert.deepEqual(moduleDirsOnDisk(root), ['real']);
|
|
116
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('the scan cannot pass vacuously — an empty modules/ is a BROKEN scan, not a clean one', () => {
|
|
120
|
+
// The failure this mirrors is the one the sibling check above documents: a scanner
|
|
121
|
+
// that matches nothing reports a healthy repo, and healthy is what broken looks like.
|
|
122
|
+
const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'bongos-empty-'));
|
|
123
|
+
assert.deepEqual(moduleDirsOnDisk(empty), []);
|
|
124
|
+
assert.deepEqual(moduleDirsOnDisk(path.join(empty, 'does-not-exist')), [],
|
|
125
|
+
'a missing modules/ must return empty rather than throw — the check turns that into a hard fail');
|
|
126
|
+
fs.rmSync(empty, { recursive: true, force: true });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ── the record of what is deliberately unowned, and that it cannot rot (task 1004132) ──
|
|
130
|
+
//
|
|
131
|
+
// The decision "the core is owned by nobody" is DATA (UNOWNED_ROOTS), not prose, so
|
|
132
|
+
// that it can be enforced. A one-directional check would let the list become a
|
|
133
|
+
// rubber stamp, so both directions are pinned here.
|
|
134
|
+
|
|
135
|
+
test('the repo has no unowned root that UNOWNED_ROOTS does not account for', () => {
|
|
136
|
+
const r = checkNoNewUnownedRoot();
|
|
137
|
+
assert.equal(r.ok, true, r.violations.join('\n'));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('a NEW unowned root fails — the growth this exists to stop', () => {
|
|
141
|
+
const files = require('node:child_process')
|
|
142
|
+
.execFileSync('git', ['ls-files'], { encoding: 'utf8' }).trim().split('\n');
|
|
143
|
+
const r = checkNoNewUnownedRoot([...files, 'vendor/thing.js']);
|
|
144
|
+
assert.equal(r.ok, false);
|
|
145
|
+
assert.equal(r.hardFail, true);
|
|
146
|
+
assert.match(r.violations.join('\n'), /'vendor\/' is tracked but NO module glob/);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('a STALE entry fails too — the list may shrink, never lie', () => {
|
|
150
|
+
const files = require('node:child_process')
|
|
151
|
+
.execFileSync('git', ['ls-files'], { encoding: 'utf8' }).trim().split('\n');
|
|
152
|
+
const r = checkNoNewUnownedRoot(files.filter((f) => f !== 'LICENSE'));
|
|
153
|
+
assert.equal(r.ok, false);
|
|
154
|
+
assert.match(r.violations.join('\n'), /lists 'LICENSE', which is not a tracked root any more/);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('the scan cannot pass vacuously', () => {
|
|
158
|
+
const r = checkNoNewUnownedRoot([]);
|
|
159
|
+
assert.equal(r.ok, false);
|
|
160
|
+
assert.equal(r.hardFail, true);
|
|
161
|
+
assert.match(r.violations.join('\n'), /broken scan, not a clean result/);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('trackedRoots collapses paths to roots; rootIsTouched sees a PARTIAL carve', () => {
|
|
165
|
+
assert.deepEqual(trackedRoots(['src/a/b.js', 'src/c.js', 'LICENSE', 'bin/x']),
|
|
166
|
+
['LICENSE', 'bin/', 'src/']);
|
|
167
|
+
// src/ is partially owned (one file claimed) — that is the normal in-progress
|
|
168
|
+
// state of ADR 0083's carve, and must NOT read as unowned territory.
|
|
169
|
+
assert.equal(rootIsTouched('src/', ['src/bongos/llm-pricing.js']), true);
|
|
170
|
+
assert.equal(rootIsTouched('vendor/', ['src/bongos/llm-pricing.js']), false);
|
|
171
|
+
// A directory glob covering the whole root counts.
|
|
172
|
+
assert.equal(rootIsTouched('modules/', ['modules/ideas/']), true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('every UNOWNED_ROOTS entry carries a reason', () => {
|
|
176
|
+
const { UNOWNED_ROOTS } = require('../src/bongos/module-scope-map.js');
|
|
177
|
+
for (const [root, reason] of Object.entries(UNOWNED_ROOTS)) {
|
|
178
|
+
assert.equal(typeof reason, 'string', `${root} has no reason`);
|
|
179
|
+
assert.ok(reason.trim().length > 8,
|
|
180
|
+
`${root}'s reason is too thin to justify an unowned root: ${JSON.stringify(reason)}`);
|
|
181
|
+
}
|
|
182
|
+
});
|