mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-30 11:22:34 +00:00
* docs: design engine dynamic-import reconciliation Co-Authored-By: Claude <noreply@anthropic.com> * fix(engine): reconcile dynamic import hardening Co-Authored-By: Claude <noreply@anthropic.com> * test(engine): guard dynamic import policy * docs: plan engine dynamic-import reconciliation Record the approved TDD sequence for selective engine-path hardening, repository guard wiring, documentation, and local verification. Preserve the no-version-bump and no-publication boundaries for the remaining work. Co-Authored-By: Claude <noreply@anthropic.com> * docs(engine): record static import invariant * fix(engine): parse block comments in import guard Co-Authored-By: Claude <noreply@anthropic.com> * fix(engine): parse dynamic imports with TypeScript Co-Authored-By: Claude <noreply@anthropic.com> * fix(engine): close import guard bypasses Co-Authored-By: Claude <noreply@anthropic.com> * fix(engine): close parser guard edge cases Co-Authored-By: Claude <noreply@anthropic.com> * fix(engine): aggregate parser diagnostics Co-Authored-By: Claude <noreply@anthropic.com> * fix(engine): bound dynamic import marker directive Require the line-level opt-out marker to be standalone inside real comment trivia so negated or incidental longer tokens cannot authorize an import. Preserve the existing general marked-line contract and pin it with focused regression coverage. Co-Authored-By: Claude <noreply@anthropic.com> * fix(engine): close Unicode marker boundary bypasses Treat Unicode identifier continuations as marker-token characters and inspect adjacent text by code point so supplementary-plane characters cannot turn longer comment tokens into approvals.\n\nCo-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
81 lines
2.8 KiB
TypeScript
81 lines
2.8 KiB
TypeScript
#!/usr/bin/env bun
|
|
|
|
import { readFile } from 'node:fs/promises';
|
|
import ts from 'typescript';
|
|
|
|
const MARKER = 'engine-dynamic-import-ok';
|
|
const MARKER_TOKEN_CHAR = /[\p{ID_Continue}$-]/u;
|
|
const files = process.argv.slice(2);
|
|
const violations: string[] = [];
|
|
const readErrors: string[] = [];
|
|
|
|
for (const file of files) {
|
|
let sourceText: string;
|
|
try {
|
|
sourceText = await readFile(file, 'utf8');
|
|
} catch (error) {
|
|
const detail = error instanceof Error ? error.message : String(error);
|
|
readErrors.push(`ERROR: cannot read input file ${file}: ${detail}`);
|
|
continue;
|
|
}
|
|
|
|
const sourceFile = ts.createSourceFile(
|
|
file,
|
|
sourceText,
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
ts.ScriptKind.TS,
|
|
);
|
|
const lines = sourceText.split(/\r?\n/);
|
|
const markerLines = new Set<number>();
|
|
|
|
if (sourceFile.parseDiagnostics.length > 0) {
|
|
const diagnostics = sourceFile.parseDiagnostics
|
|
.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, ' '))
|
|
.join('; ');
|
|
readErrors.push(`ERROR: cannot parse input file ${file}: ${diagnostics}`);
|
|
}
|
|
|
|
for (let markerPos = sourceText.indexOf(MARKER); markerPos >= 0; markerPos = sourceText.indexOf(MARKER, markerPos + MARKER.length)) {
|
|
const before = Array.from(sourceText.slice(0, markerPos)).at(-1);
|
|
const after = Array.from(sourceText.slice(markerPos + MARKER.length))[0];
|
|
const standaloneMarker = (!before || !MARKER_TOKEN_CHAR.test(before))
|
|
&& (!after || !MARKER_TOKEN_CHAR.test(after));
|
|
const token = ts.getTokenAtPosition(sourceFile, markerPos);
|
|
const insideToken = token.getStart(sourceFile) <= markerPos && markerPos < token.end;
|
|
if (standaloneMarker && !insideToken) {
|
|
markerLines.add(sourceFile.getLineAndCharacterOfPosition(markerPos).line);
|
|
}
|
|
}
|
|
|
|
function visit(node: ts.Node): void {
|
|
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
const { line } = sourceFile.getLineAndCharacterOfPosition(node.expression.getStart(sourceFile));
|
|
const sourceLine = lines[line] ?? '';
|
|
if (!markerLines.has(line)) {
|
|
violations.push(` ${file}:${line + 1}:${sourceLine}`);
|
|
}
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
|
|
visit(sourceFile);
|
|
}
|
|
|
|
for (const error of readErrors) console.error(error);
|
|
|
|
if (violations.length > 0) {
|
|
console.error('ERROR: unreviewed dynamic import on an engine-live path:');
|
|
console.error();
|
|
console.error(violations.join('\n'));
|
|
console.error();
|
|
console.error('Prefer a static top-level import. If lazy loading is load-bearing,');
|
|
console.error("append 'engine-dynamic-import-ok' to that exact line and document");
|
|
console.error('the startup or soft-failure boundary that requires it.');
|
|
process.exit(1);
|
|
}
|
|
|
|
if (readErrors.length > 0) process.exit(1);
|
|
|
|
console.log(`check-engine-dynamic-import: ok (${files.length} file(s) scanned)`);
|