Skip to content

Commit 486c8df

Browse files
author
DavidQ
committed
PR_26140_039: add intentional alias ledger guardrail documentation and validation reporting
1 parent 85f01d7 commit 486c8df

4 files changed

Lines changed: 392 additions & 0 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# PR_26140_039 Intentional Alias Ledger Guard
2+
3+
## Scope
4+
- Used PR_26140_038 as the prior delta.
5+
- Added a narrow intentional import/export alias ledger and guard script.
6+
- Added a dedicated package script: `npm run check:intentional-alias-ledger`.
7+
- Did not rename runtime symbols, change gameplay behavior, change schemas, or perform repo-wide cleanup.
8+
9+
## Guardrail Behavior
10+
- `tools/dev/checkIntentionalAliasLedgerGuard.mjs` scans repo-owned JS/MJS under `src`, `games`, `tools`, `tests`, and `samples`.
11+
- It skips `node_modules`, `tmp`, `tests/results`, generated/vendor/minified files, docs report/archive paths, and the archived `tools/Vector Map Editor` path.
12+
- It collects import/export statements containing `as` and requires each occurrence to match `tools/dev/intentionalAliasLedger.json`.
13+
- The guard also fails if a ledger entry goes stale and no longer matches current code.
14+
15+
## Intentional Alias Ledger Entries
16+
- Engine and shared-tool default public barrels: compatibility public APIs exposing default modules as named exports.
17+
- `src/shared/index.js` shared-family namespace barrels.
18+
- `tests/run-tests.mjs` `run as run...` imports because every Node test module exports `run`.
19+
- Engine and overlay namespace surface tests.
20+
- Shared legacy compatibility comparison tests.
21+
- Shared number legacy bridge local helper name.
22+
- Object Vector Studio V2 local transform adapter aliases.
23+
- Asteroids local `wrap(value, max)` gameplay adapter over the shared `wrap(value, min, max)` helper.
24+
25+
## Validation
26+
- PASS: `node --check tools/dev/checkIntentionalAliasLedgerGuard.mjs`.
27+
- PASS: `npm run check:intentional-alias-ledger`.
28+
- `files_scanned=1772`
29+
- `aliases_found=297`
30+
- `ledger_entries=10`
31+
- INFO: `npm run build` is not defined in `package.json`.
32+
- PASS: `npm run build:manifest`; generated `docs/build/sample-manifest.json` was removed after validation.
33+
- PASS: `node tests\games\AsteroidsValidation.test.mjs`.
34+
- PASS: `node tests\games\AsteroidsManifestScreenDimensions.test.mjs`.
35+
- PASS: `node tests\games\AsteroidsPresentation.test.mjs`.
36+
- PASS: `npx playwright test tests/playwright/tools/AsteroidsGameSceneCleanup.spec.mjs --project=playwright --workers=1 --reporter=list`.
37+
- PASS: `node tests\tools\ObjectVectorFinalRuntimeCleanup.test.mjs`.
38+
- PASS: `node tests\tools\ObjectVectorStudioV2DeleteCleanup.test.mjs`.
39+
- PASS: `npx playwright test tests/playwright/tools/ObjectVectorStudioV2FirstClassToolStarter.spec.mjs --project=playwright --workers=1 --reporter=list`.
40+
- PASS: `npm run test:workspace-v2` (58 passed).
41+
- PASS: required alias search completed:
42+
- `rg -n "^\s*import\b.*\bas\b|^\s*export\b.*\bas\b" src games tools tests samples -g "*.js" -g "*.mjs" -g "!**/node_modules/**" -g "!**/tests/results/**" -g "!docs/dev/reports/**" -g "!docs/archive/**" -g "!tools/Vector Map Editor/**" -g "!**/generated/**" -g "!**/vendor/**" -g "!**/*.min.js"`
43+
- Remaining line-level hits are intentional categories covered by the ledger.
44+
- PASS: `git diff --check` returned no whitespace errors; Git reported line-ending normalization warnings only.
45+
- PASS: changed-file guard found no modified node_modules, tests/results, docs/dev/reports snapshots, archived tools, generated bundles, vendor files, bundled files, or minified JS files.
46+
47+
## Observed Existing Guard
48+
- INFO: `npm run check:shared-extraction-guard` was run while checking guard patterns and currently fails against its existing baseline with unrelated shared-extraction backlog drift. This PR does not change that guard or its baseline.
49+
50+
## Notes
51+
- The new alias guard is intentionally separate from `pretest` because the existing shared-extraction guard is already noisy against its baseline. The dedicated script gives reviewers and future PRs a precise guardrail without changing runtime behavior.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"check:shared-extraction-guard": "node tools/dev/checkSharedExtractionGuard.mjs",
1111
"check:phase24-closeout-guard": "node tools/dev/checkPhase24CloseoutExecutionGuard.mjs",
1212
"check:style-system-guard": "node tools/dev/checkStyleSystemGuard.mjs",
13+
"check:intentional-alias-ledger": "node tools/dev/checkIntentionalAliasLedgerGuard.mjs",
1314
"check:games-template-contract": "node ./scripts/validate-games-template-contract.mjs",
1415
"codex:review-artifacts": "node ./scripts/write-codex-review-artifacts.mjs",
1516
"test:asset-manager-v2": "playwright test tests/playwright/tools/AssetManagerV2.spec.mjs --project=playwright --workers=1 --reporter=list",
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
4+
const repoRoot = process.cwd();
5+
const ledgerPath = path.join(repoRoot, "tools/dev/intentionalAliasLedger.json");
6+
const scanRoots = ["src", "games", "tools", "tests", "samples"];
7+
const sourceExtensions = new Set([".js", ".mjs"]);
8+
const ignoredDirNames = new Set(["node_modules", ".git", "tmp", "results", "generated", "vendor"]);
9+
const ignoredPathFragments = [
10+
"docs/dev/reports/",
11+
"docs/archive/",
12+
"tests/results/",
13+
"tools/Vector Map Editor/"
14+
];
15+
16+
function toPosix(value) {
17+
return value.replace(/\\/g, "/");
18+
}
19+
20+
function readJson(filePath) {
21+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
22+
}
23+
24+
function isIgnoredPath(relativePath) {
25+
return ignoredPathFragments.some((fragment) => relativePath.startsWith(fragment));
26+
}
27+
28+
function collectSourceFiles(startPath, outFiles) {
29+
if (!fs.existsSync(startPath)) return;
30+
const entries = fs.readdirSync(startPath, { withFileTypes: true });
31+
for (const entry of entries) {
32+
const fullPath = path.join(startPath, entry.name);
33+
const relPath = toPosix(path.relative(repoRoot, fullPath));
34+
if (entry.isDirectory()) {
35+
if (ignoredDirNames.has(entry.name)) continue;
36+
if (isIgnoredPath(`${relPath}/`)) continue;
37+
collectSourceFiles(fullPath, outFiles);
38+
continue;
39+
}
40+
41+
if (!entry.isFile()) continue;
42+
if (isIgnoredPath(relPath)) continue;
43+
if (entry.name.toLowerCase().endsWith(".min.js")) continue;
44+
if (!sourceExtensions.has(path.extname(entry.name).toLowerCase())) continue;
45+
outFiles.push(fullPath);
46+
}
47+
}
48+
49+
function normalizeStatement(statement) {
50+
return statement
51+
.split(/\r?\n/)
52+
.map((line) => line.trim())
53+
.filter(Boolean)
54+
.join(" ")
55+
.replace(/\s+/g, " ")
56+
.trim();
57+
}
58+
59+
function collectImportExportStatements(source, file) {
60+
const statements = [];
61+
const lines = source.split(/\r?\n/);
62+
let current = null;
63+
64+
for (let index = 0; index < lines.length; index += 1) {
65+
const line = lines[index];
66+
if (!current && /^\s*(?:import|export)\b/.test(line)) {
67+
current = {
68+
startLine: index + 1,
69+
chunks: [line]
70+
};
71+
} else if (current) {
72+
current.chunks.push(line);
73+
}
74+
75+
if (current && /;\s*$/.test(line)) {
76+
const statement = normalizeStatement(current.chunks.join("\n"));
77+
if (/\bas\b/.test(statement)) {
78+
statements.push({
79+
file,
80+
line: current.startLine,
81+
statement
82+
});
83+
}
84+
current = null;
85+
}
86+
}
87+
88+
if (current) {
89+
const statement = normalizeStatement(current.chunks.join("\n"));
90+
if (/\bas\b/.test(statement)) {
91+
statements.push({
92+
file,
93+
line: current.startLine,
94+
statement
95+
});
96+
}
97+
}
98+
99+
return statements;
100+
}
101+
102+
function compileEntry(entry) {
103+
const statementRegex = entry.statementRegex ? new RegExp(entry.statementRegex) : null;
104+
const fileRegex = entry.fileRegex ? new RegExp(entry.fileRegex) : null;
105+
return {
106+
...entry,
107+
statementRegex,
108+
fileRegex,
109+
files: new Set(entry.files || []),
110+
statements: new Set(entry.statements || [])
111+
};
112+
}
113+
114+
function entryMatchesOccurrence(entry, occurrence) {
115+
const fileMatches = entry.files.has(occurrence.file) || (entry.fileRegex && entry.fileRegex.test(occurrence.file));
116+
if (!fileMatches) return false;
117+
118+
return entry.statements.has(occurrence.statement)
119+
|| (entry.statementRegex && entry.statementRegex.test(occurrence.statement));
120+
}
121+
122+
function validateLedger(ledger) {
123+
if (!ledger || ledger.version !== 1 || !Array.isArray(ledger.entries)) {
124+
throw new Error("Invalid intentional alias ledger format.");
125+
}
126+
127+
for (const entry of ledger.entries) {
128+
if (!entry.id || !entry.reason) {
129+
throw new Error("Intentional alias ledger entries require id and reason.");
130+
}
131+
if (!entry.files && !entry.fileRegex) {
132+
throw new Error(`Ledger entry ${entry.id} requires files or fileRegex.`);
133+
}
134+
if (!entry.statements && !entry.statementRegex) {
135+
throw new Error(`Ledger entry ${entry.id} requires statements or statementRegex.`);
136+
}
137+
}
138+
}
139+
140+
function printFailures(unexpected, staleEntries) {
141+
if (unexpected.length > 0) {
142+
console.error("Unexpected import/export alias statements:");
143+
for (const occurrence of unexpected) {
144+
console.error(`- ${occurrence.file}:${occurrence.line} ${occurrence.statement}`);
145+
}
146+
}
147+
148+
if (staleEntries.length > 0) {
149+
console.error("Ledger entries with no current matches:");
150+
for (const entry of staleEntries) {
151+
console.error(`- ${entry.id}`);
152+
}
153+
}
154+
}
155+
156+
function run() {
157+
if (!fs.existsSync(ledgerPath)) {
158+
throw new Error("Missing tools/dev/intentionalAliasLedger.json.");
159+
}
160+
161+
const ledger = readJson(ledgerPath);
162+
validateLedger(ledger);
163+
const entries = ledger.entries.map(compileEntry);
164+
165+
const files = [];
166+
for (const root of scanRoots) {
167+
collectSourceFiles(path.join(repoRoot, root), files);
168+
}
169+
170+
const occurrences = [];
171+
for (const filePath of files) {
172+
const relativePath = toPosix(path.relative(repoRoot, filePath));
173+
const source = fs.readFileSync(filePath, "utf8");
174+
occurrences.push(...collectImportExportStatements(source, relativePath));
175+
}
176+
177+
const matchCounts = new Map(entries.map((entry) => [entry.id, 0]));
178+
const unexpected = [];
179+
180+
for (const occurrence of occurrences) {
181+
const matchedEntries = entries.filter((entry) => entryMatchesOccurrence(entry, occurrence));
182+
if (matchedEntries.length === 0) {
183+
unexpected.push(occurrence);
184+
continue;
185+
}
186+
for (const entry of matchedEntries) {
187+
matchCounts.set(entry.id, (matchCounts.get(entry.id) || 0) + 1);
188+
}
189+
}
190+
191+
const staleEntries = entries.filter((entry) => (matchCounts.get(entry.id) || 0) === 0);
192+
if (unexpected.length > 0 || staleEntries.length > 0) {
193+
console.error("INTENTIONAL_ALIAS_LEDGER_GUARD_FAILED");
194+
printFailures(unexpected, staleEntries);
195+
console.error(`Summary: files_scanned=${files.length}`);
196+
console.error(`Summary: aliases_found=${occurrences.length}`);
197+
console.error(`Summary: unexpected_aliases=${unexpected.length}`);
198+
console.error(`Summary: stale_ledger_entries=${staleEntries.length}`);
199+
process.exit(1);
200+
}
201+
202+
console.log("INTENTIONAL_ALIAS_LEDGER_GUARD_PASSED");
203+
console.log(`Summary: files_scanned=${files.length}`);
204+
console.log(`Summary: aliases_found=${occurrences.length}`);
205+
console.log(`Summary: ledger_entries=${entries.length}`);
206+
}
207+
208+
run();

0 commit comments

Comments
 (0)