@bongos/core 1.19.970 → 1.19.972
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 +64 -29
- package/docs/api/openapi.json +1 -1
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +151 -152
- package/docs/copy-registry.json +193 -212
- package/docs/module-api-changelog.md +4 -0
- package/modules/copy-desk/proposals.js +1 -1
- package/modules/hall-ui/public/goals-page.js +6 -0
- package/modules/hall-ui/public/goals-render.js +16 -0
- package/modules/hall-ui/public/mine-lens.js +172 -0
- package/modules/hall-ui/public/work-summary.js +90 -0
- package/modules/hall-ui/public/work.css +50 -0
- package/modules/hall-ui/public/work.html +24 -0
- package/modules/hall-ui/public/work.js +38 -51
- package/modules/lifecycle/db-goals.js +20 -3
- package/modules/lifecycle/routes/goals.js +2 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/claim.js +1 -1
- package/scripts/gds/fitness.js +90 -0
- package/scripts/gds/probe-cache.js +0 -0
- package/scripts/gds/scope-roster-guard.js +118 -0
- package/src/bongos/module-scope-map.js +1 -0
- package/src/module-api.js +1 -1
- package/tests/copy_inventory.mjs +0 -0
- package/tests/fitness_no_raw_nul.mjs +113 -0
- package/tests/fitness_scope_roster.mjs +117 -0
- package/tests/goal_mine_lens.mjs +233 -0
- package/tests/goal_tier.mjs +30 -4
- package/tests/hall_mine_lens.mjs +381 -0
|
@@ -395,18 +395,35 @@ async function getGoalDependencies(goalId, deps = {}) {
|
|
|
395
395
|
// table to `g` would have been tidier and would have rewritten four lines whose
|
|
396
396
|
// literal text tests/goal_tier.mjs pins ("priority-first, then stable
|
|
397
397
|
// ordering"). An additive change should read as additive in the diff.
|
|
398
|
-
|
|
398
|
+
//
|
|
399
|
+
// `viewerId` adds `viewer_is_member` — is the READER of this list one of this
|
|
400
|
+
// goal's members? The "My goals" lens on the goal board used to answer that from
|
|
401
|
+
// the per-goal DETAIL prefetch, which meant the lens silently returned NOTHING
|
|
402
|
+
// whenever that best-effort fan-out had not landed (or had failed), and the Tasks
|
|
403
|
+
// board could not ask the question at all without N extra requests. One EXISTS on
|
|
404
|
+
// a list the client already fetches answers it for both.
|
|
405
|
+
//
|
|
406
|
+
// `null` viewerId yields `false` everywhere, never NULL: an unauthenticated or
|
|
407
|
+
// identity-less read gets a well-formed row whose answer is "not yours", which is
|
|
408
|
+
// the truthful answer and keeps the wire shape one type. EXISTS (not a LEFT JOIN)
|
|
409
|
+
// for the `admitted` reason directly above — goal_members is keyed
|
|
410
|
+
// (goal_id, builder_id) so a join could not duplicate today, but the shape of the
|
|
411
|
+
// query should not depend on that.
|
|
412
|
+
async function listGoals({ versionId = null, status = null, limit = null, viewerId = null } = {}, deps = {}) {
|
|
399
413
|
const activePool = (deps.pool && process.env.NODE_ENV === 'test') ? deps.pool : pool;
|
|
400
414
|
const { rows } = await activePool.query(
|
|
401
415
|
`SELECT ${GOAL_COLS},
|
|
402
416
|
EXISTS (SELECT 1 FROM lifecycle_goal_version_admissions a
|
|
403
|
-
WHERE a.goal_id = goals.id) AS admitted
|
|
417
|
+
WHERE a.goal_id = goals.id) AS admitted,
|
|
418
|
+
($4::bigint IS NOT NULL
|
|
419
|
+
AND EXISTS (SELECT 1 FROM goal_members gm
|
|
420
|
+
WHERE gm.goal_id = goals.id AND gm.builder_id = $4::bigint)) AS viewer_is_member
|
|
404
421
|
FROM goals
|
|
405
422
|
WHERE ($1::text IS NULL OR version_id = $1)
|
|
406
423
|
AND ($2::text IS NULL OR status = $2)
|
|
407
424
|
ORDER BY version_id, sort_order, created_at, id
|
|
408
425
|
LIMIT $3::int`,
|
|
409
|
-
[versionId, status, limit]
|
|
426
|
+
[versionId, status, limit, viewerId]
|
|
410
427
|
);
|
|
411
428
|
return rows;
|
|
412
429
|
}
|
|
@@ -215,7 +215,7 @@ module.exports = function buildGoalsRouter() {
|
|
|
215
215
|
// rank: any-builder — GET /goals/graph, the goal board's map (task 1003479, ADR 0227); before /goals/:id.
|
|
216
216
|
router.get('/goals/graph', auth.requireBuilder, goalGraph.goalGraphHandler({ log }));
|
|
217
217
|
|
|
218
|
-
// GET /goals?version=&status= — list goals (read).
|
|
218
|
+
// GET /goals?version=&status= — list goals (read); each row carries `viewer_is_member` for the reader.
|
|
219
219
|
// rank: any-builder — viewing the goal board.
|
|
220
220
|
router.get('/goals', auth.requireBuilder, async (req, res) => {
|
|
221
221
|
const { version, status } = req.query;
|
|
@@ -223,7 +223,7 @@ module.exports = function buildGoalsRouter() {
|
|
|
223
223
|
return res.fail('bad_status', 400, { valid: VALID_GOAL_STATUSES });
|
|
224
224
|
}
|
|
225
225
|
try {
|
|
226
|
-
const goals = await db.listGoals({ versionId: version || null, status: status || null });
|
|
226
|
+
const goals = await db.listGoals({ versionId: version || null, status: status || null, viewerId: req.builder.id });
|
|
227
227
|
// task 1003275: ONE category query decorates the whole board (never a join
|
|
228
228
|
// per goal). Purely a label for the reader — nothing downstream branches on it.
|
|
229
229
|
const byId = await goalCategory.categoryIndex();
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.972",
|
|
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.972",
|
|
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.972",
|
|
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/claim.js
CHANGED
|
@@ -310,7 +310,7 @@ const MAX_LISTED_TASKS = 20;
|
|
|
310
310
|
function clip(s, max = 160) {
|
|
311
311
|
if (s == null) return '';
|
|
312
312
|
// eslint-disable-next-line no-control-regex
|
|
313
|
-
const flat = String(s).replace(/[
|
|
313
|
+
const flat = String(s).replace(/[\u0000--]+/g, ' ').trim();
|
|
314
314
|
return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
|
|
315
315
|
}
|
|
316
316
|
|
package/scripts/gds/fitness.js
CHANGED
|
@@ -1204,8 +1204,94 @@ function checkSkillFrontmatter() {
|
|
|
1204
1204
|
};
|
|
1205
1205
|
}
|
|
1206
1206
|
|
|
1207
|
+
// ============================================================================
|
|
1208
|
+
// Check 35 — no raw NUL byte in tracked publishable text (task 1002854)
|
|
1209
|
+
// ============================================================================
|
|
1210
|
+
//
|
|
1211
|
+
// scripts/public-mirror-export.js decides binary-vs-text by sniffing for a NUL byte in
|
|
1212
|
+
// the first 8 KiB (`isBinary`, ~line 86), and `redactBlob` skips `redactText` entirely
|
|
1213
|
+
// for anything it classifies binary. So a TEXT source containing a raw NUL is published
|
|
1214
|
+
// to the public AGPL mirror WITHOUT REDACTION.
|
|
1215
|
+
//
|
|
1216
|
+
// WHAT THIS IS AND IS NOT. The binary branch is not unguarded: it still leak-scans the
|
|
1217
|
+
// blob's latin1 view, so an embedded instance literal trips the gate. But the gate uses
|
|
1218
|
+
// `sensitiveLiterals` — raw substrings — while `redactText` applies the full `rules` set.
|
|
1219
|
+
// The exposure is therefore the DELTA between them: anything redaction would have
|
|
1220
|
+
// rewritten but the literal gate does not recognise ships verbatim. That is a latent
|
|
1221
|
+
// unredacted-publish, not a live one, which is why this is a lint rather than a fix.
|
|
1222
|
+
//
|
|
1223
|
+
// THE CLASS RECURS, WHICH IS THE ARGUMENT FOR A LINT RATHER THAN ANOTHER FIX.
|
|
1224
|
+
// Task 1703 found and fixed three instances, all the same mistake: a raw NUL used as a
|
|
1225
|
+
// join delimiter instead of the `\u0000` escape. Task 1002507 fixed a fourth in session
|
|
1226
|
+
// digests. Open idea 1000523 covers a fifth (search-ingest rollback). This check found
|
|
1227
|
+
// four more — modules/copy-desk/proposals.js, scripts/gds/claim.js,
|
|
1228
|
+
// scripts/gds/probe-cache.js and tests/copy_inventory.mjs — fixed in the same commit so
|
|
1229
|
+
// the check lands green. Nothing prevented any of them; nothing would prevent the next.
|
|
1230
|
+
//
|
|
1231
|
+
// The fix is always the same and never changes behaviour: `\u0000` compiles to exactly
|
|
1232
|
+
// the same string, so hashes, joins and regex ranges are byte-identical at runtime. Only
|
|
1233
|
+
// the SOURCE stops carrying the byte.
|
|
1234
|
+
//
|
|
1235
|
+
// SCOPE. Tracked files only (git ls-files — an untracked scratch file is not published),
|
|
1236
|
+
// with publishable text extensions. Binary formats are out of scope by definition: a PNG
|
|
1237
|
+
// is *supposed* to contain NULs and the mirror is right to pass it through.
|
|
1238
|
+
const NUL_TEXT_EXTS = ['.js', '.mjs', '.cjs', '.mts', '.md', '.sql', '.json'];
|
|
1239
|
+
|
|
1240
|
+
// Exemptions must be justified, not just listed. There are none today, and that is the
|
|
1241
|
+
// preferred state: the fix is a one-character escape with no behavioural cost, so
|
|
1242
|
+
// "exempt it" should always lose to "escape it". Kept as a named, empty constant so a
|
|
1243
|
+
// future addition is a visible, reviewable edit rather than a quiet change to a regex.
|
|
1244
|
+
const NUL_EXEMPT_PATHS = [];
|
|
1245
|
+
|
|
1246
|
+
function scanForRawNul(root, { listFiles, readFile } = {}) {
|
|
1247
|
+
const list = listFiles || (() => execFileSync('git', ['ls-files', '-z'], {
|
|
1248
|
+
cwd: root, encoding: 'latin1', maxBuffer: 1 << 26,
|
|
1249
|
+
}).split('\u0000').filter(Boolean));
|
|
1250
|
+
const read = readFile || ((f) => fs.readFileSync(path.join(root, f)));
|
|
1251
|
+
|
|
1252
|
+
const hits = [];
|
|
1253
|
+
for (const f of list()) {
|
|
1254
|
+
if (!NUL_TEXT_EXTS.includes(path.extname(f))) continue;
|
|
1255
|
+
if (NUL_EXEMPT_PATHS.some((e) => f === e || f.startsWith(e))) continue;
|
|
1256
|
+
let buf;
|
|
1257
|
+
try { buf = read(f); } catch (_) { continue; } // deleted-but-tracked, or unreadable
|
|
1258
|
+
const at = buf.indexOf(0);
|
|
1259
|
+
if (at === -1) continue;
|
|
1260
|
+
// Report the LINE, not the byte offset — a byte offset is not actionable.
|
|
1261
|
+
const line = buf.slice(0, at).toString('latin1').split('\n').length;
|
|
1262
|
+
hits.push(`${f}:${line} — raw NUL byte; use the \\u0000 escape (same string at runtime, no NUL in source)`);
|
|
1263
|
+
}
|
|
1264
|
+
return hits;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function checkNoRawNulInText() {
|
|
1268
|
+
let violations = [];
|
|
1269
|
+
let note = '';
|
|
1270
|
+
try {
|
|
1271
|
+
violations = scanForRawNul(REPO_ROOT);
|
|
1272
|
+
note = `tracked ${NUL_TEXT_EXTS.join('/')} sources carry no raw NUL byte, so none can be misclassified `
|
|
1273
|
+
+ 'as binary and skip public-mirror redaction (task 1002854).';
|
|
1274
|
+
} catch (e) {
|
|
1275
|
+
// Never let the scan itself fail the build — a git failure is not a boundary breach.
|
|
1276
|
+
return {
|
|
1277
|
+
name: 'no raw NUL byte in tracked publishable text (task 1002854)',
|
|
1278
|
+
ok: true, hardFail: false, violations: [], warnings: [`skipped: ${e.message}`],
|
|
1279
|
+
note: 'could not list tracked files — check skipped.',
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
return {
|
|
1283
|
+
name: 'no raw NUL byte in tracked publishable text (task 1002854)',
|
|
1284
|
+
ok: violations.length === 0,
|
|
1285
|
+
hardFail: violations.length > 0,
|
|
1286
|
+
violations,
|
|
1287
|
+
warnings: [],
|
|
1288
|
+
note,
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1207
1292
|
const CHECKS = [
|
|
1208
1293
|
checkCoreHostBoundary,
|
|
1294
|
+
checkNoRawNulInText, // Check 35 — task 1002854: a raw NUL makes the mirror skip redaction
|
|
1209
1295
|
checkClaudeMdBudget,
|
|
1210
1296
|
// Check 32 — task 1002990 / criterion wa6-kernel-and-packs: the root CLAUDE.md is
|
|
1211
1297
|
// now a role-neutral KERNEL and each craft's playbook lives in a pack under
|
|
@@ -1222,6 +1308,9 @@ const CHECKS = [
|
|
|
1222
1308
|
checkCoreNeverImportsModule,
|
|
1223
1309
|
checkModulesImportOnlyApi,
|
|
1224
1310
|
checkScopeMapImportsProtected,
|
|
1311
|
+
// Check 36 — task 1002900: a carved module with no roster key walls nothing. Extracted
|
|
1312
|
+
// to its own file (the role-pack-guard.js precedent) to keep fitness.js under the size ratchet.
|
|
1313
|
+
() => require('./scope-roster-guard.js').checkScopeRosterCoversModules({ repoRoot: REPO_ROOT, discoveredModuleKeys }),
|
|
1225
1314
|
checkCoreCarriesNoModuleCode,
|
|
1226
1315
|
checkKernelImportsNoDomain,
|
|
1227
1316
|
checkModuleMigrationsAdditive,
|
|
@@ -1307,6 +1396,7 @@ function main() {
|
|
|
1307
1396
|
if (require.main === module) main();
|
|
1308
1397
|
|
|
1309
1398
|
module.exports = {
|
|
1399
|
+
scanForRawNul,
|
|
1310
1400
|
checkVocabularyWall, VOCAB_ALLOW, VOCAB_CITATION, VOCAB_EXTS, vocabFilesToScan,
|
|
1311
1401
|
checkGoalMembershipVocabulary, GOAL_VOCAB_SURFACE, checkGovernmentRenameIdentity, RENAME_IDENTITY_RE, checkPrelaunchVocabulary, PRELAUNCH_SURFACE, // 19c task 1003120, 19d task 1003152
|
|
1312
1402
|
runAll,
|
|
Binary file
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// scripts/gds/scope-roster-guard.js
|
|
2
|
+
//
|
|
3
|
+
// WHAT. The fitness check behind the module scope roster (task 1002900): every module
|
|
4
|
+
// discovered at modules/<key>/module.json must have a covering key in MODULE_GLOBS
|
|
5
|
+
// (src/bongos/module-scope-map.js).
|
|
6
|
+
//
|
|
7
|
+
// WHY IT IS A HARD FAIL, when the runtime symptom is a denial rather than a crash.
|
|
8
|
+
// The roster is maintained by HAND. A module lands, nobody adds the entry, and any goal
|
|
9
|
+
// scoped to that work hits an authorize-NOTHING wall: goal-scope-check.check() denies
|
|
10
|
+
// with empty_scope / unknown_scope. Nothing announces it — it surfaces only when someone
|
|
11
|
+
// tries to set a goal's wall and cannot, which is long after the module landed and far
|
|
12
|
+
// from the edit that caused it. CI is the only place the gap is visible at the moment it
|
|
13
|
+
// is created.
|
|
14
|
+
//
|
|
15
|
+
// It has happened three times by hand: task 1002690 (provisioning + public-landing,
|
|
16
|
+
// goal 1000059), task 1002322 (platform-identity, goal 1000046) and `agents`, the
|
|
17
|
+
// instance this check was filed for. Each was fixed after someone tripped over it.
|
|
18
|
+
//
|
|
19
|
+
// WHY SUBSET AND NOT EQUALITY. The roster legitimately carries keys with no
|
|
20
|
+
// modules/<key>/ directory, for two different reasons, and equality would fail on all
|
|
21
|
+
// five of them in this tree:
|
|
22
|
+
// - NON-MODULE SCOPES — `kernel`, `methodology`, `devsecops` are surfaces, not modules.
|
|
23
|
+
// - ABSENT MODULES — `art-pipeline`, `game` are rostered but not present in this
|
|
24
|
+
// instance (ADR 0108: a host may carry a different module set).
|
|
25
|
+
// So the invariant is one-directional: every DISCOVERED module must be rostered; a
|
|
26
|
+
// rostered key with no module is not a fault. Getting that backwards would produce a
|
|
27
|
+
// check nobody can keep green, which is worse than no check.
|
|
28
|
+
//
|
|
29
|
+
// Extracted from fitness.js (which the size ratchet caps) following the
|
|
30
|
+
// role-pack-guard.js precedent. The pure scanner is exported for tests/fitness_scope_roster.mjs.
|
|
31
|
+
|
|
32
|
+
'use strict';
|
|
33
|
+
|
|
34
|
+
const fs = require('node:fs');
|
|
35
|
+
const path = require('node:path');
|
|
36
|
+
|
|
37
|
+
// The machinery already existed — fitness.js's discoveredModuleKeys() feeds the
|
|
38
|
+
// kernel-machinery check — but nothing cross-checked it against moduleKeys().
|
|
39
|
+
// checkScopeMapImportsProtected guards HOW `protected` is derived, not WHETHER every
|
|
40
|
+
// module is listed.
|
|
41
|
+
|
|
42
|
+
// SCOPE: modules/ ONLY — the two named orphans, decided rather than left open.
|
|
43
|
+
// Task 1002900 asked for a ruling on two files said to be covered by no glob. Both
|
|
44
|
+
// answers changed when checked against origin/main on 2026-09-22:
|
|
45
|
+
// - scripts/gds/fitness.js — NOT uncovered. The `devsecops` key matches it. The row's
|
|
46
|
+
// claim was stale.
|
|
47
|
+
// - src/server.js — does not exist any more.
|
|
48
|
+
// - src/platform-server.js — genuinely uncovered, and DELIBERATELY still is.
|
|
49
|
+
// This check asserts what its name says: every carved MODULE has a roster key. Widening
|
|
50
|
+
// it to arbitrary src/ entrypoints is a different question about a different registry —
|
|
51
|
+
// `kernel` lists individual files by hand, so "should platform-server join it" is a call
|
|
52
|
+
// for whoever owns that scope, not a side effect of adding a module-coverage lint. Left
|
|
53
|
+
// as a recorded decision rather than an exemption entry, because there is nothing to
|
|
54
|
+
// exempt: modules/ is the stated scope and platform-server.js was never in it.
|
|
55
|
+
|
|
56
|
+
const SCOPE_ROSTER_REL = 'src/bongos/module-scope-map.js';
|
|
57
|
+
|
|
58
|
+
function scanScopeRosterCoverage({ discovered, rostered, globs, exists }) {
|
|
59
|
+
const missing = discovered.filter((k) => !rostered.includes(k)).sort();
|
|
60
|
+
const violations = missing.map((k) =>
|
|
61
|
+
`module '${k}' (modules/${k}/module.json) has no key in ${SCOPE_ROSTER_REL} MODULE_GLOBS — `
|
|
62
|
+
+ 'any goal scoped to it will deny with empty_scope/unknown_scope');
|
|
63
|
+
|
|
64
|
+
// The mirror-image rot, reported as a WARNING rather than a failure: a roster glob that
|
|
65
|
+
// names an exact FILE which no longer exists. It cannot wall anything off, so it is a
|
|
66
|
+
// silent hole in a scope rather than a build breakage — and unlike a missing key it has
|
|
67
|
+
// no runtime symptom at all. Only exact file paths are checked; a directory glob may
|
|
68
|
+
// legitimately point at an absent optional module (see SUBSET above).
|
|
69
|
+
const warnings = [];
|
|
70
|
+
for (const [key, list] of Object.entries(globs || {})) {
|
|
71
|
+
for (const g of list || []) {
|
|
72
|
+
if (g.endsWith('/') || g.includes('*')) continue;
|
|
73
|
+
if (!exists(g)) warnings.push(`${SCOPE_ROSTER_REL}: scope '${key}' lists '${g}', which does not exist — dead entry, walls nothing`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { violations, warnings };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// REPO_ROOT and discoveredModuleKeys come from fitness.js, which owns module discovery
|
|
80
|
+
// (it unions across MODULE_ROOTS per ADR 0108). Passed in rather than re-derived so the
|
|
81
|
+
// two can never disagree about what a module is.
|
|
82
|
+
function checkScopeRosterCoversModules({ repoRoot, discoveredModuleKeys }) {
|
|
83
|
+
let res;
|
|
84
|
+
try {
|
|
85
|
+
const scopeMap = require(path.join(repoRoot, SCOPE_ROSTER_REL));
|
|
86
|
+
res = scanScopeRosterCoverage({
|
|
87
|
+
discovered: discoveredModuleKeys(),
|
|
88
|
+
rostered: scopeMap.moduleKeys(),
|
|
89
|
+
globs: scopeMap.MODULE_GLOBS,
|
|
90
|
+
exists: (g) => fs.existsSync(path.join(repoRoot, g)),
|
|
91
|
+
});
|
|
92
|
+
} catch (e) {
|
|
93
|
+
// FAILS OPEN, deliberately and narrowly. This catch fires only if requiring the scope
|
|
94
|
+
// map throws — i.e. the file is syntactically broken — and that is not this check's
|
|
95
|
+
// job to report: src/bongos/module-scope-map.js has its own unit coverage, and every
|
|
96
|
+
// sibling scope check (checkScopeMapImportsProtected, the ownership checks) would red
|
|
97
|
+
// on the same input. A coverage lint that hard-fails on someone else's syntax error
|
|
98
|
+
// adds a second confusing message to a build that is already failing for the real
|
|
99
|
+
// reason. The skip is surfaced as a warning rather than swallowed.
|
|
100
|
+
return {
|
|
101
|
+
name: 'every discovered module has a scope-roster key (task 1002900)',
|
|
102
|
+
ok: true, hardFail: false, violations: [], warnings: [`skipped: ${e.message}`],
|
|
103
|
+
note: `could not load ${SCOPE_ROSTER_REL} — check skipped.`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
name: 'every discovered module has a scope-roster key (task 1002900)',
|
|
108
|
+
ok: res.violations.length === 0,
|
|
109
|
+
hardFail: res.violations.length > 0,
|
|
110
|
+
violations: res.violations,
|
|
111
|
+
warnings: res.warnings,
|
|
112
|
+
note: `every modules/<key>/ carrying module.json has a MODULE_GLOBS entry, so a goal can be `
|
|
113
|
+
+ `scoped to it. Subset, not equality: a rostered key with no module is legitimate `
|
|
114
|
+
+ `(non-module scopes like kernel/methodology/devsecops, and modules absent from this instance).`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { checkScopeRosterCoversModules, scanScopeRosterCoverage, SCOPE_ROSTER_REL };
|
|
@@ -451,6 +451,7 @@ const MODULE_GLOBS = {
|
|
|
451
451
|
'scripts/gds/fitness-checks-packaging.js', 'scripts/gds/fitness-checks-route-pins.js',
|
|
452
452
|
'scripts/gds/fitness-checks-vocabulary.js', 'scripts/gds/fitness-checks-write-validation.js',
|
|
453
453
|
'scripts/gds/fitness-migration-contraction.js', 'scripts/gds/role-pack-guard.js',
|
|
454
|
+
'scripts/gds/scope-roster-guard.js',
|
|
454
455
|
'scripts/gds/route-shadow-guard.js', 'scripts/gds/routine-schedule.js',
|
|
455
456
|
'scripts/gds/seam-wiring-guard.js', 'scripts/gds/skill-preflight.js',
|
|
456
457
|
'scripts/gds/test-path-guard.js', 'scripts/gds/version-literal-guard.js',
|
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.972'; // 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/copy_inventory.mjs
CHANGED
|
Binary file
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// tests/fitness_no_raw_nul.mjs
|
|
2
|
+
//
|
|
3
|
+
// Task 1002854 — scripts/public-mirror-export.js sniffs for a NUL byte in the first
|
|
4
|
+
// 8 KiB to decide binary-vs-text (`isBinary`), and `redactBlob` skips `redactText`
|
|
5
|
+
// entirely for anything it classifies binary. So a TEXT source containing a raw NUL is
|
|
6
|
+
// published to the public AGPL mirror WITHOUT redaction.
|
|
7
|
+
//
|
|
8
|
+
// The binary branch is not unguarded — it still leak-scans the latin1 view — but that
|
|
9
|
+
// gate uses `sensitiveLiterals` (raw substrings) while redaction applies the full
|
|
10
|
+
// `rules` set. The exposure is the DELTA: anything redaction would rewrite that the
|
|
11
|
+
// literal gate does not recognise ships verbatim. Latent, not live — hence a lint.
|
|
12
|
+
//
|
|
13
|
+
// THE CLASS RECURS, which is why this is a lint and not a fifth point fix. Task 1703
|
|
14
|
+
// fixed three instances, task 1002507 a fourth, idea 1000523 covers a fifth, and this
|
|
15
|
+
// task found four more. Every one was the same mistake: a raw NUL used as a join
|
|
16
|
+
// delimiter instead of the \u0000 escape.
|
|
17
|
+
//
|
|
18
|
+
// These tests drive the PURE scanner over INJECTED FIXTURES rather than over the real
|
|
19
|
+
// tree. A test that only asserts "the repo is clean today" passes vacuously the moment
|
|
20
|
+
// the scanner breaks — it would report success on a scanner that always returns [].
|
|
21
|
+
//
|
|
22
|
+
// Run: node --test tests/fitness_no_raw_nul.mjs
|
|
23
|
+
|
|
24
|
+
import { strict as assert } from 'node:assert';
|
|
25
|
+
import { test } from 'node:test';
|
|
26
|
+
import { createRequire } from 'node:module';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import { fileURLToPath } from 'node:url';
|
|
29
|
+
|
|
30
|
+
const require = createRequire(import.meta.url);
|
|
31
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
32
|
+
const { scanForRawNul } = require(path.join(REPO_ROOT, 'scripts/gds/fitness.js'));
|
|
33
|
+
|
|
34
|
+
// Built at runtime so THIS file never carries the byte it is about — otherwise the
|
|
35
|
+
// check's own test would be its first violation.
|
|
36
|
+
const NUL = String.fromCharCode(0);
|
|
37
|
+
|
|
38
|
+
// A fake tree: { 'path' => 'contents' }, injected through the scanner's seams so no
|
|
39
|
+
// temp files are needed and every fixture is visible in the test.
|
|
40
|
+
function fakeTree(files) {
|
|
41
|
+
return {
|
|
42
|
+
listFiles: () => Object.keys(files),
|
|
43
|
+
readFile: (f) => Buffer.from(files[f], 'utf8'),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test('THE BUG: a raw NUL in a .js source is caught', () => {
|
|
48
|
+
const hits = scanForRawNul('/fake', fakeTree({
|
|
49
|
+
'src/join.js': `const k = a + '${NUL}' + b;\n`,
|
|
50
|
+
}));
|
|
51
|
+
assert.equal(hits.length, 1, 'a raw NUL in tracked JS must be a violation');
|
|
52
|
+
assert.match(hits[0], /src\/join\.js:1/, 'must name the file and LINE — a byte offset is not actionable');
|
|
53
|
+
assert.match(hits[0], /u0000/, 'and must name the fix');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('THE FIX: the escape form is NOT caught — it leaves no NUL in the source', () => {
|
|
57
|
+
// The whole premise of the remedy: the escape compiles to the same string at runtime,
|
|
58
|
+
// and only the source stops carrying the byte. If the check flagged the escape too,
|
|
59
|
+
// the violation would be unfixable.
|
|
60
|
+
const hits = scanForRawNul('/fake', fakeTree({
|
|
61
|
+
'src/join.js': "const k = a + '\\u0000' + b;",
|
|
62
|
+
}));
|
|
63
|
+
assert.deepEqual(hits, [], 'the escape is the prescribed fix and must pass');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('the reported line is the line the NUL is on, not the first line', () => {
|
|
67
|
+
const hits = scanForRawNul('/fake', fakeTree({
|
|
68
|
+
'a.js': `line one\nline two\nconst k = '${NUL}';\n`,
|
|
69
|
+
}));
|
|
70
|
+
assert.match(hits[0], /a\.js:3/);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('binary extensions are out of scope — a PNG is SUPPOSED to contain NULs', () => {
|
|
74
|
+
const hits = scanForRawNul('/fake', fakeTree({
|
|
75
|
+
'public/logo.png': `PNG${NUL}${NUL}`,
|
|
76
|
+
'docs/diagram.svg': `<svg>${NUL}</svg>`,
|
|
77
|
+
}));
|
|
78
|
+
assert.deepEqual(hits, [],
|
|
79
|
+
'the mirror is right to pass binaries through verbatim; only TEXT sources are the hole');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('every publishable text extension is scanned', () => {
|
|
83
|
+
const files = {};
|
|
84
|
+
for (const ext of ['.js', '.mjs', '.cjs', '.mts', '.md', '.sql', '.json']) files[`f${ext}`] = `x${NUL}y`;
|
|
85
|
+
assert.equal(scanForRawNul('/fake', fakeTree(files)).length, 7,
|
|
86
|
+
'a gap in the extension list is a gap in the guard');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('an unreadable or deleted-but-tracked file is skipped, not a crash', () => {
|
|
90
|
+
const hits = scanForRawNul('/fake', {
|
|
91
|
+
listFiles: () => ['gone.js', 'here.js'],
|
|
92
|
+
readFile: (f) => {
|
|
93
|
+
if (f === 'gone.js') throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
|
|
94
|
+
return Buffer.from(`ok${NUL}`, 'utf8');
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
assert.equal(hits.length, 1, 'the readable file is still checked');
|
|
98
|
+
assert.match(hits[0], /here\.js/);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('a clean tree reports nothing', () => {
|
|
102
|
+
assert.deepEqual(scanForRawNul('/fake', fakeTree({
|
|
103
|
+
'a.js': 'const k = a + b;',
|
|
104
|
+
'b.md': '# heading',
|
|
105
|
+
})), []);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('the REAL tree is clean (the check must land green)', () => {
|
|
109
|
+
// Deliberately last, and deliberately paired with the fixtures above: on its own this
|
|
110
|
+
// assertion would pass against a scanner that always returns [].
|
|
111
|
+
assert.deepEqual(scanForRawNul(REPO_ROOT), [],
|
|
112
|
+
'a raw NUL landed in a tracked text source — escape it (identical string at runtime)');
|
|
113
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// tests/fitness_scope_roster.mjs
|
|
2
|
+
//
|
|
3
|
+
// Task 1002900 — the scope roster in src/bongos/module-scope-map.js (MODULE_GLOBS) is
|
|
4
|
+
// maintained by hand, and nothing asserted that every carved module has a covering key.
|
|
5
|
+
// A module lands in modules/<key>/, nobody adds the entry, and any goal scoped to that
|
|
6
|
+
// work hits an authorize-NOTHING wall: goal-scope-check.check() denies with
|
|
7
|
+
// empty_scope / unknown_scope. It surfaces only when someone tries to set a goal's wall.
|
|
8
|
+
//
|
|
9
|
+
// Three hand-fixed instances before this check existed: task 1002690 (provisioning +
|
|
10
|
+
// public-landing), task 1002322 (platform-identity), and `agents` — the one this row
|
|
11
|
+
// was filed for, since added at module-scope-map.js:322.
|
|
12
|
+
//
|
|
13
|
+
// SUBSET, NOT EQUALITY. The roster legitimately carries keys with no modules/<key>/ dir,
|
|
14
|
+
// for two distinct reasons, and equality would fail on all five in this tree:
|
|
15
|
+
// - non-module SCOPES: kernel, methodology, devsecops
|
|
16
|
+
// - modules ABSENT from this instance: art-pipeline, game (ADR 0108)
|
|
17
|
+
// So the invariant is one-directional. These tests pin that direction explicitly,
|
|
18
|
+
// because getting it wrong turns a useful check into one nobody can keep green.
|
|
19
|
+
//
|
|
20
|
+
// Run: node --test tests/fitness_scope_roster.mjs
|
|
21
|
+
|
|
22
|
+
import { strict as assert } from 'node:assert';
|
|
23
|
+
import { test } from 'node:test';
|
|
24
|
+
import { createRequire } from 'node:module';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
const require = createRequire(import.meta.url);
|
|
29
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
30
|
+
const { discoveredModuleKeys } = require(path.join(REPO_ROOT, 'scripts/gds/fitness.js'));
|
|
31
|
+
// The check lives in its own file (the role-pack-guard.js precedent) so fitness.js stays
|
|
32
|
+
// under the size ratchet; module discovery stays in fitness.js, which owns it.
|
|
33
|
+
const { scanScopeRosterCoverage } = require(path.join(REPO_ROOT, 'scripts/gds/scope-roster-guard.js'));
|
|
34
|
+
const scopeMap = require(path.join(REPO_ROOT, 'src/bongos/module-scope-map.js'));
|
|
35
|
+
|
|
36
|
+
const nothingExists = () => false;
|
|
37
|
+
const allExist = () => true;
|
|
38
|
+
|
|
39
|
+
test('THE BUG: a discovered module with no roster key is a violation', () => {
|
|
40
|
+
const { violations } = scanScopeRosterCoverage({
|
|
41
|
+
discovered: ['lifecycle', 'agents'],
|
|
42
|
+
rostered: ['lifecycle'],
|
|
43
|
+
globs: {},
|
|
44
|
+
exists: allExist,
|
|
45
|
+
});
|
|
46
|
+
assert.equal(violations.length, 1);
|
|
47
|
+
assert.match(violations[0], /'agents'/, 'must name the uncovered module');
|
|
48
|
+
assert.match(violations[0], /empty_scope|unknown_scope/,
|
|
49
|
+
'and must name the symptom, since that is how this is actually discovered');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('SUBSET NOT EQUALITY: a rostered key with no module is NOT a fault', () => {
|
|
53
|
+
// kernel/methodology/devsecops are surfaces, not modules; art-pipeline/game may be
|
|
54
|
+
// absent from a given instance. Failing on these would make the check unkeepable.
|
|
55
|
+
const { violations } = scanScopeRosterCoverage({
|
|
56
|
+
discovered: ['lifecycle'],
|
|
57
|
+
rostered: ['lifecycle', 'kernel', 'methodology', 'devsecops', 'art-pipeline', 'game'],
|
|
58
|
+
globs: {},
|
|
59
|
+
exists: allExist,
|
|
60
|
+
});
|
|
61
|
+
assert.deepEqual(violations, []);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('several uncovered modules are all reported, sorted', () => {
|
|
65
|
+
const { violations } = scanScopeRosterCoverage({
|
|
66
|
+
discovered: ['zeta', 'alpha', 'lifecycle'],
|
|
67
|
+
rostered: ['lifecycle'],
|
|
68
|
+
globs: {},
|
|
69
|
+
exists: allExist,
|
|
70
|
+
});
|
|
71
|
+
assert.equal(violations.length, 2, 'one line per uncovered module — not just the first');
|
|
72
|
+
assert.match(violations[0], /'alpha'/);
|
|
73
|
+
assert.match(violations[1], /'zeta'/);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('a dead exact-file glob is a WARNING, never a failure', () => {
|
|
77
|
+
// The mirror-image rot: a roster entry naming a file that no longer exists cannot wall
|
|
78
|
+
// anything off, but it breaks no build either — so it is reported, not enforced.
|
|
79
|
+
const { violations, warnings } = scanScopeRosterCoverage({
|
|
80
|
+
discovered: [], rostered: [],
|
|
81
|
+
globs: { kernel: ['server.js'] },
|
|
82
|
+
exists: nothingExists,
|
|
83
|
+
});
|
|
84
|
+
assert.deepEqual(violations, [], 'a dead glob must not fail the build');
|
|
85
|
+
assert.equal(warnings.length, 1);
|
|
86
|
+
assert.match(warnings[0], /server\.js/);
|
|
87
|
+
assert.match(warnings[0], /walls nothing/);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('directory and wildcard globs are exempt from the existence warning', () => {
|
|
91
|
+
// A directory glob may legitimately point at an optional module absent from this
|
|
92
|
+
// instance — warning on those would bury the real signal in noise.
|
|
93
|
+
const { warnings } = scanScopeRosterCoverage({
|
|
94
|
+
discovered: [], rostered: [],
|
|
95
|
+
globs: { game: ['modules/game/', 'src/**/game-*.js'] },
|
|
96
|
+
exists: nothingExists,
|
|
97
|
+
});
|
|
98
|
+
assert.deepEqual(warnings, []);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('THE REAL TREE: every discovered module is rostered (the check must land green)', () => {
|
|
102
|
+
// Last, and paired with the fixtures above: alone this would pass against a scanner
|
|
103
|
+
// that always returns [].
|
|
104
|
+
const { violations } = scanScopeRosterCoverage({
|
|
105
|
+
discovered: discoveredModuleKeys(),
|
|
106
|
+
rostered: scopeMap.moduleKeys(),
|
|
107
|
+
globs: {},
|
|
108
|
+
exists: allExist,
|
|
109
|
+
});
|
|
110
|
+
assert.deepEqual(violations, [],
|
|
111
|
+
'a module was carved without a MODULE_GLOBS key — any goal scoped to it denies everything');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('agents specifically is rostered (the instance this row was filed for)', () => {
|
|
115
|
+
assert.ok(scopeMap.moduleKeys().includes('agents'));
|
|
116
|
+
assert.ok(discoveredModuleKeys().includes('agents'), 'and it is a real discovered module, not just a roster entry');
|
|
117
|
+
});
|