Skip to content

Commit deae9f4

Browse files
author
DavidQ
committed
BUILD PR: tighten shared extraction guard with strict detection rules. & BUILD PR: add self-test runner for shared extraction guard.
1 parent d01209e commit deae9f4

8 files changed

+406
-12
lines changed

docs/dev/CODEX_COMMANDS.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
MODEL: GPT-5.3-codex
22
REASONING: high
33
COMMAND:
4-
Execute docs/pr/BUILD_PR_SHARED_EXTRACTION_23_GUARD_DOC_MINIMAL_USAGE.md exactly.
5-
Create only:
6-
- docs/dev/SHARED_EXTRACTION_GUARD_USAGE.md
4+
Execute docs/pr/BUILD_PR_SHARED_EXTRACTION_25_GUARD_SELFTEST_RUNNER.md exactly.
5+
Edit only these files:
6+
- tools/dev/checkSharedExtractionGuard.selftest.mjs (new file)
7+
- tools/dev/checkSharedExtractionGuard.mjs (only if a minimal export is strictly required)
8+
Fail fast if tools/dev/checkSharedExtractionGuard.mjs does not exist.
79
Do not expand scope.
8-
Package delta to <project folder>/tmp/BUILD_PR_SHARED_EXTRACTION_23_GUARD_DOC_MINIMAL_USAGE_delta.zip
10+
Package the delta output to <project folder>/tmp/BUILD_PR_SHARED_EXTRACTION_25_GUARD_SELFTEST_RUNNER_delta.zip

docs/dev/COMMIT_COMMENT.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
BUILD PR: add minimal usage doc for shared extraction guard.
1+
BUILD PR: add self-test runner for shared extraction guard.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Adds guard usage documentation.
1+
Adds a temp-workspace self-test runner for the guard.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
doc added correctly
1+
self-test runner added correctly
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# BUILD_PR_SHARED_EXTRACTION_24_GUARD_STRICT_MODE
2+
3+
## Purpose
4+
Tighten the shared-extraction guard to catch edge cases and prevent bypass patterns.
5+
6+
## Single PR Purpose
7+
Enhance the existing guard script ONLY:
8+
9+
tools/dev/checkSharedExtractionGuard.mjs
10+
11+
## Exact Files Allowed
12+
1. tools/dev/checkSharedExtractionGuard.mjs
13+
14+
Do not edit any other file.
15+
16+
## Fail-Fast Gate
17+
Before editing:
18+
- confirm file exists
19+
20+
If not:
21+
- stop
22+
- no changes
23+
- no ZIP
24+
25+
## Exact Enhancements
26+
27+
### 1. Add detection for inline arrow/function variants
28+
Detect:
29+
- `(value) => Number.isFinite`
30+
- `Number.isFinite(` (outside shared import context)
31+
- `typeof value === 'object' && value !== null`
32+
33+
### 2. Add detection for renamed helper clones
34+
Detect patterns:
35+
- `finiteNumber`
36+
- `positiveInt`
37+
- `plainObj`
38+
(only when used in function declarations)
39+
40+
### 3. Add detection for deep relative traversal attempts
41+
Detect:
42+
- '../../../../src/shared'
43+
- '../../../src/../shared'
44+
45+
### 4. Improve output grouping
46+
Group results by:
47+
- violation type
48+
- file
49+
50+
### 5. Output summary
51+
At end print:
52+
- total files scanned
53+
- total violations
54+
- types of violations found
55+
56+
### 6. Maintain strict exit behavior
57+
- 0 = clean
58+
- 1 = violations
59+
60+
## Hard Constraints
61+
- do not change existing checks
62+
- only extend detection
63+
- no new files
64+
- no config changes
65+
- no performance-heavy scanning
66+
- keep script readable
67+
68+
## Validation Checklist
69+
1. Script still runs
70+
2. New patterns detected
71+
3. Existing patterns still detected
72+
4. Output clearer
73+
5. Exit codes correct
74+
75+
## Non-Goals
76+
- no CI
77+
- no lint
78+
- no repo-wide changes
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# BUILD_PR_SHARED_EXTRACTION_25_GUARD_SELFTEST_RUNNER
2+
3+
## Purpose
4+
Add a narrow self-test runner for the shared-extraction guard so the guard logic can be verified without modifying repo source files.
5+
6+
## Single PR Purpose
7+
Create one new self-test runner:
8+
9+
- `tools/dev/checkSharedExtractionGuard.selftest.mjs`
10+
11+
This BUILD does not change the guard script itself unless a tiny export is already present and needed. Default expectation: create the self-test runner only.
12+
13+
## Exact Files Allowed
14+
Edit only these files:
15+
16+
1. `tools/dev/checkSharedExtractionGuard.selftest.mjs` **(new file)**
17+
2. `tools/dev/checkSharedExtractionGuard.mjs` **only if a minimal export is strictly required to support the self-test runner**
18+
19+
Do not edit any other file.
20+
21+
## Fail-Fast Gate
22+
Before editing:
23+
1. confirm `tools/dev/checkSharedExtractionGuard.mjs` exists
24+
25+
If not:
26+
- stop
27+
- no changes
28+
- no ZIP
29+
30+
## Exact New File
31+
Create:
32+
33+
`tools/dev/checkSharedExtractionGuard.selftest.mjs`
34+
35+
## Exact Self-Test Requirements
36+
37+
### 1) Execution model
38+
The self-test runner must:
39+
- create a temporary workspace under OS temp
40+
- create small synthetic `.js` files that intentionally violate guard rules
41+
- invoke the existing guard script against that temp workspace
42+
- verify the guard exits with failure on violation cases
43+
- verify the guard exits clean on a compliant case
44+
- clean up the temp workspace when done
45+
46+
### 2) Required test cases
47+
Include at least these cases:
48+
49+
#### violation: local helper definition
50+
Synthetic file containing one of:
51+
- `function asFiniteNumber(`
52+
Expected:
53+
- guard fails
54+
55+
#### violation: disallowed shared relative import
56+
Synthetic file containing:
57+
- `../../../src/shared/utils/numberUtils.js`
58+
Expected:
59+
- guard fails
60+
61+
#### violation: alias usage
62+
Synthetic file containing:
63+
- `@shared/utils/numberUtils.js`
64+
Expected:
65+
- guard fails
66+
67+
#### clean case
68+
Synthetic file with no banned helper definitions or banned import strings
69+
Expected:
70+
- guard passes
71+
72+
### 3) Output
73+
Print a concise pass/fail line for each case.
74+
Print a final summary:
75+
- tests run
76+
- tests passed
77+
- tests failed
78+
79+
### 4) Exit behavior
80+
- exit code `0` if all self-tests pass
81+
- exit code `1` if any self-test fails
82+
83+
## Guard Integration Rules
84+
Preferred:
85+
- call the existing guard script as a child process
86+
87+
Allowed fallback only if strictly necessary:
88+
- make the minimum export change in `tools/dev/checkSharedExtractionGuard.mjs` to support invocation from the self-test runner
89+
90+
If fallback is used:
91+
- do not change detection rules
92+
- do not change guard behavior
93+
- do not add unrelated refactor
94+
95+
## Hard Constraints
96+
- no package.json changes
97+
- no CI changes
98+
- no npm script changes
99+
- no source/runtime app changes
100+
- no guard rule changes in this PR unless a tiny export is strictly required
101+
- keep one PR purpose only
102+
103+
## Validation Checklist
104+
1. Confirm no more than the 2 listed files changed
105+
2. Confirm the self-test runner creates and cleans temp test inputs
106+
3. Confirm violation cases fail
107+
4. Confirm clean case passes
108+
5. Confirm self-test summary and exit code behavior are correct
109+
6. Confirm guard detection logic itself was not altered unless a minimal export was strictly required
110+
111+
## Non-Goals
112+
- no new guard rules
113+
- no repo source scanning changes
114+
- no CI wiring
115+
- no package.json wiring
116+
- no documentation changes

tools/dev/checkSharedExtractionGuard.mjs

Lines changed: 112 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,23 @@ const DIRECT_SHARED_IMPORT_RULES = [
2222
];
2323

2424
const ALIAS_RULE = { rule: "shared-alias-import-disallowed", regex: /@shared\//g, label: "rule:shared-alias-marker" };
25+
const NUMBER_UTIL_IMPORT_HINT = /from\s+["'][^"']*shared\/utils\/numberUtils\.js["']/;
26+
27+
const INLINE_HELPER_VARIANT_RULES = [
28+
{ rule: "inline-helper-clone", regex: /\(value\)\s*=>\s*Number\.isFinite/g, label: "rule:inline-arrow-number-is-finite" },
29+
{ rule: "inline-helper-clone", regex: /typeof\s+value\s*===\s*'object'\s*&&\s*value\s*!==\s*null/g, label: "rule:inline-plain-object-check" }
30+
];
31+
32+
const RENAMED_HELPER_FUNCTION_RULES = [
33+
{ rule: "renamed-helper-clone", regex: /function\s+finiteNumber\s*\(/g, label: "rule:renamed-helper-finiteNumber" },
34+
{ rule: "renamed-helper-clone", regex: /function\s+positiveInt\s*\(/g, label: "rule:renamed-helper-positiveInt" },
35+
{ rule: "renamed-helper-clone", regex: /function\s+plainObj\s*\(/g, label: "rule:renamed-helper-plainObj" }
36+
];
37+
38+
const DEEP_RELATIVE_TRAVERSAL_RULES = [
39+
{ rule: "deep-relative-shared-traversal", regex: /\.\.\/\.\.\/\.\.\/\.\.\/src\/shared/g, label: "rule:deep-relative-src-shared-traversal" },
40+
{ rule: "deep-relative-shared-traversal", regex: /\.\.\/\.\.\/\.\.\/src\/\.\.\/shared/g, label: "rule:relative-src-parent-shared-traversal" }
41+
];
2542

2643
async function pathExists(targetPath) {
2744
try {
@@ -86,9 +103,101 @@ function findViolations(fileContent, filePathFromRoot) {
86103
});
87104
}
88105

106+
for (const check of INLINE_HELPER_VARIANT_RULES) {
107+
const matches = fileContent.match(check.regex) || [];
108+
for (const _match of matches) {
109+
violations.push({
110+
file: filePathFromRoot,
111+
type: check.rule,
112+
match: check.label
113+
});
114+
}
115+
}
116+
117+
for (const check of RENAMED_HELPER_FUNCTION_RULES) {
118+
const matches = fileContent.match(check.regex) || [];
119+
for (const _match of matches) {
120+
violations.push({
121+
file: filePathFromRoot,
122+
type: check.rule,
123+
match: check.label
124+
});
125+
}
126+
}
127+
128+
for (const check of DEEP_RELATIVE_TRAVERSAL_RULES) {
129+
const matches = fileContent.match(check.regex) || [];
130+
for (const _match of matches) {
131+
violations.push({
132+
file: filePathFromRoot,
133+
type: check.rule,
134+
match: check.label
135+
});
136+
}
137+
}
138+
139+
const finiteMatches = [...fileContent.matchAll(/Number\.isFinite\s*\(/g)];
140+
for (const finiteMatch of finiteMatches) {
141+
const matchIndex = finiteMatch.index ?? -1;
142+
if (matchIndex < 0) continue;
143+
const lineStart = fileContent.lastIndexOf("\n", matchIndex) + 1;
144+
const lineEndCandidate = fileContent.indexOf("\n", matchIndex);
145+
const lineEnd = lineEndCandidate === -1 ? fileContent.length : lineEndCandidate;
146+
const lineText = fileContent.slice(lineStart, lineEnd);
147+
148+
if (NUMBER_UTIL_IMPORT_HINT.test(lineText)) continue;
149+
150+
violations.push({
151+
file: filePathFromRoot,
152+
type: "inline-helper-clone",
153+
match: "rule:number-is-finite-usage"
154+
});
155+
}
156+
89157
return violations;
90158
}
91159

160+
function summarizeViolationLabels(violations) {
161+
const counts = new Map();
162+
for (const violation of violations) {
163+
const next = (counts.get(violation.match) || 0) + 1;
164+
counts.set(violation.match, next);
165+
}
166+
return [...counts.entries()].sort(([a], [b]) => a.localeCompare(b));
167+
}
168+
169+
function printGroupedViolations(violations) {
170+
const byType = new Map();
171+
for (const violation of violations) {
172+
if (!byType.has(violation.type)) byType.set(violation.type, new Map());
173+
const byFile = byType.get(violation.type);
174+
if (!byFile.has(violation.file)) byFile.set(violation.file, []);
175+
byFile.get(violation.file).push(violation);
176+
}
177+
178+
const typeEntries = [...byType.entries()].sort(([a], [b]) => a.localeCompare(b));
179+
for (const [type, filesMap] of typeEntries) {
180+
console.error(`TYPE: ${type}`);
181+
const fileEntries = [...filesMap.entries()].sort(([a], [b]) => a.localeCompare(b));
182+
for (const [file, fileViolations] of fileEntries) {
183+
console.error(` FILE: ${file}`);
184+
const summaries = summarizeViolationLabels(fileViolations);
185+
for (const [label, count] of summaries) {
186+
console.error(` MATCH: ${label}${count > 1 ? ` (x${count})` : ""}`);
187+
}
188+
}
189+
}
190+
}
191+
192+
function printSummary(filesScanned, violations, useErrorStream = false) {
193+
const out = useErrorStream ? console.error : console.log;
194+
const types = [...new Set(violations.map((violation) => violation.type))].sort();
195+
const typeSummary = types.length > 0 ? types.join(", ") : "none";
196+
out(`Summary: files_scanned=${filesScanned}`);
197+
out(`Summary: total_violations=${violations.length}`);
198+
out(`Summary: violation_types=${typeSummary}`);
199+
}
200+
92201
async function run() {
93202
const repoRoot = process.cwd();
94203
const filesToScan = [];
@@ -108,15 +217,13 @@ async function run() {
108217

109218
if (violations.length === 0) {
110219
console.log("Shared extraction guard passed. No violations found.");
220+
printSummary(filesToScan.length, violations);
111221
process.exit(0);
112222
}
113223

114224
console.error(`Shared extraction guard failed with ${violations.length} violation(s).`);
115-
for (const violation of violations) {
116-
console.error(
117-
`${violation.file} | ${violation.type} | ${violation.match}`
118-
);
119-
}
225+
printGroupedViolations(violations);
226+
printSummary(filesToScan.length, violations, true);
120227
process.exit(1);
121228
}
122229

0 commit comments

Comments
 (0)