@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 +12 -12
- package/docs/module-api-changelog.md +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/gate-review.js +14 -6
- package/src/bongos/permission-path-check.js +80 -3
- package/src/module-api.js +1 -1
- package/tests/government_protected_surfaces.mjs +137 -0
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.726",
|
|
6
|
+
"core_contract": "1.19.726",
|
|
7
|
+
"source_commit": "d8623af04d5a30742d38eb9ba239a3f63f775a36",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
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": "
|
|
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": "
|
|
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": "
|
|
7840
|
+
"sha256": "7637583d4d85202afbf079b2db2c89fb687377c8478d5baa8c058dc282e6d141"
|
|
7841
7841
|
},
|
|
7842
7842
|
{
|
|
7843
7843
|
"path": "package.json",
|
|
7844
7844
|
"mode": "0000644",
|
|
7845
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
42
|
-
const {
|
|
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
|
-
|
|
93
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
+
});
|