@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 +21 -16
- package/docs/module-api-changelog.md +2 -0
- package/modules/copy-desk/proposals.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/claim.js +1 -1
- package/scripts/gds/fitness.js +87 -0
- package/scripts/gds/probe-cache.js +0 -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/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.971",
|
|
6
|
+
"core_contract": "1.19.971",
|
|
7
|
+
"source_commit": "64be47e290d62d4a4397189fe7c91e056e557c92",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-23T01:
|
|
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":
|
|
14
|
+
"functional_verbatim": 2371,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
8110
|
+
"sha256": "7b458e15cda96ebf05079d2bb4fe7af847fc64e401a2d8f9584831cfb591cd64"
|
|
8111
8111
|
},
|
|
8112
8112
|
{
|
|
8113
8113
|
"path": "package.json",
|
|
8114
8114
|
"mode": "0000644",
|
|
8115
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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}
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
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
|
|
@@ -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.
|
|
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');
|
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
|
+
});
|