@bongos/core 1.19.970 → 1.19.971

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 CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.970",
6
- "core_contract": "1.19.970",
7
- "source_commit": "6fcd89630051b6a8a27ab995131716370ed9082e",
5
+ "core_version": "1.19.971",
6
+ "core_contract": "1.19.971",
7
+ "source_commit": "64be47e290d62d4a4397189fe7c91e056e557c92",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-23T01:02:05.515Z",
9
+ "built_at": "2026-09-23T01:18:55.107Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 515,
13
13
  "agent_docs_stubbed": 26,
14
- "functional_verbatim": 2370,
14
+ "functional_verbatim": 2371,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2911,
20
- "tree_sha256": "60a4632de03ed24bd98cbfe694a93acfed70e02c72838da91213a9c68ad14813",
19
+ "file_count": 2912,
20
+ "tree_sha256": "eb2fb77b0138a60460479f83e4a369b7f2c8101b489e124e33596a81711ddffd",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2697,7 +2697,7 @@
2697
2697
  {
2698
2698
  "path": "docs/module-api-changelog.md",
2699
2699
  "mode": "0000644",
2700
- "sha256": "e842c06ae274c61f5f38063af7b5eb5da4798954df2b9e3e42c1929352dd9a3f"
2700
+ "sha256": "f4d8da00ac6a0416fc0b38be5186cfa841ba18d8afccfd53b8c6055d56ad0f88"
2701
2701
  },
2702
2702
  {
2703
2703
  "path": "docs/modules-contract.md",
@@ -4047,7 +4047,7 @@
4047
4047
  {
4048
4048
  "path": "modules/copy-desk/proposals.js",
4049
4049
  "mode": "0000644",
4050
- "sha256": "de674674cf2f80fb36569cad67be4f1ba3a94e938b690b8f1d0c96446df346bf"
4050
+ "sha256": "c9a99cbda84386baa0da149a72ac0763b335bda34f1d08f52ba9656e25cb319b"
4051
4051
  },
4052
4052
  {
4053
4053
  "path": "modules/copy-desk/registry.js",
@@ -8107,12 +8107,12 @@
8107
8107
  {
8108
8108
  "path": "package-lock.json",
8109
8109
  "mode": "0000644",
8110
- "sha256": "b398350d28d5e528b46daafbadd4b19ddd9e9423a0c234aaabd27dc1718d6864"
8110
+ "sha256": "7b458e15cda96ebf05079d2bb4fe7af847fc64e401a2d8f9584831cfb591cd64"
8111
8111
  },
8112
8112
  {
8113
8113
  "path": "package.json",
8114
8114
  "mode": "0000644",
8115
- "sha256": "03e808eb0d1d2cb9be5d0ee13830a79fd5b0752ee5ff7f0a17993df3dc147903"
8115
+ "sha256": "211d62a555349042e0d897f9f6a795a7fb45abac88dd2584231ecf042d6b0051"
8116
8116
  },
8117
8117
  {
8118
8118
  "path": "public-docs/index.html",
@@ -8447,7 +8447,7 @@
8447
8447
  {
8448
8448
  "path": "scripts/gds/claim.js",
8449
8449
  "mode": "0000644",
8450
- "sha256": "a02e76a5f817501745c843c1df25fdd9584e91eb3d97fe76976fd508b70f72d9"
8450
+ "sha256": "942581a081da9134cb7038c61b646ddba6e25f2de15411fe59378a477ac05637"
8451
8451
  },
8452
8452
  {
8453
8453
  "path": "scripts/gds/classify-discipline.js",
@@ -8697,7 +8697,7 @@
8697
8697
  {
8698
8698
  "path": "scripts/gds/fitness.js",
8699
8699
  "mode": "0000644",
8700
- "sha256": "35e377253453c975ce7a8e420d72af718a4f8ab6d3837d39fd38c1ebd5658ddc"
8700
+ "sha256": "c84fbcc5d9c2676c90756a9cce02987c9dd30b68d17bee01698d6a7bc5cf9c7c"
8701
8701
  },
8702
8702
  {
8703
8703
  "path": "scripts/gds/gate-review.js",
@@ -9037,7 +9037,7 @@
9037
9037
  {
9038
9038
  "path": "scripts/gds/probe-cache.js",
9039
9039
  "mode": "0000644",
9040
- "sha256": "34d891e8ff58f0da957d300021210dbc34448fe676873b6b77e13ad3b7179d2e"
9040
+ "sha256": "6860b900c3b60e4e15108d3033a808312b090f0b21b2dcf4021d39cff1af3f50"
9041
9041
  },
9042
9042
  {
9043
9043
  "path": "scripts/gds/prove-strict-versioning.js",
@@ -10122,7 +10122,7 @@
10122
10122
  {
10123
10123
  "path": "src/module-api.js",
10124
10124
  "mode": "0000644",
10125
- "sha256": "a16e899a5c50b65af3e0976a27e72e2f1e50b7cfc8a8acafc64b19cebae1eb93"
10125
+ "sha256": "ef138765707d6d54a7992d02ad0b3e21673b90c47e2ea6a3384f95ee6e90a00a"
10126
10126
  },
10127
10127
  {
10128
10128
  "path": "src/module-loader/catalog.js",
@@ -11022,7 +11022,7 @@
11022
11022
  {
11023
11023
  "path": "tests/copy_inventory.mjs",
11024
11024
  "mode": "0000644",
11025
- "sha256": "e621021bdab92a17c9f0f0760ced4c2f2c955ef3df91fb584a7a398c46a717aa"
11025
+ "sha256": "bdfb3c305a60f3767484e86b1ec41295abad9142db1b51ff6bf49855c882f286"
11026
11026
  },
11027
11027
  {
11028
11028
  "path": "tests/core_update_banner.mjs",
@@ -11419,6 +11419,11 @@
11419
11419
  "mode": "0000644",
11420
11420
  "sha256": "230e532b54a69a3ee2c009b3a018a6cb920c92fb402364427cbdc244434d4c1b"
11421
11421
  },
11422
+ {
11423
+ "path": "tests/fitness_no_raw_nul.mjs",
11424
+ "mode": "0000644",
11425
+ "sha256": "30ad1bacf0f695bfcf50c65f86780581a9215e70183792e3130860bb1bef73dd"
11426
+ },
11422
11427
  {
11423
11428
  "path": "tests/fitness_ratchets.mjs",
11424
11429
  "mode": "0000644",
@@ -2414,5 +2414,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
2414
2414
  landed since 1.19.968 with no explicit bump. run 35798488315. (task 1002620)
2415
2415
  1.19.970 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2416
2416
  landed since 1.19.969 with no explicit bump. run 35804568036. (task 1002620)
2417
+ 1.19.971 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
2418
+ landed since 1.19.970 with no explicit bump. run 35805746484. (task 1002620)
2417
2419
  ---------------------------------------------------------------------------
2418
2420
  ```
@@ -255,7 +255,7 @@ function proposalTaskBody(patch) {
255
255
  // right: two people can disagree about the fix, and the board should hold both.
256
256
  function proposalSourceRef(patch) {
257
257
  let hash = 5381;
258
- const material = `${patch.string_id}${patch.file}${patch.line}${patch.proposed}`;
258
+ const material = `${patch.string_id}\u0000${patch.file}\u0000${patch.line}\u0000${patch.proposed}`;
259
259
  for (let i = 0; i < material.length; i += 1) hash = (((hash << 5) + hash) ^ material.charCodeAt(i)) >>> 0;
260
260
  return `copy-proposal-${patch.string_id}-${hash.toString(36)}`;
261
261
  }
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.970",
3
+ "version": "1.19.971",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.970",
9
+ "version": "1.19.971",
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.970",
3
+ "version": "1.19.971",
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",
@@ -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(/[--Ÿ]+/g, ' ').trim();
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
 
@@ -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
@@ -1307,6 +1393,7 @@ function main() {
1307
1393
  if (require.main === module) main();
1308
1394
 
1309
1395
  module.exports = {
1396
+ scanForRawNul,
1310
1397
  checkVocabularyWall, VOCAB_ALLOW, VOCAB_CITATION, VOCAB_EXTS, vocabFilesToScan,
1311
1398
  checkGoalMembershipVocabulary, GOAL_VOCAB_SURFACE, checkGovernmentRenameIdentity, RENAME_IDENTITY_RE, checkPrelaunchVocabulary, PRELAUNCH_SURFACE, // 19c task 1003120, 19d task 1003152
1312
1399
  runAll,
Binary file
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.970'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.971'; // 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');
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
+ });