* refactor(javascript): break the semantic analysis import cycle Split shared semantic facts out of the analysis hub so projectors of effects, evaluation, and identity no longer import one another in a cycle. * refactor(javascript): share the import specifier reader Read static imports in one module used by the boundary checker and both cycle guards, so those rules cannot drift apart. --------- Co-authored-by: Cursor Agent <[email protected]>
105 lines
3.2 KiB
JavaScript
105 lines
3.2 KiB
JavaScript
import { isAbsolute, relative, resolve } from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
|
import { readImportSpecifiers } from "./import-specifiers.mjs";
|
|
|
|
const PURE_LAYERS = new Set(["domain", "contracts"]);
|
|
const MIGRATED_PROVIDER_ROOTS = new Set([
|
|
"sqlite",
|
|
"evm",
|
|
"go",
|
|
"il2cpp",
|
|
"model",
|
|
"gpu",
|
|
"python",
|
|
"rust",
|
|
"wasm",
|
|
"captures",
|
|
"signals",
|
|
"android",
|
|
"firmware",
|
|
"ghidra",
|
|
"hopper",
|
|
"ida",
|
|
"inspector",
|
|
"javascript",
|
|
]);
|
|
const repositoryPath = (root, path) =>
|
|
relative(root, path).replaceAll("\\", "/");
|
|
|
|
const sourceTarget = (file, specifier) => {
|
|
if (specifier.startsWith("file:")) return fileURLToPath(specifier);
|
|
if (isAbsolute(specifier)) return resolve(specifier);
|
|
return specifier.startsWith(".")
|
|
? fileURLToPath(new URL(specifier, pathToFileURL(file)))
|
|
: undefined;
|
|
};
|
|
|
|
const failedBoundary = (file, target) => {
|
|
if (!file.startsWith("src/") || !target.startsWith("src/")) return undefined;
|
|
const layer = file.split("/")[1];
|
|
const targetLayer = target.split("/")[1];
|
|
if (
|
|
file.startsWith("src/process/capture/") &&
|
|
(["application", "composition", "server", "cli", "main"].includes(
|
|
targetLayer,
|
|
) ||
|
|
/^src\/(?:cli|main)\./u.test(target))
|
|
)
|
|
return "process-capture";
|
|
if (
|
|
file.startsWith("src/application/javascript/") &&
|
|
/^src\/artifacts\/(?:Asar|Directory)ArtifactReader\.(?:js|ts)$/u.test(
|
|
target,
|
|
)
|
|
)
|
|
return "provider-construction";
|
|
if (
|
|
(file.startsWith("src/artifacts/javascript/") ||
|
|
file.startsWith("src/artifacts/apple/") ||
|
|
file.startsWith("src/artifacts/inventory/") ||
|
|
file.startsWith("src/artifacts/extraction/") ||
|
|
/^src\/artifacts\/ArtifactHash\.(?:js|ts)$/u.test(file)) &&
|
|
(["application", "composition", "server", "cli", "main"].includes(
|
|
targetLayer,
|
|
) ||
|
|
/^src\/(?:cli|main)\./u.test(target))
|
|
)
|
|
return "artifact-acquisition";
|
|
if (PURE_LAYERS.has(layer)) {
|
|
if (
|
|
(/^src\/[^/]+\//u.test(target) && !PURE_LAYERS.has(targetLayer)) ||
|
|
/^src\/(?:cli|main)\./u.test(target)
|
|
)
|
|
return "pure-layer";
|
|
}
|
|
if (layer === "application" && targetLayer === "composition")
|
|
return "application-composition";
|
|
if (
|
|
(layer === "application" || layer === "server") &&
|
|
(MIGRATED_PROVIDER_ROOTS.has(targetLayer) ||
|
|
/^src\/(?:artifacts\/ArtifactProvider|dotnet\/ManagedStaticProvider|native\/NativeMacOSProvider)\.[^/]+$/u.test(
|
|
target,
|
|
) ||
|
|
/^src\/browser\/[^/]*Provider\.[^/]+$/u.test(target))
|
|
)
|
|
return "provider-construction";
|
|
return undefined;
|
|
};
|
|
|
|
/** Check resolved source ownership, including type imports and reexports. */
|
|
export const inspectModuleBoundaries = (file, source, root) => {
|
|
const absoluteFile = resolve(root, file);
|
|
const importer = repositoryPath(root, absoluteFile);
|
|
const violations = [];
|
|
for (const { specifier, line } of readImportSpecifiers(source)) {
|
|
const resolved = sourceTarget(absoluteFile, specifier);
|
|
if (resolved === undefined) continue;
|
|
const target = repositoryPath(root, resolved);
|
|
const boundary = failedBoundary(importer, target);
|
|
if (boundary !== undefined)
|
|
violations.push({ file: importer, line, specifier, target, boundary });
|
|
}
|
|
return violations;
|
|
};
|