@bongos/core 1.19.725 → 1.19.726

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,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.725",
6
- "core_contract": "1.19.725",
7
- "source_commit": "5a98310e9967d2430aba88122213a7a722ae07ad",
5
+ "core_version": "1.19.726",
6
+ "core_contract": "1.19.726",
7
+ "source_commit": "d8623af04d5a30742d38eb9ba239a3f63f775a36",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-13T13:36:04.522Z",
9
+ "built_at": "2026-09-13T14:41:51.726Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 478,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 2665,
20
- "tree_sha256": "41c25536b149d9641dc2e413a0da01a4e49c306b619ec412c2f2ea7b552944c6",
20
+ "tree_sha256": "a9b351048d2b02f92d5d3a8a5639a19ab5ed6731a39eb8ac8a937ac19eb22448",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2807,7 +2807,7 @@
2807
2807
  {
2808
2808
  "path": "docs/module-api-changelog.md",
2809
2809
  "mode": "0000644",
2810
- "sha256": "793f097cac5faadbbf7d9aaec4c2b5b413a535b913b2e3258ef17361982f7ef9"
2810
+ "sha256": "1d9b38322833641b859bcf8e401135219788455e5aa0d5082d7044551beefd08"
2811
2811
  },
2812
2812
  {
2813
2813
  "path": "docs/modules-contract.md",
@@ -7837,12 +7837,12 @@
7837
7837
  {
7838
7838
  "path": "package-lock.json",
7839
7839
  "mode": "0000644",
7840
- "sha256": "82be9f1fd6d0b97fdc7b5ac3d36e9e7537c2b42818af2bb643be5ecf6610d22a"
7840
+ "sha256": "7637583d4d85202afbf079b2db2c89fb687377c8478d5baa8c058dc282e6d141"
7841
7841
  },
7842
7842
  {
7843
7843
  "path": "package.json",
7844
7844
  "mode": "0000644",
7845
- "sha256": "f1796793605e11cb891209283c562e08772ee65f956bbe5039ee799e1969a013"
7845
+ "sha256": "c9ee4d8541a9fb546d0556b13d3814e132b301e606676d6d23c2416c8c3b23b7"
7846
7846
  },
7847
7847
  {
7848
7848
  "path": "public-docs/index.html",
@@ -8332,7 +8332,7 @@
8332
8332
  {
8333
8333
  "path": "scripts/gds/gate-review.js",
8334
8334
  "mode": "0000644",
8335
- "sha256": "19d739354ed6f485a842ce77379ed4b63f9b86ba848c3b9d7528809a79ad74f1"
8335
+ "sha256": "183c4592b68f059ef89b4d516ae589526af08901543ded0183b8854cd9b1e5a2"
8336
8336
  },
8337
8337
  {
8338
8338
  "path": "scripts/gds/gating.js",
@@ -9467,7 +9467,7 @@
9467
9467
  {
9468
9468
  "path": "src/bongos/permission-path-check.js",
9469
9469
  "mode": "0000644",
9470
- "sha256": "5116468286f737a3421dbcdfdc48f78ecc8f25496f7db0382f9af9e1a0110e9c"
9470
+ "sha256": "14ba2ffb603cbea7d8388c196b74f8e7db92b777fb0da7c69c7d3c7c9e27f386"
9471
9471
  },
9472
9472
  {
9473
9473
  "path": "src/bongos/platform-visibility-gate.js",
@@ -9637,7 +9637,7 @@
9637
9637
  {
9638
9638
  "path": "src/module-api.js",
9639
9639
  "mode": "0000644",
9640
- "sha256": "5aa7b8b121af462b9feba77bbd2a87b997e6dad22d3c8e6bf467ca122016d3d0"
9640
+ "sha256": "958848fd9515e4e2c7544579ba4023f136ab5d1e5d521964ae12404682fc193c"
9641
9641
  },
9642
9642
  {
9643
9643
  "path": "src/module-loader/catalog.js",
@@ -11127,7 +11127,7 @@
11127
11127
  {
11128
11128
  "path": "tests/government_protected_surfaces.mjs",
11129
11129
  "mode": "0000644",
11130
- "sha256": "d11562d59861a5f5d294c072b3aa15f03703ddbb2102210ff92f7ccbcff5b5f6"
11130
+ "sha256": "dfde7ca6e223324317a0d56960db7adc9b30bdfc35f718269e9d6978344545ac"
11131
11131
  },
11132
11132
  {
11133
11133
  "path": "tests/government_rank_sync.mjs",
@@ -1924,5 +1924,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1924
1924
  landed since 1.19.723 with no explicit bump. run 34759302425. (task 1002620)
1925
1925
  1.19.725 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1926
1926
  landed since 1.19.724 with no explicit bump. run 34760272743. (task 1002620)
1927
+ 1.19.726 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1928
+ landed since 1.19.725 with no explicit bump. run 34763380132. (task 1002620)
1927
1929
  ---------------------------------------------------------------------------
1928
1930
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.725",
3
+ "version": "1.19.726",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.725",
9
+ "version": "1.19.726",
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.725",
3
+ "version": "1.19.726",
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",
@@ -38,8 +38,11 @@
38
38
 
39
39
  const { execFileSync } = require('node:child_process');
40
40
  const fs = require('node:fs');
41
- const permPath = require('../../src/bongos/permission-path-check');
42
- const { PROTECTED_GLOBS, globToRegExp } = permPath;
41
+ // Destructured AT the require, not off a held module object: knip's static pass
42
+ // cannot follow `const x = require(...); const { y } = x;` and reports `y` as an
43
+ // unused export, which reds the CI knip step (this file's own CLAUDE.md records
44
+ // the same trap for src/instance-config.js).
45
+ const { PROTECTED_GLOBS, compileGlobMatcher } = require('../../src/bongos/permission-path-check');
43
46
 
44
47
  // --- HARD FLOOR (rank-independent ESCALATE) -------------------------------------------
45
48
  // A subset of PROTECTED_GLOBS: the surfaces that, if a compromised/injected senior
@@ -89,12 +92,17 @@ const AUTO_NARROWING = {
89
92
  '.husky/secrets-allowlist.txt': certifyAllowlistNarrowing,
90
93
  };
91
94
 
92
- const COMPILED_HARD = HARD_FLOOR_GLOBS.map((g) => ({ glob: g, re: globToRegExp(g) }));
93
- const matchesHardFloor = (f) => COMPILED_HARD.some((c) => c.re.test(f));
95
+ // task 1002874: compile through the authoritative module's FOLDING matcher, not
96
+ // globToRegExp directly. Both of these used to build their own byte-exact regexes
97
+ // from the raw globs and test raw candidates, so after protected-path matching
98
+ // gained NFC + case-folding, an uppercased spelling of a gate surface still walked
99
+ // past THIS classifier — the CI auto-merge gate — which is the bypass the fix
100
+ // exists to close. isCanonicalPath below rejects non-NFC and non-ASCII, but it
101
+ // does not case-fold, so it never covered this.
102
+ const matchesHardFloor = compileGlobMatcher(HARD_FLOOR_GLOBS);
94
103
 
95
104
  // A path is "protected" per the authoritative module.
96
- const protectedRe = PROTECTED_GLOBS.map((g) => ({ glob: g, re: globToRegExp(g) }));
97
- const matchesProtected = (f) => protectedRe.some((c) => c.re.test(f));
105
+ const matchesProtected = compileGlobMatcher(PROTECTED_GLOBS);
98
106
 
99
107
  // Only [\x20-\x7e] path chars, NFC-stable, no leading/trailing whitespace. Defeats the
100
108
  // unicode-homoglyph + whitespace-alias evasions (red-team Holes 6/8).
@@ -140,13 +140,80 @@ function escapeRe(s) {
140
140
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
141
141
  }
142
142
 
143
- const COMPILED = PROTECTED_SURFACES.map((s) => ({ ...s, re: globToRegExp(s.glob) }));
143
+ // task 1002874: the glob is normalised ONCE here, at build, not per candidate —
144
+ // there are 51 surfaces and `matchProtected` runs over every changed file on the
145
+ // push/claim/grade path. `s.glob` itself is kept VERBATIM on the entry, because it
146
+ // is what gets reported back to a refused builder and in the registry's own docs;
147
+ // only the compiled matcher is folded.
148
+ const COMPILED = PROTECTED_SURFACES.map((s) => ({ ...s, re: globToRegExp(foldForMatch(s.glob)) }));
144
149
 
145
150
  // Normalize a path the way git reports it (forward slashes, no leading ./).
151
+ // normalize — put a path in the ONE spelling matching happens in.
152
+ //
153
+ // task 1002874 (ADR 0151 §4, the last piece of R101's scope): this used to do
154
+ // only backslash→slash and strip a leading './', so matching was exact-BYTES. On
155
+ // a case-insensitive filesystem — macOS by default, and Windows, i.e. the two
156
+ // platforms most builders use — `Src/Bongos/Auth.js` and `src/bongos/auth.js` name
157
+ // the SAME file, and only one of them matched the protected-surface glob. Unicode
158
+ // composition does the same thing invisibly: NFD (`e` + U+0301) and NFC (`é`) are
159
+ // different byte strings for one filename, and a checkout can hand you either.
160
+ // A protected-path wall you can step over by spelling is not a wall.
161
+ //
162
+ // BOTH SIDES go through this — the candidate here, and every registry glob ONCE at
163
+ // COMPILED build — so the transform is applied symmetrically and matching cannot
164
+ // become narrower. It is a strict widening: any pair that matched byte-for-byte
165
+ // still matches after both are folded the same way. The only regression shape is
166
+ // OVER-protection, surfacing as an unexpected claim/push refusal, never a silent
167
+ // un-protection.
168
+ //
169
+ // CASE FOLD: `toLowerCase()`, and that IS the invariant fold here. The Turkish-I
170
+ // hazard the ADR warns about belongs to `toLocaleLowerCase('tr')`, which maps
171
+ // 'I' → 'ı' (dotless) — measured. Plain `toLowerCase()` is Unicode Default Case
172
+ // Conversion and is locale-independent by specification, so it is the correct
173
+ // primitive and `toLocaleLowerCase` is the one that must never appear on this
174
+ // path. A test asserts it does not.
175
+ //
176
+ // ORDER: NFC first, then fold. Measured equivalent to folding first for the
177
+ // composed-accent cases, and NFC-first keeps the input in one canonical form
178
+ // before any other transform reasons about it.
146
179
  function normalize(p) {
147
180
  return String(p || '').replace(/\\/g, '/').replace(/^\.\//, '').trim();
148
181
  }
149
182
 
183
+ // foldForMatch — the spelling MATCHING happens in. Separate from normalize() on
184
+ // purpose: normalize() still produces the path we REPORT, and that must keep its
185
+ // original case. A finding, a refusal message and a grader issue all carry
186
+ // `file`, and on a case-sensitive filesystem (Linux, i.e. CI) a lowercased path
187
+ // names nothing — a consumer that tried to read it would get ENOENT for a file
188
+ // that exists.
189
+ function foldForMatch(p) {
190
+ return normalize(p).normalize('NFC').toLowerCase();
191
+ }
192
+
193
+ // compileGlobMatcher(globs) → (file) => boolean, with the fold applied to BOTH
194
+ // sides: each glob once here, the candidate on every call.
195
+ //
196
+ // task 1002874: this exists because `globToRegExp` + the raw `PROTECTED_GLOBS`
197
+ // are BOTH exported, and a consumer that compiles them itself gets a byte-exact
198
+ // matcher — which is precisely the wall this task removed, rebuilt by accident.
199
+ // gate-review.js had done exactly that (its ESCALATE / HARD_FLOOR classifier),
200
+ // so an uppercased spelling still slipped past CI's auto-merge gate after the
201
+ // matcher here was fixed. Anything matching a PROTECTED-surface list should
202
+ // compile through this, not through globToRegExp directly.
203
+ //
204
+ // NOT a blanket replacement for globToRegExp, deliberately. Folding widens what
205
+ // matches, and that is only safe when matching more means protecting more. On an
206
+ // ALLOW-list — publish-manifest's PUBLISH_ALLOWLIST, which decides what reaches
207
+ // the public OSS mirror — widening would publish MORE files. Those lists keep
208
+ // the exact matcher on purpose.
209
+ function compileGlobMatcher(globs) {
210
+ const compiled = (globs || []).map((g) => globToRegExp(foldForMatch(g)));
211
+ return (file) => {
212
+ const folded = foldForMatch(file);
213
+ return folded ? compiled.some((re) => re.test(folded)) : false;
214
+ };
215
+ }
216
+
150
217
  // Which of `files` are permission-sensitive? Returns
151
218
  // [{ file, glob, floor, permission }] — floor + permission come straight from the
152
219
  // registry entry that matched, so a governance re-map flows through every caller.
@@ -155,7 +222,11 @@ function matchProtected(files) {
155
222
  for (const raw of files || []) {
156
223
  const f = normalize(raw);
157
224
  if (!f) continue;
158
- const hit = COMPILED.find((c) => c.re.test(f));
225
+ // Match on the FOLDED spelling, report the original one — see foldForMatch.
226
+ // Hoisted OUT of the predicate on purpose: inside it, the fold re-runs once
227
+ // per registry surface (51 today) for every candidate file.
228
+ const folded = foldForMatch(raw);
229
+ const hit = COMPILED.find((c) => c.re.test(folded));
159
230
  if (hit) out.push({ file: f, glob: hit.glob, floor: hit.floor, permission: hit.permission });
160
231
  }
161
232
  return out;
@@ -164,7 +235,8 @@ function matchProtected(files) {
164
235
  // The registry entry governing `file`, or null when the path is unprotected.
165
236
  function surfaceFor(file) {
166
237
  const f = normalize(file);
167
- const hit = f ? COMPILED.find((c) => c.re.test(f)) : null;
238
+ const folded = f ? foldForMatch(file) : '';
239
+ const hit = f ? COMPILED.find((c) => c.re.test(folded)) : null;
168
240
  return hit ? { glob: hit.glob, floor: hit.floor, permission: hit.permission, group: hit.group, why: hit.why } : null;
169
241
  }
170
242
 
@@ -271,6 +343,11 @@ module.exports = {
271
343
  REGISTRY_PATH,
272
344
  SPECIALTY,
273
345
  globToRegExp,
346
+ compileGlobMatcher,
347
+ // foldForMatch is deliberately NOT exported: it is the internal spelling rule,
348
+ // and every caller should go through compileGlobMatcher so the fold is applied
349
+ // to BOTH sides. Handing it out invites a consumer to fold one side only, which
350
+ // is the asymmetry that breaks matching outright.
274
351
  matchProtected,
275
352
  surfaceFor,
276
353
  floorFor,
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.725'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.726'; // 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');
@@ -518,3 +518,140 @@ test('the floor decision reads the hit\'s registry floor, so a re-map takes effe
518
518
  const atFloor = ppc.checkPermissionPaths({ changedFiles: ['migrations/001.sql'], builderRank: 'metic' });
519
519
  assert.equal(atFloor.hardFail, false);
520
520
  });
521
+
522
+
523
+ // ---------------------------------------------------------------------------
524
+ // task 1002874 — NFC + case-folding, the last piece of R101's ADR 0151 §4 scope.
525
+ //
526
+ // R101 shipped the registry with matching left exact-BYTES, deliberately and
527
+ // declared: its hard constraint was byte-identical behaviour, and normalisation
528
+ // changes what matches. This IS that change, so these come in two halves — the
529
+ // wall must now hold against a re-spelling, and nothing protected before may have
530
+ // stopped being protected.
531
+ // ---------------------------------------------------------------------------
532
+
533
+ // A real literal surface from the registry, not a fixture: this cannot pass
534
+ // against a glob that no longer exists.
535
+ const LITERAL = ppc.PROTECTED_SURFACES.find((s) => !s.glob.includes('*') && !s.glob.endsWith('/'));
536
+
537
+ test('a CASE variant of a protected path is still protected', () => {
538
+ assert.ok(LITERAL, 'no literal surface in the registry — this test is reading the wrong shape');
539
+ assert.ok(ppc.surfaceFor(LITERAL.glob), 'the exact spelling must match (sanity)');
540
+ // macOS by default and Windows are case-INsensitive: this names the SAME file.
541
+ assert.ok(ppc.surfaceFor(LITERAL.glob.toUpperCase()),
542
+ 'an uppercased spelling of a protected file must not step over the wall');
543
+ const mixed = LITERAL.glob.replace(/(^|\/)([a-z])/g, (m, s, c) => s + c.toUpperCase());
544
+ assert.ok(ppc.surfaceFor(mixed), `a mixed-case spelling must not step over the wall: ${mixed}`);
545
+ });
546
+
547
+ test('the REPORTED path keeps its original case — only matching is folded', () => {
548
+ const shouty = LITERAL.glob.toUpperCase();
549
+ const [hit] = ppc.matchProtected([shouty]);
550
+ assert.ok(hit, 'the case variant must be caught');
551
+ assert.equal(hit.file, shouty,
552
+ 'findings, refusals and grader issues all carry this path; a lowercased one names '
553
+ + 'nothing on a case-SENSITIVE filesystem (Linux, i.e. CI) and would ENOENT for a file that exists');
554
+ assert.equal(hit.glob, LITERAL.glob, 'and the registry glob is reported verbatim, not folded');
555
+ });
556
+
557
+ test('an NFD-composed path matches its NFC glob', () => {
558
+ // The registry is pure ASCII today, so this exercises the mechanism directly:
559
+ // one filename, two different byte strings, either of which a checkout can hand you.
560
+ const nfc = 'modules/caf\u00e9/secret.js';
561
+ const nfd = 'modules/cafe\u0301/secret.js';
562
+ assert.notEqual(nfc, nfd, 'the two spellings must really differ in bytes, or this proves nothing');
563
+ const fold = (s) => s.normalize('NFC').toLowerCase();
564
+ const re = ppc.globToRegExp(fold(nfc));
565
+ assert.ok(re.test(fold(nfd)), 'NFD and NFC spellings of one filename must match the same glob');
566
+ assert.ok(re.test(fold(nfc.toUpperCase())), 'and case must not separate them either');
567
+ });
568
+
569
+ test('STRICTLY WIDENING: every glob still matches a path it matched before', () => {
570
+ // The regression that would matter is a path silently becoming UNprotected, so
571
+ // walk the whole registry and synthesise a concrete path from each glob.
572
+ let checked = 0;
573
+ for (const s of ppc.PROTECTED_SURFACES) {
574
+ const sample = s.glob.endsWith('/')
575
+ ? `${s.glob}probe.js`
576
+ : s.glob.replace(/\*\*/g, 'x/y').replace(/\*/g, 'z');
577
+ assert.ok(ppc.surfaceFor(sample), `${s.glob} no longer protects ${sample} — this change must never narrow`);
578
+ checked += 1;
579
+ }
580
+ assert.ok(checked >= 40, `only ${checked} surfaces walked — the registry read is not working`);
581
+ });
582
+
583
+ test('an UNprotected path is still unprotected — folding must not over-reach', () => {
584
+ for (const p of ['README.md', 'docs/notes.md', 'tests/example.mjs']) {
585
+ assert.equal(ppc.surfaceFor(p), null, `${p} must not become protected by normalisation`);
586
+ }
587
+ });
588
+
589
+ test('the matcher never uses a LOCALE-sensitive case fold (the Turkish-I hazard)', () => {
590
+ const src = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'permission-path-check.js'), 'utf8')
591
+ .replace(/\/\*[\s\S]*?\*\//g, '')
592
+ .replace(/\/\/[^\n]*/g, '');
593
+ assert.ok(src.length > 1000, `comment-stripping left only ${src.length} chars — the scan is reading nothing`);
594
+ assert.ok(!/toLocaleLowerCase|toLocaleUpperCase/.test(src),
595
+ 'toLocaleLowerCase("tr") maps I to dotless i — measured — so a Turkish locale would fold '
596
+ + 'two different paths together. Plain toLowerCase is locale-independent by specification '
597
+ + 'and is the correct primitive here; the ADR note warning against it does not apply to JS.');
598
+ assert.match(src, /toLowerCase\(\)/, 'and the fold must actually be applied');
599
+ assert.match(src, /normalize\('NFC'\)/, 'as must NFC composition');
600
+ });
601
+
602
+
603
+ // ---------------------------------------------------------------------------
604
+ // task 1002874, round 2 — the fold has to reach every PROTECTED matcher, not just
605
+ // the one in permission-path-check.
606
+ //
607
+ // Round 1 fixed matchProtected/surfaceFor and trusted the task description's claim
608
+ // that all eleven substrate consumers route through them. They do not.
609
+ // gate-review.js — the CI ESCALATE / HARD_FLOOR classifier — imported the RAW
610
+ // PROTECTED_GLOBS plus globToRegExp and compiled its own byte-exact regexes, so an
611
+ // uppercased spelling of a gate surface still walked past the auto-merge gate after
612
+ // the wall itself was fixed. Its isCanonicalPath() rejects non-NFC and non-ASCII
613
+ // but does not case-fold, so it never covered this.
614
+ // ---------------------------------------------------------------------------
615
+
616
+ test('compileGlobMatcher folds BOTH sides', () => {
617
+ const m = ppc.compileGlobMatcher(['src/bongos/auth.js', 'migrations/']);
618
+ assert.ok(m('src/bongos/auth.js'), 'exact still matches');
619
+ assert.ok(m('SRC/BONGOS/AUTH.JS'), 'case variant matches');
620
+ assert.ok(m('Migrations/001.sql'), 'a folded subtree prefix still matches');
621
+ assert.ok(!m('docs/readme.md'), 'and an unrelated path does not');
622
+ assert.ok(!m(''), 'empty is never a match');
623
+ assert.ok(!m(null), 'nor is a missing path');
624
+ });
625
+
626
+ test('gate-review classifies a CASE-VARIANT gate surface as hard-floor', () => {
627
+ const gr = require(path.join(ROOT, 'scripts', 'gds', 'gate-review.js'));
628
+ const literal = gr.HARD_FLOOR_GLOBS.find((g) => !g.includes('*') && !g.endsWith('/'));
629
+ assert.ok(literal, 'no literal hard-floor glob — this test is reading the wrong shape');
630
+ assert.ok(gr.matchesHardFloor(literal), 'the exact spelling must escalate (sanity)');
631
+ assert.ok(gr.matchesHardFloor(literal.toUpperCase()),
632
+ 'an uppercased gate surface must still escalate — otherwise CI auto-merge is bypassable '
633
+ + 'by re-spelling, which is the hole this task exists to close');
634
+ });
635
+
636
+ test('no consumer compiles a PROTECTED glob list byte-exact', () => {
637
+ // The drift guard. Both globToRegExp and the raw glob lists are exported, so a
638
+ // consumer can rebuild the byte-exact matcher by accident — which is exactly what
639
+ // gate-review had done. globToRegExp stays exported because ALLOW-lists
640
+ // legitimately need it: folding publish-manifest's PUBLISH_ALLOWLIST would widen
641
+ // what reaches the public OSS mirror, i.e. publish MORE. So the ban is narrow —
642
+ // pairing a PROTECTED list with the raw compiler in one file.
643
+ const dir = path.join(ROOT, 'scripts', 'gds');
644
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.js'));
645
+ assert.ok(files.length > 20, `only ${files.length} scripts scanned — the sweep is not reading the CLI directory`);
646
+ const offenders = [];
647
+ for (const f of files) {
648
+ const src = fs.readFileSync(path.join(dir, f), 'utf8')
649
+ .replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '');
650
+ const usesRawCompiler = /globToRegExp\s*\(/.test(src);
651
+ const usesProtectedList = /(PROTECTED_GLOBS|HARD_FLOOR_GLOBS)\s*\.\s*map/.test(src);
652
+ if (usesRawCompiler && usesProtectedList) offenders.push(f);
653
+ }
654
+ assert.deepEqual(offenders, [],
655
+ `these compile a protected-surface list with the raw byte-exact compiler; use `
656
+ + `compileGlobMatcher so the fold reaches them too: ${offenders.join(', ')}`);
657
+ });