From 9aec6908b5c7ad50da6882415700cbe53c3f62ed Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 29 Sep 2026 15:30:43 +0800 Subject: [PATCH] refactor(javascript): remove analysis ceilings --- src/application/JavaScriptArtifactAnalysis.ts | 36 ++------- .../JavaScriptArtifactAnalysisTypes.ts | 20 +---- .../JavaScriptArtifactGraphBundlers.ts | 2 - .../JavaScriptArtifactGraphDocuments.ts | 10 +-- .../JavaScriptArtifactPathResolution.test.ts | 77 +++++++++++++++++-- .../JavaScriptArtifactPathResolution.ts | 53 ++++++------- .../JavaScriptModuleRelationships.ts | 17 +--- .../JavaScriptSemanticGraphBuilder.test.ts | 44 ++++------- .../JavaScriptSemanticGraphBuilder.ts | 8 +- .../JavaScriptSemanticGraphFlowProjection.ts | 20 ----- .../JavaScriptSemanticGraphResources.test.ts | 16 +--- .../javascriptApplicationVersionKeys.ts | 3 +- src/domain/javascriptAstFingerprint.test.ts | 4 +- src/domain/javascriptAstFingerprint.ts | 35 ++------- .../javascriptExportShapeComparisonSchemas.ts | 1 - ...avascriptSemanticAnalysis.dataflow.test.ts | 1 - ...vascriptSemanticAnalysis.rejection.test.ts | 27 ++++--- ...vascriptSemanticAnalysis.structure.test.ts | 1 - src/domain/javascriptSemanticAnalysis.ts | 29 ++----- .../javascriptSemanticCallResolution.ts | 30 ++------ src/domain/javascriptSemanticCalls.ts | 1 - src/domain/javascriptSemanticCoverage.ts | 42 ++-------- src/domain/javascriptSemanticFingerprints.ts | 6 +- src/domain/javascriptSemanticIr.ts | 16 +--- src/domain/javascriptSemanticProjection.ts | 10 --- src/domain/javascriptSemanticPromises.ts | 6 +- src/domain/javascriptSemanticProvenance.ts | 21 +---- src/domain/javascriptSemanticReturns.ts | 1 - src/domain/javascriptSemanticState.ts | 6 +- src/domain/javascriptSemanticValueTypes.ts | 6 +- src/domain/javascriptSemanticValues.ts | 24 +----- ...riptStaticAnalysis.no_finding_caps.test.ts | 18 ++--- src/domain/javascriptStaticAnalysis.ts | 52 ++++--------- src/domain/javascriptStaticAnalysisBundler.ts | 32 ++------ src/domain/javascriptStaticAnalysisHelpers.ts | 67 ++++++---------- src/domain/javascriptStaticAnalysisState.ts | 2 - src/domain/javascriptStaticAnalysisTypes.ts | 26 ++----- .../applicationEvidenceMcp.test.ts | 2 +- .../applicationWorkflowCli.test.ts | 11 ++- 39 files changed, 249 insertions(+), 534 deletions(-) diff --git a/src/application/JavaScriptArtifactAnalysis.ts b/src/application/JavaScriptArtifactAnalysis.ts index ab7847e05e..711f39a51b 100644 --- a/src/application/JavaScriptArtifactAnalysis.ts +++ b/src/application/JavaScriptArtifactAnalysis.ts @@ -2,10 +2,6 @@ import { createHash } from "node:crypto"; import { analyzeParsedJavaScriptStaticSource } from "../domain/javascriptStaticAnalysis.js"; import { analyzeParsedJavaScriptSemantics } from "../domain/javascriptSemanticAnalysis.js"; -import { - DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS, - type JavaScriptSemanticLimits, -} from "../domain/javascriptSemanticIr.js"; import { parseJavaScriptSource } from "../domain/javascriptSourceParser.js"; import type { JavaScriptSourceRange, @@ -58,10 +54,7 @@ export const analyzeJavaScriptArtifactFiles = ( const finalizeArtifactAnalysis = ( state: MutableArtifactAnalysis, ): JavaScriptArtifactAnalysis => { - const sourceMapTruncations = state.sourceMaps.filter( - ({ status }) => status === "truncated", - ).length; - const truncatedScopes = state.truncatedScopes + sourceMapTruncations; + const truncatedScopes = state.truncatedScopes; return { files: state.files, packages: state.packages, @@ -103,27 +96,16 @@ const analyzeArtifactFile = ( state.parseFailures += 1; return; } - const analysis = analyzeParsedJavaScriptStaticSource( - file.text.value, - parsed, - { - maxAstNodes: Number.POSITIVE_INFINITY, - deadline: Number.POSITIVE_INFINITY, - now: () => 0, - }, - ); + const analysis = analyzeParsedJavaScriptStaticSource(file.text.value, parsed); const staticFindings = findingCount(analysis); - const semanticLimits: JavaScriptSemanticLimits = - DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS; const semantics = analysis.parse_status === "complete" || analysis.parse_status === "partial" - ? analyzeParsedJavaScriptSemantics(parsed, semanticLimits) + ? analyzeParsedJavaScriptSemantics(parsed) : null; state.files.push({ file, javascript: analysis, - semantic: - semantics === null ? null : { ir: semantics, limits: semanticLimits }, + semantic: semantics === null ? null : { ir: semantics }, }); state.visitedNodes += analysis.visited_ast_nodes; state.findings += staticFindings + (semantics?.moduleLinks.length ?? 0); @@ -132,8 +114,6 @@ const analyzeArtifactFile = ( 0, ); if (analysis.parse_status === "failed") state.parseFailures += 1; - if (analysis.parse_status === "truncated") state.truncatedScopes += 1; - if (semantics?.coverage.status === "truncated") state.truncatedScopes += 1; }; const addSourceMap = ( @@ -293,7 +273,6 @@ const collectSourceMapOriginals = ( sha256: file.sha256, status: "included", sources, - omitted_sources: 0, limitation: null, }; }; @@ -308,7 +287,6 @@ const unavailableSourceMap = ( sha256: file.sha256, status: "invalid", sources: [], - omitted_sources: 0, limitation: "Source-map text could not be decoded as UTF-8.", }; }; @@ -340,7 +318,6 @@ const invalidSourceMap = ( sha256: file.sha256, status: "invalid", sources: [], - omitted_sources: 0, limitation, }); @@ -376,10 +353,7 @@ const pointForOffset = ( }; const resolveSourceName = (root: string, source: string): string => - `${root}${root !== "" && !root.endsWith("/") ? "/" : ""}${source}`.slice( - 0, - 4_096, - ); + `${root}${root !== "" && !root.endsWith("/") ? "/" : ""}${source}`; const optionalString = (value: unknown): string | null => typeof value === "string" && value.length > 0 ? value : null; diff --git a/src/application/JavaScriptArtifactAnalysisTypes.ts b/src/application/JavaScriptArtifactAnalysisTypes.ts index bac87494ad..65b1b9c244 100644 --- a/src/application/JavaScriptArtifactAnalysisTypes.ts +++ b/src/application/JavaScriptArtifactAnalysisTypes.ts @@ -2,10 +2,7 @@ import type { JavaScriptSourceRange, JavaScriptStaticAnalysis, } from "../domain/javascriptStaticAnalysisTypes.js"; -import type { - JavaScriptSemanticIr, - JavaScriptSemanticLimits, -} from "../domain/javascriptSemanticIr.js"; +import type { JavaScriptSemanticIr } from "../domain/javascriptSemanticIr.js"; import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; interface JavaScriptStructuredObservation { @@ -52,30 +49,22 @@ export interface JavaScriptSourceMapOriginal { readonly content_sha256: string | null; } -/** Bounded local source-map parse result. */ +/** Local source-map parse result. */ export type JavaScriptSourceMapObservation = JavaScriptStructuredObservation & ( | { readonly status: "included"; readonly sources: readonly JavaScriptSourceMapOriginal[]; - readonly omitted_sources: 0; readonly limitation: null; } - | { - readonly status: "truncated"; - readonly sources: readonly JavaScriptSourceMapOriginal[]; - readonly omitted_sources: number | null; - readonly limitation: string; - } | { readonly status: "invalid"; readonly sources: readonly []; - readonly omitted_sources: 0; readonly limitation: string; } ); -/** Bounded parse status for one inventoried JSON module. */ +/** Parse status for one inventoried JSON module. */ export type JavaScriptJsonModuleObservation = JavaScriptStructuredObservation & ( | { @@ -104,11 +93,10 @@ export interface AnalyzedJavaScriptArtifactFile { readonly javascript: JavaScriptStaticAnalysis | null; readonly semantic: { readonly ir: JavaScriptSemanticIr; - readonly limits: JavaScriptSemanticLimits; } | null; } -/** Complete bounded static-analysis projection before graph construction. */ +/** Complete static-analysis projection before graph construction. */ export interface JavaScriptArtifactAnalysis { readonly files: readonly AnalyzedJavaScriptArtifactFile[]; readonly packages: readonly JavaScriptPackageObservation[]; diff --git a/src/application/JavaScriptArtifactGraphBundlers.ts b/src/application/JavaScriptArtifactGraphBundlers.ts index 04c83125e6..2cfa6ab2a9 100644 --- a/src/application/JavaScriptArtifactGraphBundlers.ts +++ b/src/application/JavaScriptArtifactGraphBundlers.ts @@ -134,8 +134,6 @@ const addBundlerModuleNodes = ( moduleValue.structural_fingerprint_sha256, structural_fingerprint_algorithm: moduleValue.structural_fingerprint_algorithm, - structural_fingerprint_status: - moduleValue.structural_fingerprint_status, exports: moduleValue.exports, }, evidence: astObservationEvidence({ diff --git a/src/application/JavaScriptArtifactGraphDocuments.ts b/src/application/JavaScriptArtifactGraphDocuments.ts index 3c72e024fc..fa48f552c0 100644 --- a/src/application/JavaScriptArtifactGraphDocuments.ts +++ b/src/application/JavaScriptArtifactGraphDocuments.ts @@ -6,10 +6,7 @@ import { type JavaScriptArtifactGraphContext, } from "./JavaScriptArtifactGraphContext.js"; import { artifactObservationEvidence } from "./JavaScriptArtifactGraphEvidence.js"; -import { - completeApplicationCoverage, - partialApplicationCoverage, -} from "../domain/javascriptApplicationEvidenceSchemas.js"; +import { completeApplicationCoverage } from "../domain/javascriptApplicationEvidenceSchemas.js"; import { resolveArtifactPathByContext } from "./JavaScriptArtifactPathResolution.js"; /** Project HTML renderer entrypoints and their local script assets. */ @@ -91,10 +88,7 @@ export const addJavaScriptSourceMapOriginals = ( const mapFile = context.filesByPath.get(sourceMap.path); const mapNode = context.fileNodes.get(sourceMap.path); if (mapFile === undefined || mapNode === undefined) continue; - const coverage = - sourceMap.status === "truncated" - ? partialApplicationCoverage([], null) - : completeApplicationCoverage(); + const coverage = completeApplicationCoverage(); for (const original of sourceMap.sources) { const node = context.accumulator.addNode({ kind: "source-module", diff --git a/src/application/JavaScriptArtifactPathResolution.test.ts b/src/application/JavaScriptArtifactPathResolution.test.ts index 8aeb049975..63d083fe6a 100644 --- a/src/application/JavaScriptArtifactPathResolution.test.ts +++ b/src/application/JavaScriptArtifactPathResolution.test.ts @@ -159,9 +159,81 @@ describe("contextual JavaScript artifact path resolution", () => { }), ).toMatchObject({ resolution_status: "not-found", resolved_path: null }); }); + + it("resolves inventoried paths longer than the former character ceiling", () => { + const declaredPath = `./${"a".repeat(5_000)}.js`; + const expectedPath = `app/${declaredPath.slice(2)}`; + const files = fileMap([ + file("app/consumer.js", "root"), + file(expectedPath, "root"), + ]); + + expect( + resolve({ + declaredPath, + sourcePath: "app/consumer.js", + context: "module-specifier", + files, + }), + ).toMatchObject({ + resolution_status: "resolved", + resolved_path: expectedPath, + limitations: [], + }); + }); }); describe("contextual JavaScript package path resolution", () => { + it("follows package entrypoints beyond the former recursion ceiling", () => { + const files = fileMap([ + file("app/consumer.js", "root"), + file( + "app/node_modules/pkg-1/package.json", + "root", + '{"main":"../pkg-2"}', + ), + file( + "app/node_modules/pkg-2/package.json", + "root", + '{"main":"../pkg-3"}', + ), + file( + "app/node_modules/pkg-3/package.json", + "root", + '{"main":"../pkg-4"}', + ), + file( + "app/node_modules/pkg-4/package.json", + "root", + '{"main":"../pkg-5"}', + ), + file( + "app/node_modules/pkg-5/package.json", + "root", + '{"main":"../pkg-6"}', + ), + file( + "app/node_modules/pkg-6/package.json", + "root", + '{"main":"entry.js"}', + ), + file("app/node_modules/pkg-6/entry.js", "root"), + ]); + + expect( + resolve({ + declaredPath: "pkg-1", + sourcePath: "app/consumer.js", + context: "module-specifier", + files, + }), + ).toMatchObject({ + resolution_status: "resolved", + resolved_path: "app/node_modules/pkg-6/entry.js", + limitations: [], + }); + }); + it("keeps an inventoried but unreadable directory package unavailable", () => { const files = fileMap([ file("app/package.json", "root"), @@ -272,11 +344,6 @@ describe("contextual JavaScript module identity", () => { }}); window.loadURL("file:///renderer/index.html"); `, - { - maxAstNodes: Number.POSITIVE_INFINITY, - deadline: Number.POSITIVE_INFINITY, - now: () => 0, - }, ); expect(analysis.role_paths).toEqual( diff --git a/src/application/JavaScriptArtifactPathResolution.ts b/src/application/JavaScriptArtifactPathResolution.ts index 3ed0408ec4..92462c4dea 100644 --- a/src/application/JavaScriptArtifactPathResolution.ts +++ b/src/application/JavaScriptArtifactPathResolution.ts @@ -21,7 +21,7 @@ interface ArtifactPathResolutionBase { readonly limitations: readonly string[]; } -/** Explicit outcome of bounded, artifact-confined path resolution. */ +/** Explicit outcome of artifact-confined path resolution. */ export type ArtifactPathResolution = ArtifactPathResolutionBase & ( | { @@ -54,7 +54,6 @@ const EXTENSIONS = [ ".html", ".node", ]; -const MAX_PACKAGE_DEPTH = 4; const NODE_BUILTINS = new Set( builtinModules.map((name) => name.replace(/^node:/u, "")), ); @@ -74,11 +73,11 @@ type CandidateResolution = /** Resolve one declaration under its exact syntax/metadata context. */ export const resolveArtifactPathByContext = ( input: ResolveArtifactPathInput, -): ArtifactPathResolution => resolveAtDepth(input, 0); +): ArtifactPathResolution => resolvePackagePath(input, new Set()); -const resolveAtDepth = ( +const resolvePackagePath = ( input: ResolveArtifactPathInput, - packageDepth: number, + packageChain: ReadonlySet, ): ArtifactPathResolution => { const rejected = rejectDeclaration(input); if (rejected !== null) return rejected; @@ -86,7 +85,7 @@ const resolveAtDepth = ( if (typeof candidate !== "string") return candidate; const confined = confineCandidate(input, candidate); if (typeof confined !== "string") return confined; - const resolved = resolveCandidate(input, confined, packageDepth); + const resolved = resolveCandidate(input, confined, packageChain); return outcome(input, resolved); }; @@ -98,10 +97,6 @@ const rejectDeclaration = ( return unresolvedOutcome(input, "rejected", [ "The declared path is empty.", ]); - if (declared.length > 4_096) - return unresolvedOutcome(input, "rejected", [ - "The declared path exceeds the 4096-character resolution bound.", - ]); if (declared.includes("\0") || declared.includes("\\")) return unresolvedOutcome(input, "rejected", [ "NUL and backslash path syntax are not admitted for canonical artifact paths.", @@ -239,7 +234,7 @@ const confineCandidate = ( const resolveCandidate = ( input: ResolveArtifactPathInput, candidate: string, - packageDepth: number, + packageChain: ReadonlySet, ): CandidateResolution => { const source = input.files.get(input.sourcePath); for (const path of directCandidates(candidate)) { @@ -251,14 +246,6 @@ const resolveCandidate = ( ) return { resolvedPath: path, status: "resolved", limitations: [] }; } - if (packageDepth >= MAX_PACKAGE_DEPTH) - return { - resolvedPath: null, - status: "unavailable", - limitations: [ - `Directory package resolution exceeded its ${String(MAX_PACKAGE_DEPTH)}-package recursion bound.`, - ], - }; const packagePath = posix.join(candidate, "package.json"); const packageFile = input.files.get(packagePath); if ( @@ -275,24 +262,32 @@ const resolveCandidate = ( `Directory package metadata ${packagePath} was inventoried but its text is unavailable: ${packageFile.text.reason}.`, ], }; + if (packageChain.has(packagePath)) + return { + resolvedPath: null, + status: "unavailable", + limitations: [ + `Directory package entrypoint cycle includes ${packagePath}.`, + ], + }; const main = packageEntry(packageFile.text.value, input.moduleKind); if (main.status === "invalid") return { resolvedPath: null, status: "unavailable", limitations: [ - `Directory package metadata ${packagePath} is not valid bounded package JSON.`, + `Directory package metadata ${packagePath} is not valid package JSON.`, ], }; if (main.status === "missing") return notFoundCandidate(); - const nested = resolveAtDepth( + const nested = resolvePackagePath( { declaredPath: main.value, sourcePath: packagePath, context: "package-entrypoint", files: input.files, }, - packageDepth + 1, + new Set([...packageChain, packagePath]), ); return nested.resolution_status === "resolved" ? { @@ -332,7 +327,7 @@ const packageEntry = ( : [Reflect.get(value, "main"), Reflect.get(value, "module")]; const entry = preferred.find((candidate) => candidate !== undefined); if (entry === undefined) return { status: "missing" }; - return boundedPackagePath(entry); + return packagePathValue(entry); } catch { return { status: "invalid" }; } @@ -344,24 +339,24 @@ const packageExport = ( ): | { readonly status: "value"; readonly value: string } | { readonly status: "invalid" } => { - if (typeof value === "string") return boundedPackagePath(value); + if (typeof value === "string") return packagePathValue(value); if (typeof value !== "object" || value === null || Array.isArray(value)) return { status: "invalid" }; const root = Reflect.get(value, ".") ?? value; - if (typeof root === "string") return boundedPackagePath(root); + if (typeof root === "string") return packagePathValue(root); if (typeof root !== "object" || root === null || Array.isArray(root)) return { status: "invalid" }; const condition = Reflect.get(root, moduleKind ?? "default") ?? Reflect.get(root, "default"); - return boundedPackagePath(condition); + return packagePathValue(condition); }; -const boundedPackagePath = ( +const packagePathValue = ( value: unknown, ): | { readonly status: "value"; readonly value: string } | { readonly status: "invalid" } => - typeof value === "string" && value.length > 0 && value.length <= 4_096 + typeof value === "string" && value.length > 0 ? { status: "value", value } : { status: "invalid" }; @@ -369,7 +364,7 @@ const notFoundCandidate = (): CandidateResolution => ({ resolvedPath: null, status: "not-found", limitations: [ - "No bounded extension, directory package, or index candidate exists in the inventoried artifact container.", + "No extension, directory package, or index candidate exists in the inventoried artifact container.", ], }); diff --git a/src/application/JavaScriptModuleRelationships.ts b/src/application/JavaScriptModuleRelationships.ts index e45ca18352..461decdafb 100644 --- a/src/application/JavaScriptModuleRelationships.ts +++ b/src/application/JavaScriptModuleRelationships.ts @@ -4,12 +4,10 @@ import type { ApplicationNode } from "../domain/javascriptApplicationGraph.js"; import { completeApplicationCoverage, partialApplicationCoverage, - truncatedApplicationCoverage, } from "../domain/javascriptApplicationEvidenceSchemas.js"; import type { JavaScriptModuleOrigin, JavaScriptSemanticIr, - JavaScriptSemanticLimits, JavaScriptSemanticModuleLink, } from "../domain/javascriptSemanticIr.js"; import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; @@ -31,7 +29,6 @@ import { projectJavaScriptExportReturnShapes } from "./JavaScriptReturnShapeProj interface SemanticAnalysis { readonly ir: JavaScriptSemanticIr; - readonly limits: JavaScriptSemanticLimits; } interface RelationshipInput { @@ -369,16 +366,9 @@ const relationshipEvidence = ( const semanticCoverage = ( semantic: SemanticAnalysis, ): JavaScriptArtifactGraphCoverage => { - const limits = Object.entries(semantic.limits).map(([name, value]) => ({ - name: `semantic-${camelToKebab(name)}`, - value, - unit: "items" as const, - })); if (semantic.ir.coverage.status === "complete") - return completeApplicationCoverage(limits); - return semantic.ir.coverage.status === "truncated" - ? truncatedApplicationCoverage(limits, semantic.ir.coverage.omittedCount) - : partialApplicationCoverage(limits, semantic.ir.coverage.omittedCount); + return completeApplicationCoverage(); + return partialApplicationCoverage([], semantic.ir.coverage.omittedCount); }; const moduleFormat = ( @@ -420,8 +410,5 @@ const isExportLink = (link: JavaScriptSemanticModuleLink): boolean => link.kind === "re-export" || link.kind === "commonjs-export"; -const camelToKebab = (value: string): string => - value.replaceAll(/([a-z])([A-Z])/gu, "$1-$2").toLowerCase(); - const compareCodePoints = (left: string, right: string): number => left < right ? -1 : left > right ? 1 : 0; diff --git a/src/application/JavaScriptSemanticGraphBuilder.test.ts b/src/application/JavaScriptSemanticGraphBuilder.test.ts index 6d143d2d4b..21ae326129 100644 --- a/src/application/JavaScriptSemanticGraphBuilder.test.ts +++ b/src/application/JavaScriptSemanticGraphBuilder.test.ts @@ -5,10 +5,6 @@ import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTyp import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; import { queryJavaScriptSemanticGraph } from "../domain/javascriptSemanticQuery.js"; import { analyzeJavaScriptSemantics } from "../domain/javascriptSemanticAnalysis.js"; -import { - DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS, - type JavaScriptSemanticLimits, -} from "../domain/javascriptSemanticIr.js"; const SHA256 = "a".repeat(64); const GRAPH_ID = `jag_${"b".repeat(64)}`; @@ -173,25 +169,19 @@ it("connects caller results, direct returns, parameters, and captures", () => { expect(capture?.evidence.location).not.toEqual(readOuter.evidence.location); }); -it("preserves the semantic value-depth guard in graph coverage", () => { - const graph = graphFor( - "const first = 1; const second = first; const third = second;", - { maxValueDepth: 1 }, - ); +it("reports complete graph coverage for deeply nested static values", () => { + const nesting = 40; + const nestedValue = + Array.from({ length: nesting }, () => "{ next: ").join("") + + '"value"' + + " }".repeat(nesting); + const graph = graphFor(`const nested = ${nestedValue};`); expect(graph.coverage).toMatchObject({ - status: "partial", - truncated: true, - omitted_nodes: null, - omitted_relations: null, - limits: expect.arrayContaining([ - { name: "semantic.maxValueDepth", value: 1, unit: "depth" }, - ]), + truncated: false, + omitted_nodes: 0, + omitted_relations: 0, + limits: [], }); - expect( - graph.coverage.families.find( - ({ family }) => family === "promise-ownership", - ), - ).toMatchObject({ status: "unknown", retained_relations: 0 }); }); it("projects bounded Promise ownership and unresolved sources", () => { @@ -259,10 +249,7 @@ it("keeps duplicate function fingerprints ambiguous", () => { expect(query.summary.total_seed_matches).toBe(2); }); -const graphFor = ( - source: string, - inputLimits: Partial = {}, -) => { +const graphFor = (source: string) => { const file: JavaScriptArtifactFile = { path: "app.js", container_sha256: SHA256, @@ -273,18 +260,13 @@ const graphFor = ( unpacked: false, text: { included: true, value: source }, }; - const semanticLimits = { - ...DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS, - ...inputLimits, - }; const analysis: JavaScriptArtifactAnalysis = { files: [ { file, javascript: null, semantic: { - ir: analyzeJavaScriptSemantics(source, inputLimits), - limits: semanticLimits, + ir: analyzeJavaScriptSemantics(source), }, }, ], diff --git a/src/application/JavaScriptSemanticGraphBuilder.ts b/src/application/JavaScriptSemanticGraphBuilder.ts index 41d744848b..365b2d279b 100644 --- a/src/application/JavaScriptSemanticGraphBuilder.ts +++ b/src/application/JavaScriptSemanticGraphBuilder.ts @@ -35,7 +35,6 @@ import { projectSemanticFrontiers, projectSemanticPromises, projectSemanticReturnValues, - semanticRecoveryLimits, type SemanticFlowProjectionContext, } from "./JavaScriptSemanticGraphFlowProjection.js"; import { @@ -90,10 +89,7 @@ export const buildJavaScriptSemanticGraph = ({ ); } if (state.roots.size === 0) addFallbackRoot(rootArtifactSha256, state); - const semanticTruncated = analysis.files.some( - ({ semantic }) => semantic?.ir.coverage.status === "truncated", - ); - const truncated = semanticTruncated; + const truncated = false; const unknowns = [...state.unknowns.values()]; return createJavaScriptSemanticGraph({ schema: "JavaScriptSemanticRelationGraph", @@ -109,7 +105,7 @@ export const buildJavaScriptSemanticGraph = ({ truncated, omitted_nodes: truncated ? null : 0, omitted_relations: truncated ? null : 0, - limits: [...semanticRecoveryLimits(analysis)], + limits: [], families: JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.map((family) => ({ family, status: semanticFamilyStatus(family, analysis, truncated), diff --git a/src/application/JavaScriptSemanticGraphFlowProjection.ts b/src/application/JavaScriptSemanticGraphFlowProjection.ts index 6a8c49c020..42ac6d6382 100644 --- a/src/application/JavaScriptSemanticGraphFlowProjection.ts +++ b/src/application/JavaScriptSemanticGraphFlowProjection.ts @@ -3,7 +3,6 @@ import type { JavaScriptSemanticGraphNode } from "../domain/javascriptSemanticGr import type { JavaScriptSemanticIr } from "../domain/javascriptSemanticIr.js"; import { sourceRangesEqual } from "../domain/javascriptStaticAnalysisHelpers.js"; import type { JavaScriptSourceRange } from "../domain/javascriptStaticAnalysisTypes.js"; -import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js"; import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; import { addSemanticGraphNode, @@ -280,22 +279,3 @@ export const projectSemanticFrontiers = ( addSemanticGraphUnknown(context.state, unknown); } }; - -/** Publish the semantic recursion guard when it caused incomplete recovery. */ -export const semanticRecoveryLimits = ( - analysis: JavaScriptArtifactAnalysis, -): { - readonly name: string; - readonly value: number; - readonly unit: "depth"; -}[] => { - const limits = new Map(); - for (const { semantic } of analysis.files) { - if (semantic === null) continue; - for (const name of semantic.ir.coverage.limitsReached) - limits.set(`semantic.${name}`, semantic.limits[name]); - } - return [...limits] - .sort(([left], [right]) => left.localeCompare(right)) - .map(([name, value]) => ({ name, value, unit: "depth" })); -}; diff --git a/src/application/JavaScriptSemanticGraphResources.test.ts b/src/application/JavaScriptSemanticGraphResources.test.ts index 2b832a4262..d099a5e849 100644 --- a/src/application/JavaScriptSemanticGraphResources.test.ts +++ b/src/application/JavaScriptSemanticGraphResources.test.ts @@ -5,10 +5,6 @@ import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTyp import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; import { queryJavaScriptSemanticGraph } from "../domain/javascriptSemanticQuery.js"; import { analyzeJavaScriptSemantics } from "../domain/javascriptSemanticAnalysis.js"; -import { - DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS, - type JavaScriptSemanticLimits, -} from "../domain/javascriptSemanticIr.js"; const SHA256 = "a".repeat(64); const GRAPH_ID = `jag_${"b".repeat(64)}`; @@ -270,10 +266,7 @@ const emptyAnalysis = (): JavaScriptArtifactAnalysis => ({ limitations: [], }); -const graphFor = ( - source: string, - inputLimits: Partial = {}, -) => { +const graphFor = (source: string) => { const file: JavaScriptArtifactFile = { path: "app.js", container_sha256: SHA256, @@ -284,18 +277,13 @@ const graphFor = ( unpacked: false, text: { included: true, value: source }, }; - const semanticLimits = { - ...DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS, - ...inputLimits, - }; const analysis: JavaScriptArtifactAnalysis = { files: [ { file, javascript: null, semantic: { - ir: analyzeJavaScriptSemantics(source, inputLimits), - limits: semanticLimits, + ir: analyzeJavaScriptSemantics(source), }, }, ], diff --git a/src/domain/javascriptApplicationVersionKeys.ts b/src/domain/javascriptApplicationVersionKeys.ts index c90de9ec64..76512dda77 100644 --- a/src/domain/javascriptApplicationVersionKeys.ts +++ b/src/domain/javascriptApplicationVersionKeys.ts @@ -205,8 +205,7 @@ const structuralFingerprintKey = (node: ApplicationNode): string | null => { node, "structural_fingerprint_algorithm", ); - const status = uniqueStringProperty(node, "structural_fingerprint_status"); - return digest === null || algorithm === null || status !== "complete" + return digest === null || algorithm === null ? null : `structural\0${node.kind}\0${algorithm}\0${digest}`; }; diff --git a/src/domain/javascriptAstFingerprint.test.ts b/src/domain/javascriptAstFingerprint.test.ts index 67f4d0aeed..94eafeb82b 100644 --- a/src/domain/javascriptAstFingerprint.test.ts +++ b/src/domain/javascriptAstFingerprint.test.ts @@ -9,8 +9,8 @@ describe("JavaScript AST fingerprints", () => { const left = parse(`const value = ${JSON.stringify(`${prefix}left`)};`); const right = parse(`const value = ${JSON.stringify(`${prefix}right`)};`); - expect(fingerprintJavaScriptAst(left, 100).sha256).not.toBe( - fingerprintJavaScriptAst(right, 100).sha256, + expect(fingerprintJavaScriptAst(left)).not.toBe( + fingerprintJavaScriptAst(right), ); }); }); diff --git a/src/domain/javascriptAstFingerprint.ts b/src/domain/javascriptAstFingerprint.ts index 09bfce65c6..1b1ff6e97a 100644 --- a/src/domain/javascriptAstFingerprint.ts +++ b/src/domain/javascriptAstFingerprint.ts @@ -2,41 +2,19 @@ import { createHash } from "node:crypto"; import * as t from "@babel/types"; -/** Complete or prefix-only digest from a bounded AST traversal. */ -export interface AstFingerprint { - readonly sha256: string; - readonly truncated: boolean; -} - /** Static CommonJS export names. */ export interface StaticExports { readonly values: readonly string[]; } -/** Derive a rename-resistant, bounded syntax fingerprint without code execution. */ -export const fingerprintJavaScriptAst = ( - node: t.Node, - maximumTokens: number, -): AstFingerprint => { +/** Derive a rename-resistant syntax fingerprint without code execution. */ +export const fingerprintJavaScriptAst = (node: t.Node): string => { const tokens: string[] = []; - let truncated = false; t.traverseFast(node, (current) => { - if (tokens.length >= maximumTokens) { - truncated = true; - return t.traverseFast.stop; - } tokens.push(...semanticTokens(current)); - if (tokens.length > maximumTokens) { - tokens.length = maximumTokens; - truncated = true; - return t.traverseFast.stop; - } return undefined; }); - return { - sha256: createHash("sha256").update(JSON.stringify(tokens)).digest("hex"), - truncated, - }; + return createHash("sha256").update(JSON.stringify(tokens)).digest("hex"); }; /** Collect statically declared CommonJS/bundler export names from one factory. */ @@ -130,16 +108,13 @@ const addExport = (output: Set, name: string): void => { output.add(name); }; -const memberPath = (node: t.Node | null | undefined, depth = 0): string => { - if (depth >= 128) return "[deep]"; +const memberPath = (node: t.Node | null | undefined): string => { if (node === undefined || node === null) return ""; if (t.isIdentifier(node) || t.isPrivateName(node)) return t.isIdentifier(node) ? node.name : ""; if (t.isThisExpression(node)) return "this"; if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) { - const object = t.isNode(node.object) - ? memberPath(node.object, depth + 1) - : ""; + const object = t.isNode(node.object) ? memberPath(node.object) : ""; const property = propertyName(node.property); return object === "" ? property : `${object}.${property}`; } diff --git a/src/domain/javascriptExportShapeComparisonSchemas.ts b/src/domain/javascriptExportShapeComparisonSchemas.ts index 3edda08c78..295f021d3c 100644 --- a/src/domain/javascriptExportShapeComparisonSchemas.ts +++ b/src/domain/javascriptExportShapeComparisonSchemas.ts @@ -123,7 +123,6 @@ const projectedReturnShapeSchema = z "unknown", "ambiguous", "cycle", - "limit-reached", ]), fields: z.array(projectedReturnFieldSchema), property_coverage: z.array(projectedPropertyCoverageSchema), diff --git a/src/domain/javascriptSemanticAnalysis.dataflow.test.ts b/src/domain/javascriptSemanticAnalysis.dataflow.test.ts index b4412e057a..03b0274cc5 100644 --- a/src/domain/javascriptSemanticAnalysis.dataflow.test.ts +++ b/src/domain/javascriptSemanticAnalysis.dataflow.test.ts @@ -95,7 +95,6 @@ describe("JavaScript semantic analysis: dataflow 1", () => { status: "complete", retainedCount: 2, omittedCount: 0, - limitsReached: [], }); expect(parse.returnSites).toHaveLength(2); expect(parse.returnSites[0]?.value).toMatchObject({ diff --git a/src/domain/javascriptSemanticAnalysis.rejection.test.ts b/src/domain/javascriptSemanticAnalysis.rejection.test.ts index e7baa098bd..5f6c05f3a1 100644 --- a/src/domain/javascriptSemanticAnalysis.rejection.test.ts +++ b/src/domain/javascriptSemanticAnalysis.rejection.test.ts @@ -2,13 +2,14 @@ import { fc, it } from "@fast-check/vitest"; import { describe, expect } from "vitest"; import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; +import type { JavaScriptSemanticValue } from "./javascriptSemanticIr.js"; import { onlyCallable, topLevelBinding, } from "./javascriptSemanticAnalysis.fixture.js"; describe("JavaScript semantic analysis: rejection 1", () => { - it("retains complete finite facts and reports the recursive depth guard", () => { + it("retains complete finite facts and deeply nested static values", () => { const names = Array.from( { length: 280 }, (_, index) => `key${String(index)}`, @@ -26,14 +27,22 @@ describe("JavaScript semantic analysis: rejection 1", () => { ]), }); - const depth = analyzeJavaScriptSemantics( - 'const first = "value"; const second = first;', - { maxValueDepth: 1 }, - ); - expect(topLevelBinding(depth, "second").value).toMatchObject({ - status: "limit-reached", - }); - expect(depth.coverage.limitsReached).toContain("maxValueDepth"); + const nesting = 40; + const nestedValue = + Array.from({ length: nesting }, () => "{ next: ").join("") + + '"value"' + + " }".repeat(nesting); + const deep = analyzeJavaScriptSemantics(`const nested = ${nestedValue};`); + let value: JavaScriptSemanticValue = topLevelBinding(deep, "nested").value; + for (let depth = 0; depth < nesting; depth += 1) { + expect(value.status).toBe("object"); + if (value.status !== "object") + throw new TypeError("Expected a recovered nested object"); + const next = value.properties.find(({ name }) => name === "next"); + if (next === undefined) throw new TypeError("Missing nested property"); + value = next.value; + } + expect(value).toEqual({ status: "literal", value: "value" }); }); it("is deterministic and returns failed coverage for an unparseable source", () => { diff --git a/src/domain/javascriptSemanticAnalysis.structure.test.ts b/src/domain/javascriptSemanticAnalysis.structure.test.ts index 894756f925..65f087529a 100644 --- a/src/domain/javascriptSemanticAnalysis.structure.test.ts +++ b/src/domain/javascriptSemanticAnalysis.structure.test.ts @@ -34,7 +34,6 @@ describe("JavaScript semantic analysis: structure 1", () => { expect(ir.coverage).toEqual({ status: "complete", omittedCount: 0, - limitsReached: [], }); expect(origin(topLevelBinding(ir, "ir"))).toEqual({ specifier: "electron", diff --git a/src/domain/javascriptSemanticAnalysis.ts b/src/domain/javascriptSemanticAnalysis.ts index 91859c392e..7514392823 100644 --- a/src/domain/javascriptSemanticAnalysis.ts +++ b/src/domain/javascriptSemanticAnalysis.ts @@ -1,12 +1,10 @@ import * as t from "@babel/types"; import { - DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS, failedJavaScriptSemanticIr, type JavaScriptModuleOrigin, type JavaScriptSemanticDefinition, type JavaScriptSemanticIr, - type JavaScriptSemanticLimits, } from "./javascriptSemanticIr.js"; import { collectSemanticModuleLink, @@ -63,24 +61,21 @@ interface AddBindingInput { readonly projection?: readonly (string | number | null)[]; } -/** Recover bounded lexical bindings, aliases, constants, and module links. */ +/** Recover lexical bindings, aliases, constants, and module links. */ export const analyzeJavaScriptSemantics = ( source: string, - inputLimits: Partial = {}, ): JavaScriptSemanticIr => { const file = parseJavaScriptSource(source); return file === null ? failedJavaScriptSemanticIr() - : analyzeParsedJavaScriptSemantics(file, inputLimits); + : analyzeParsedJavaScriptSemantics(file); }; /** Recover semantics from an already parsed JavaScript artifact. */ export const analyzeParsedJavaScriptSemantics = ( file: ParsedJavaScriptSource, - inputLimits: Partial = {}, ): JavaScriptSemanticIr => { - const limits = { ...DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS, ...inputLimits }; - const state = createState(file.program, limits); + const state = createState(file.program); collectDefinitions(file.program, state); const references = collectSemanticReferences(file.program, state); const parserPartial = file.errors.length > 0; @@ -101,16 +96,13 @@ export const analyzeParsedJavaScriptSemantics = ( references, moduleLinks, ...derived, - coverage: semanticCoverage(state, parserPartial), + coverage: semanticCoverage(parserPartial), limitations: [ ...(parserPartial ? [ "The parser recovered from syntax errors; affected bindings are partial.", ] : []), - ...(state.limitsReached.size === 0 - ? [] - : ["Semantic recovery stopped retaining facts at explicit limits."]), "Values and aliases were recovered from inert syntax only; no JavaScript was executed.", "Return sites include only direct callable returns; nested callable returns remain separate.", "Local call, argument, return, and closure relations are static candidates and do not prove runtime invocation.", @@ -120,10 +112,7 @@ export const analyzeParsedJavaScriptSemantics = ( }; }; -const createState = ( - program: t.Program, - limits: JavaScriptSemanticLimits, -): JavaScriptSemanticAnalysisState => { +const createState = (program: t.Program): JavaScriptSemanticAnalysisState => { const root: JavaScriptSemanticScopeState = { scopeId: semanticScopeId("program", program), parentScopeId: null, @@ -133,7 +122,6 @@ const createState = ( bindings: new Map(), }; return { - limits, scopes: [root], scopesById: new Map([[root.scopeId, root]]), scopeByNode: new WeakMap([[program, root]]), @@ -141,8 +129,6 @@ const createState = ( callables: [], callableNodesById: new Map(), moduleLinks: [], - limitsReached: new Set(), - omittedCount: 0, }; }; @@ -431,10 +417,7 @@ const addBinding = (input: AddBindingInput): void => { } = input; let binding = scope.bindings.get(name); if (binding === undefined) { - if (!scope.bindingsComplete) { - state.omittedCount += 1; - return; - } + if (!scope.bindingsComplete) return; binding = createBinding(scope, name, kind, mutable); scope.bindings.set(name, binding); state.bindingsById.set(binding.bindingId, binding); diff --git a/src/domain/javascriptSemanticCallResolution.ts b/src/domain/javascriptSemanticCallResolution.ts index 62873dc220..021839bca3 100644 --- a/src/domain/javascriptSemanticCallResolution.ts +++ b/src/domain/javascriptSemanticCallResolution.ts @@ -4,10 +4,7 @@ import type { JavaScriptSemanticArgumentFlow, JavaScriptSemanticCallable, } from "./javascriptSemanticIr.js"; -import { - reachSemanticLimit, - semanticCallableIdForNode, -} from "./javascriptSemanticProjection.js"; +import { semanticCallableIdForNode } from "./javascriptSemanticProjection.js"; import { resolveSemanticBindingState, type JavaScriptSemanticAnalysisState, @@ -26,7 +23,6 @@ interface CallResolutionContext { readonly state: JavaScriptSemanticAnalysisState; readonly callableById: ReadonlyMap; readonly seenBindings: ReadonlySet; - readonly depth: number; readonly bindingCache: Map; } @@ -36,11 +32,10 @@ export interface ResolveLocalCallablesInput { readonly state: JavaScriptSemanticAnalysisState; readonly callableById: ReadonlyMap; readonly seenBindings: ReadonlySet; - readonly depth: number; readonly bindingCache: Map; } -/** Resolve bounded local callable candidates without following dynamic properties. */ +/** Resolve local callable candidates without following dynamic properties. */ export const resolveLocalCallables = ( input: ResolveLocalCallablesInput, ): LocalCallableResolution => @@ -48,7 +43,6 @@ export const resolveLocalCallables = ( state: input.state, callableById: input.callableById, seenBindings: input.seenBindings, - depth: input.depth, bindingCache: input.bindingCache, }); @@ -56,14 +50,6 @@ const resolveCallables = ( node: t.Node, context: CallResolutionContext, ): LocalCallableResolution => { - if (context.depth >= context.state.limits.maxValueDepth) { - reachSemanticLimit(context.state, "maxValueDepth"); - return { - callableIds: [], - complete: false, - reason: "Call target depth limit reached.", - }; - } const direct = semanticCallableIdForNode(node); if (direct !== null && context.callableById.has(direct)) return { @@ -77,7 +63,7 @@ const resolveCallables = ( t.isTSTypeAssertion(node) || t.isTSNonNullExpression(node) ) - return resolveCallables(node.expression, nestedContext(context)); + return resolveCallables(node.expression, context); if (t.isConditionalExpression(node) || t.isLogicalExpression(node)) return resolveAlternatives( t.isConditionalExpression(node) ? node.consequent : node.left, @@ -102,7 +88,7 @@ const resolveIdentifier = ( complete: false, reason: `Unresolved call target ${node.name}.`, } - : resolveBindingCallables(binding, nestedContext(context)); + : resolveBindingCallables(binding, context); }; const resolveAlternatives = ( @@ -111,7 +97,7 @@ const resolveAlternatives = ( context: CallResolutionContext, ): LocalCallableResolution => { const candidates = [left, right].map((candidate) => - resolveCallables(candidate, nestedContext(context)), + resolveCallables(candidate, context), ); return { callableIds: uniqueCallableIds( @@ -137,7 +123,7 @@ const resolveBindingCallables = ( if (binding.initializers.length === 0) return missingBindingInitializer(binding); const candidateContext = { - ...nestedContext(context), + ...context, seenBindings: new Set([...context.seenBindings, binding.bindingId]), }; const candidates = binding.initializers.map(({ node, projection }) => @@ -221,9 +207,5 @@ const rangeContains = ( (outer.end.line === inner.end.line && outer.end.column >= inner.end.column)); -const nestedContext = ( - context: CallResolutionContext, -): CallResolutionContext => ({ ...context, depth: context.depth + 1 }); - const uniqueCallableIds = (values: readonly string[]): string[] => [...new Set(values)].sort(compareCodePoints); diff --git a/src/domain/javascriptSemanticCalls.ts b/src/domain/javascriptSemanticCalls.ts index b4a1be0a8b..71a1dedbbb 100644 --- a/src/domain/javascriptSemanticCalls.ts +++ b/src/domain/javascriptSemanticCalls.ts @@ -129,7 +129,6 @@ const collectCallSite = ( state, callableById, seenBindings: new Set(), - depth: 0, bindingCache: context.bindingResolutionCache, }); const callSiteId = semanticCallSiteId(node); diff --git a/src/domain/javascriptSemanticCoverage.ts b/src/domain/javascriptSemanticCoverage.ts index 78be8369d6..e987a64eb5 100644 --- a/src/domain/javascriptSemanticCoverage.ts +++ b/src/domain/javascriptSemanticCoverage.ts @@ -1,58 +1,29 @@ -import type { JavaScriptSemanticLimits } from "./javascriptSemanticIr.js"; -import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState.js"; -import { compareCodePoints } from "./javascriptStaticAnalysisHelpers.js"; - /** Exact retention state for one callable's direct returns. */ export type JavaScriptSemanticReturnCoverage = { readonly retainedCount: number; } & { readonly status: "complete" | "partial"; readonly omittedCount: 0 | null; - readonly limitsReached: readonly []; }; -/** Coverage state for one bounded semantic recovery pass. */ +/** Coverage state for one semantic recovery pass. */ export type JavaScriptSemanticCoverage = | { readonly status: "complete" | "partial"; readonly omittedCount: 0; - readonly limitsReached: readonly []; - } - | { - readonly status: "truncated"; - readonly omittedCount: number; - readonly limitsReached: readonly [ - keyof JavaScriptSemanticLimits, - ...(keyof JavaScriptSemanticLimits)[], - ]; } | { readonly status: "failed"; readonly omittedCount: null; - readonly limitsReached: readonly []; }; -/** Classify whole-file semantic coverage from parser and limit state. */ +/** Classify whole-file semantic coverage from parser recovery. */ export const semanticCoverage = ( - state: JavaScriptSemanticAnalysisState, parserPartial: boolean, -): JavaScriptSemanticCoverage => { - if (state.limitsReached.size === 0) - return { - status: parserPartial ? "partial" : "complete", - omittedCount: 0, - limitsReached: [], - }; - const limits = [...state.limitsReached].sort(compareCodePoints); - const first = limits[0]; - if (first === undefined) - throw new TypeError("Semantic truncation lost its limiting dimension"); - return { - status: "truncated", - omittedCount: state.omittedCount, - limitsReached: [first, ...limits.slice(1)], - }; -}; +): JavaScriptSemanticCoverage => ({ + status: parserPartial ? "partial" : "complete", + omittedCount: 0, +}); /** Classify one callable's direct-return coverage. */ export const semanticReturnCoverage = ( @@ -62,5 +33,4 @@ export const semanticReturnCoverage = ( status: parserPartial ? "partial" : "complete", retainedCount, omittedCount: 0, - limitsReached: [], }); diff --git a/src/domain/javascriptSemanticFingerprints.ts b/src/domain/javascriptSemanticFingerprints.ts index b96190fa47..dd645be8a3 100644 --- a/src/domain/javascriptSemanticFingerprints.ts +++ b/src/domain/javascriptSemanticFingerprints.ts @@ -78,9 +78,7 @@ export const collectJavaScriptSemanticFingerprints = ( { callableId: callable.callableId, status: - parserPartial || - state.limitsReached.size > 0 || - callable.returnCoverage.status !== "complete" + parserPartial || callable.returnCoverage.status !== "complete" ? "partial" : "complete", components: { @@ -107,7 +105,7 @@ export const collectJavaScriptSemanticFingerprints = ( }, limitations: [ "The fingerprint is a bounded static candidate and does not prove behavioral equivalence.", - ...(parserPartial || state.limitsReached.size > 0 + ...(parserPartial ? ["Incomplete semantic recovery makes this fingerprint partial."] : []), ], diff --git a/src/domain/javascriptSemanticIr.ts b/src/domain/javascriptSemanticIr.ts index 7a21ce7ab8..84bbb454f5 100644 --- a/src/domain/javascriptSemanticIr.ts +++ b/src/domain/javascriptSemanticIr.ts @@ -11,16 +11,6 @@ export type { JavaScriptSemanticValue, } from "./javascriptSemanticValueTypes.js"; -/** Hard bounds for one execution-free JavaScript semantic analysis. */ -export interface JavaScriptSemanticLimits { - readonly maxValueDepth: number; -} - -/** Stack guard for recursive value and provenance recovery. */ -export const DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS: JavaScriptSemanticLimits = { - maxValueDepth: 16, -}; - /** One exact module origin followed through imports, requires, or aliases. */ export interface JavaScriptModuleOrigin { readonly specifier: string; @@ -45,7 +35,7 @@ export type JavaScriptBindingProvenance = readonly reason: string; } | { - readonly status: "unknown" | "cycle" | "limit-reached"; + readonly status: "unknown" | "cycle"; readonly origins: readonly []; readonly reason: string; }; @@ -73,7 +63,7 @@ export interface JavaScriptSemanticDefinition { readonly location: JavaScriptSourceRange; } -/** One resolved lexical binding plus bounded value and provenance. */ +/** One resolved lexical binding plus static value and provenance. */ export interface JavaScriptSemanticBinding { readonly bindingId: string; readonly scopeId: string; @@ -473,7 +463,7 @@ export const failedJavaScriptSemanticIr = (): JavaScriptSemanticIr => ({ objectOperations: [], functionFingerprints: [], frontiers: [], - coverage: { status: "failed", omittedCount: null, limitsReached: [] }, + coverage: { status: "failed", omittedCount: null }, limitations: [ "JavaScript parsing failed; no semantic absence claim is available.", "No JavaScript was executed.", diff --git a/src/domain/javascriptSemanticProjection.ts b/src/domain/javascriptSemanticProjection.ts index aa7533c8b9..3fb5502f66 100644 --- a/src/domain/javascriptSemanticProjection.ts +++ b/src/domain/javascriptSemanticProjection.ts @@ -55,7 +55,6 @@ export const collectSemanticCallable = (input: CollectCallableInput): void => { status: "partial", retainedCount: 0, omittedCount: null, - limitsReached: [], }, }); state.callableNodesById.set(callableId, node); @@ -192,15 +191,6 @@ export const semanticStaticPropertyName = ( ? "" : propertyName(property); -/** Record one exact limit hit without inventing the total omitted population. */ -export const reachSemanticLimit = ( - state: JavaScriptSemanticAnalysisState, - limit: keyof JavaScriptSemanticAnalysisState["limits"], -): void => { - state.limitsReached.add(limit); - state.omittedCount += 1; -}; - const callableKind = ( node: t.Node, ): "function" | "class" | "method" | undefined => { diff --git a/src/domain/javascriptSemanticPromises.ts b/src/domain/javascriptSemanticPromises.ts index 1d1f93c231..98e022f5fb 100644 --- a/src/domain/javascriptSemanticPromises.ts +++ b/src/domain/javascriptSemanticPromises.ts @@ -48,7 +48,6 @@ interface PromiseExpressionResolutionContext { readonly candidateByNode: WeakMap; readonly state: JavaScriptSemanticAnalysisState; readonly seenBindings: ReadonlySet; - readonly depth: number; } interface PromiseOwnershipContext { @@ -388,8 +387,7 @@ const resolvePromiseExpression = ( rawNode: t.Node, context: PromiseExpressionResolutionContext, ): PromiseResolution => { - const { current, candidateByNode, state, seenBindings, depth } = context; - if (depth >= state.limits.maxValueDepth) return unresolvedPromise(); + const { current, candidateByNode, state, seenBindings } = context; const node = unwrapExpression(rawNode); const direct = candidateByNode.get(node); if (direct !== undefined && direct.promiseId !== current.promiseId) @@ -411,7 +409,6 @@ const resolvePromiseExpression = ( return resolvePromiseExpression(initializer.node, { ...context, seenBindings: nestedSeen, - depth: depth + 1, }); }; @@ -424,7 +421,6 @@ const promiseResolutionContext = ( candidateByNode, state, seenBindings: new Set(), - depth: 0, }); const unwrapExpression = (node: t.Node): t.Node => { diff --git a/src/domain/javascriptSemanticProvenance.ts b/src/domain/javascriptSemanticProvenance.ts index 1f92f8430d..1559d7c41c 100644 --- a/src/domain/javascriptSemanticProvenance.ts +++ b/src/domain/javascriptSemanticProvenance.ts @@ -3,7 +3,6 @@ import type { JavaScriptModuleOrigin, JavaScriptSemanticPrimitive, } from "./javascriptSemanticIr.js"; -import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState.js"; import { compareCodePoints } from "./javascriptStaticAnalysisHelpers.js"; /** Construct exact local provenance without inventing module origins. */ @@ -15,7 +14,7 @@ export const semanticLocalProvenance = (): JavaScriptBindingProvenance => ({ /** Construct an unresolved provenance outcome with an actionable reason. */ export const semanticUnresolvedProvenance = ( - status: "unknown" | "cycle" | "limit-reached", + status: "unknown" | "cycle", reason: string, ): JavaScriptBindingProvenance => ({ status, origins: [], reason }); @@ -48,24 +47,6 @@ export const uniqueSemanticOrigins = ( compareCodePoints(semanticOriginKey(left), semanticOriginKey(right)), ); -/** Mark and return an explicit provenance-limit value. */ -export const semanticLimitProvenance = ( - state: JavaScriptSemanticAnalysisState, - limit: "maxValueDepth", -): JavaScriptBindingProvenance => { - reachSemanticValueLimit(state, limit); - return semanticUnresolvedProvenance("limit-reached", `${limit} reached.`); -}; - -/** Record one value-lattice limit against the shared semantic analysis state. */ -export const reachSemanticValueLimit = ( - state: JavaScriptSemanticAnalysisState, - limit: keyof JavaScriptSemanticAnalysisState["limits"], -): void => { - state.limitsReached.add(limit); - state.omittedCount += 1; -}; - const semanticOriginKey = (origin: JavaScriptModuleOrigin): string => `${origin.specifier}\0${origin.importedPath.join("\0")}`; diff --git a/src/domain/javascriptSemanticReturns.ts b/src/domain/javascriptSemanticReturns.ts index 4f9688e877..d4fc064442 100644 --- a/src/domain/javascriptSemanticReturns.ts +++ b/src/domain/javascriptSemanticReturns.ts @@ -42,7 +42,6 @@ export const collectSemanticReturns = ( status: "partial" as const, retainedCount: 0, omittedCount: null, - limitsReached: [], }, }; const expressions = directReturnExpressions(node); diff --git a/src/domain/javascriptSemanticState.ts b/src/domain/javascriptSemanticState.ts index ee0a73e55c..cc5466aa77 100644 --- a/src/domain/javascriptSemanticState.ts +++ b/src/domain/javascriptSemanticState.ts @@ -3,7 +3,6 @@ import * as t from "@babel/types"; import type { JavaScriptModuleOrigin, JavaScriptSemanticDefinition, - JavaScriptSemanticLimits, JavaScriptSemanticModuleLink, JavaScriptSemanticCallable, JavaScriptSemanticScope, @@ -37,9 +36,8 @@ export interface JavaScriptSemanticScopeState { readonly bindings: Map; } -/** Shared bounded state for semantic collection and evaluation. */ +/** Shared state for semantic collection and evaluation. */ export interface JavaScriptSemanticAnalysisState { - readonly limits: JavaScriptSemanticLimits; readonly scopes: JavaScriptSemanticScopeState[]; readonly scopesById: Map; readonly scopeByNode: WeakMap; @@ -47,8 +45,6 @@ export interface JavaScriptSemanticAnalysisState { readonly callables: JavaScriptSemanticCallable[]; readonly callableNodesById: Map; readonly moduleLinks: JavaScriptSemanticModuleLink[]; - readonly limitsReached: Set; - omittedCount: number; } /** Return the active scope from a non-empty construction stack. */ diff --git a/src/domain/javascriptSemanticValueTypes.ts b/src/domain/javascriptSemanticValueTypes.ts index a344c83ba0..99ec2f6fd1 100644 --- a/src/domain/javascriptSemanticValueTypes.ts +++ b/src/domain/javascriptSemanticValueTypes.ts @@ -1,4 +1,4 @@ -/** Primitive values admitted into the bounded constant lattice. */ +/** Primitive values admitted into the constant lattice. */ export type JavaScriptSemanticPrimitive = string | number | boolean | null; type JavaScriptSemanticObjectValue = { @@ -29,7 +29,7 @@ type JavaScriptSemanticArrayValue = { } ); -/** Bounded, execution-free value lattice for JavaScript expressions. */ +/** Execution-free value lattice for JavaScript expressions. */ export type JavaScriptSemanticValue = | { readonly status: "literal"; @@ -42,7 +42,7 @@ export type JavaScriptSemanticValue = | JavaScriptSemanticObjectValue | JavaScriptSemanticArrayValue | { - readonly status: "unknown" | "ambiguous" | "cycle" | "limit-reached"; + readonly status: "unknown" | "ambiguous" | "cycle"; readonly reason: string; }; diff --git a/src/domain/javascriptSemanticValues.ts b/src/domain/javascriptSemanticValues.ts index bbc53d1cea..d933efe532 100644 --- a/src/domain/javascriptSemanticValues.ts +++ b/src/domain/javascriptSemanticValues.ts @@ -17,9 +17,7 @@ import { stringValue, } from "./javascriptStaticAnalysisHelpers.js"; import { - reachSemanticValueLimit, semanticAmbiguousProvenance, - semanticLimitProvenance, semanticLocalProvenance, semanticOriginsProvenance, semanticUnresolvedProvenance, @@ -34,7 +32,6 @@ import { interface EvaluationContext { readonly state: JavaScriptSemanticAnalysisState; readonly bindings: ReadonlySet; - readonly depth: number; } /** Evaluate one binding in the bounded constant-value lattice. */ @@ -42,21 +39,21 @@ export const evaluateSemanticBinding = ( binding: JavaScriptSemanticBindingState, state: JavaScriptSemanticAnalysisState, ): JavaScriptSemanticValue => - evaluateBinding(binding, { state, bindings: new Set(), depth: 0 }); + evaluateBinding(binding, { state, bindings: new Set() }); /** Evaluate one arbitrary inert expression in the established lexical state. */ export const evaluateSemanticExpression = ( node: t.Node, state: JavaScriptSemanticAnalysisState, ): JavaScriptSemanticValue => - evaluateExpression(node, { state, bindings: new Set(), depth: 0 }); + evaluateExpression(node, { state, bindings: new Set() }); /** Follow module provenance through destructuring, members, and aliases. */ export const evaluateSemanticProvenance = ( binding: JavaScriptSemanticBindingState, state: JavaScriptSemanticAnalysisState, ): JavaScriptBindingProvenance => - provenanceForBinding(binding, { state, bindings: new Set(), depth: 0 }); + provenanceForBinding(binding, { state, bindings: new Set() }); const evaluateBinding = ( binding: JavaScriptSemanticBindingState, @@ -64,8 +61,6 @@ const evaluateBinding = ( ): JavaScriptSemanticValue => { if (context.bindings.has(binding.bindingId)) return { status: "cycle", reason: `Alias cycle at ${binding.name}.` }; - if (context.depth >= context.state.limits.maxValueDepth) - return limitValue(context.state, "maxValueDepth"); if (binding.initializers.length === 0) return { status: "unknown", @@ -90,8 +85,6 @@ const evaluateExpression = ( node: t.Node, context: EvaluationContext, ): JavaScriptSemanticValue => { - if (context.depth >= context.state.limits.maxValueDepth) - return limitValue(context.state, "maxValueDepth"); const literal = primitiveValue(node); if (literal.found) return { status: "literal", value: literal.value }; if (t.isIdentifier(node)) { @@ -323,8 +316,6 @@ const provenanceForBinding = ( "cycle", `Alias cycle at ${binding.name}.`, ); - if (context.depth >= context.state.limits.maxValueDepth) - return semanticLimitProvenance(context.state, "maxValueDepth"); if (binding.directOrigins.length > 0) return semanticOriginsProvenance(binding.directOrigins); if (binding.initializers.length === 0) return semanticLocalProvenance(); @@ -460,20 +451,11 @@ const memberKey = ( return undefined; }; -const limitValue = ( - state: JavaScriptSemanticAnalysisState, - limit: "maxValueDepth", -): JavaScriptSemanticValue => { - reachSemanticValueLimit(state, limit); - return { status: "limit-reached", reason: `${limit} reached.` }; -}; - const nestedContext = ( context: EvaluationContext, bindingId?: string, ): EvaluationContext => ({ state: context.state, - depth: context.depth + 1, bindings: bindingId === undefined ? context.bindings diff --git a/src/domain/javascriptStaticAnalysis.no_finding_caps.test.ts b/src/domain/javascriptStaticAnalysis.no_finding_caps.test.ts index 7237bda236..b32db4c766 100644 --- a/src/domain/javascriptStaticAnalysis.no_finding_caps.test.ts +++ b/src/domain/javascriptStaticAnalysis.no_finding_caps.test.ts @@ -4,7 +4,7 @@ import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; describe("JavaScript static analysis findings", () => { it("retains every static reference and Electron property in large literals", () => { - const names = Array.from({ length: 300 }, (_, index) => `key${index}`); + const names = Array.from({ length: 10_001 }, (_, index) => `key${index}`); const source = [ ...names.map( (name, index) => `import value${index} from "./${name}.js";`, @@ -13,20 +13,18 @@ describe("JavaScript static analysis findings", () => { `contextBridge.exposeInMainWorld("api", { ${names.map((name) => `${name}: () => true`).join(", ")} });`, `const { ${names.join(", ")} } = require("./addon.node");`, ].join("\n"); - const analysis = analyzeJavaScriptStaticSource(source, { - maxAstNodes: 100_000, - deadline: Number.POSITIVE_INFINITY, - now: () => 0, - }); + const analysis = analyzeJavaScriptStaticSource(source); expect(analysis.parse_status).toBe("complete"); - expect(analysis.references).toHaveLength(301); + expect(analysis.references).toHaveLength(10_002); expect(analysis.electron.browser_windows[0]?.web_preferences).toHaveLength( - 300, + 10_001, + ); + expect(analysis.electron.context_bridge_apis[0]?.members).toHaveLength( + 10_001, ); - expect(analysis.electron.context_bridge_apis[0]?.members).toHaveLength(300); expect(analysis.electron.native_addon_bindings[0]?.members).toHaveLength( - 300, + 10_001, ); }); }); diff --git a/src/domain/javascriptStaticAnalysis.ts b/src/domain/javascriptStaticAnalysis.ts index 9e925dfad4..945470c3f4 100644 --- a/src/domain/javascriptStaticAnalysis.ts +++ b/src/domain/javascriptStaticAnalysis.ts @@ -24,66 +24,49 @@ import { createJavaScriptAnalysisAccumulator, type JavaScriptAnalysisAccumulator as AnalysisAccumulator, } from "./javascriptStaticAnalysisState.js"; -import type { - JavaScriptStaticAnalysis, - JavaScriptStaticAnalysisLimits, -} from "./javascriptStaticAnalysisTypes.js"; +import type { JavaScriptStaticAnalysis } from "./javascriptStaticAnalysisTypes.js"; import { parseJavaScriptSource, type ParsedJavaScriptSource, } from "./javascriptSourceParser.js"; -/** Parse one bounded JavaScript artifact and recover static structure only. */ +/** Parse one JavaScript artifact and recover static structure only. */ export const analyzeJavaScriptStaticSource = ( source: string, - limits: JavaScriptStaticAnalysisLimits, ): JavaScriptStaticAnalysis => { const file = parseJavaScriptSource(source); return file === null ? failedJavaScriptStaticAnalysis() - : analyzeParsedJavaScriptStaticSource(source, file, limits); + : analyzeParsedJavaScriptStaticSource(source, file); }; /** Recover static structure from an already parsed JavaScript artifact. */ export const analyzeParsedJavaScriptStaticSource = ( source: string, file: ParsedJavaScriptSource, - limits: JavaScriptStaticAnalysisLimits, ): JavaScriptStaticAnalysis => { const accumulator = createJavaScriptAnalysisAccumulator(); - traverseStaticSource(source, file, accumulator, limits); + traverseStaticSource(source, file, accumulator); addSourceMapDirectives(source, accumulator); - return finalizeStaticAnalysis(source, file, accumulator, limits); + return finalizeStaticAnalysis(source, file, accumulator); }; const traverseStaticSource = ( source: string, file: ParsedJavaScriptSource, accumulator: AnalysisAccumulator, - limits: JavaScriptStaticAnalysisLimits, ): void => { - if (limits.now() > limits.deadline) accumulator.truncated = true; - if (!accumulator.truncated) - t.traverseFast(file, (node) => { - accumulator.visitedNodes += 1; - if ( - accumulator.visitedNodes > limits.maxAstNodes || - (accumulator.visitedNodes % 1_024 === 0 && - limits.now() > limits.deadline) - ) { - accumulator.truncated = true; - return t.traverseFast.stop; - } - inspectNode(source, node, accumulator, limits); - return undefined; - }); + t.traverseFast(file, (node) => { + accumulator.visitedNodes += 1; + inspectNode(source, node, accumulator); + return undefined; + }); }; const finalizeStaticAnalysis = ( source: string, file: ParsedJavaScriptSource, accumulator: AnalysisAccumulator, - limits: JavaScriptStaticAnalysisLimits, ): JavaScriptStaticAnalysis => { const parserErrors = file.errors.length; const limitations = [ @@ -92,9 +75,6 @@ const finalizeStaticAnalysis = ( : [ "The parser recovered from syntax errors; affected facts are partial.", ]), - ...(accumulator.truncated - ? ["AST analysis stopped at the configured node or time bound."] - : []), ...(accumulator.unknownFindings === 0 ? [] : [ @@ -103,13 +83,12 @@ const finalizeStaticAnalysis = ( "JavaScript syntax was parsed as data and was never evaluated.", ]; return { - parse_status: accumulator.truncated - ? "truncated" - : parserErrors > 0 || accumulator.unknownFindings > 0 + parse_status: + parserErrors > 0 || accumulator.unknownFindings > 0 ? "partial" : "complete", parse_error_count: parserErrors, - visited_ast_nodes: Math.min(accumulator.visitedNodes, limits.maxAstNodes), + visited_ast_nodes: accumulator.visitedNodes, references: finalizeLocatedFindings( accumulator.references, accumulator.modules, @@ -157,7 +136,6 @@ const inspectNode = ( source: string, node: t.Node, accumulator: AnalysisAccumulator, - limits: JavaScriptStaticAnalysisLimits, ): void => { const findings = { source, @@ -165,8 +143,8 @@ const inspectNode = ( }; inspectElectronStaticNode(node, findings); if (t.isCallExpression(node)) { - inspectBundlerRegistration(source, node, accumulator, limits); - inspectEsbuildWrapper(source, node, accumulator, limits); + inspectBundlerRegistration(source, node, accumulator); + inspectEsbuildWrapper(source, node, accumulator); inspectCall(source, node, findings); } else if (t.isNewExpression(node)) inspectCall(source, node, findings); if ( diff --git a/src/domain/javascriptStaticAnalysisBundler.ts b/src/domain/javascriptStaticAnalysisBundler.ts index 2b26dfaf86..a353909917 100644 --- a/src/domain/javascriptStaticAnalysisBundler.ts +++ b/src/domain/javascriptStaticAnalysisBundler.ts @@ -23,7 +23,6 @@ import type { JavaScriptAnalysisAccumulator as AnalysisAccumulator } from "./jav import type { JavaScriptBundlerModule, JavaScriptBundlerRegistration, - JavaScriptStaticAnalysisLimits, } from "./javascriptStaticAnalysisTypes.js"; /** Inspect a bundler registration call and recover its module table. */ @@ -31,7 +30,6 @@ export const inspectBundlerRegistration = ( source: string, call: t.CallExpression, accumulator: AnalysisAccumulator, - limits: JavaScriptStaticAnalysisLimits, ): void => { const runtime = chunkRuntime(call); const entry = call.arguments[0]; @@ -40,7 +38,7 @@ export const inspectBundlerRegistration = ( const table = entry.elements[1]; const runtimeValue = runtimeMetadata(entry.elements[2]); if (!t.isArrayExpression(chunkIds) || !t.isObjectExpression(table)) return; - const recovered = recoverBundlerModules(source, table, accumulator, limits); + const recovered = recoverBundlerModules(source, table, accumulator); const chunkKeys = staticArrayValues(chunkIds); accumulator.unknownFindings += chunkKeys.unknown; accumulator.unknownFindings += @@ -82,7 +80,6 @@ export const inspectEsbuildWrapper = ( source: string, call: t.CallExpression, accumulator: AnalysisAccumulator, - limits: JavaScriptStaticAnalysisLimits, ): void => { const name = calleeName(call.callee); const wrapperKind = @@ -93,7 +90,7 @@ export const inspectEsbuildWrapper = ( : null; const table = call.arguments[0]; if (wrapperKind === null || !t.isObjectExpression(table)) return; - const recovered = recoverBundlerModules(source, table, accumulator, limits); + const recovered = recoverBundlerModules(source, table, accumulator); if (recovered.modules.length === 0) return; accumulator.unknownFindings += recovered.unknownAsyncChunkKeys; const registration: JavaScriptBundlerRegistration = { @@ -129,18 +126,12 @@ const recoverBundlerModules = ( source: string, table: t.ObjectExpression, accumulator: AnalysisAccumulator, - limits: JavaScriptStaticAnalysisLimits, ): RecoveredBundlerModules => { const modules: JavaScriptBundlerModule[] = []; const asyncChunkKeys: string[] = []; let unknownAsyncChunkKeys = 0; for (const property of table.properties) { - const recovered = recoverBundlerModule( - source, - property, - accumulator, - limits, - ); + const recovered = recoverBundlerModule(source, property, accumulator); if (recovered === null) continue; modules.push(recovered.module); asyncChunkKeys.push(...recovered.asyncChunkKeys); @@ -157,7 +148,6 @@ const recoverBundlerModule = ( source: string, property: t.ObjectMethod | t.ObjectProperty | t.SpreadElement, accumulator: AnalysisAccumulator, - limits: JavaScriptStaticAnalysisLimits, ): { readonly module: JavaScriptBundlerModule; readonly asyncChunkKeys: readonly string[]; @@ -168,7 +158,7 @@ const recoverBundlerModule = ( const key = modulePropertyName(property); if (key.startsWith("[computed@") || key.startsWith("[unknown@")) accumulator.unknownFindings += 1; - const fingerprint = fingerprintJavaScriptAst(factory, limits.maxAstNodes); + const fingerprint = fingerprintJavaScriptAst(factory); const exportsValue = collectJavaScriptExports(factory); const requireName = factoryRequireName(factory); const asyncChunks = collectBundlerAsyncChunkKeys(factory, requireName); @@ -179,23 +169,13 @@ const recoverBundlerModule = ( key, requireName, }); - const structuralFingerprint = fingerprint.truncated - ? { - structural_fingerprint_sha256: null, - structural_fingerprint_algorithm: null, - structural_fingerprint_status: "truncated" as const, - } - : { - structural_fingerprint_sha256: fingerprint.sha256, - structural_fingerprint_algorithm: "babel-ast-v1" as const, - structural_fingerprint_status: "complete" as const, - }; return { module: { module_key: key, factory_require_name: requireName, source_sha256: sha256Text(sourceSlice(source, factory)), - ...structuralFingerprint, + structural_fingerprint_sha256: fingerprint, + structural_fingerprint_algorithm: "babel-ast-v1", exports: exportsValue.values, location: range(factory), }, diff --git a/src/domain/javascriptStaticAnalysisHelpers.ts b/src/domain/javascriptStaticAnalysisHelpers.ts index 4f38b74a03..020ecc1051 100644 --- a/src/domain/javascriptStaticAnalysisHelpers.ts +++ b/src/domain/javascriptStaticAnalysisHelpers.ts @@ -57,32 +57,23 @@ export const chunkRuntime = (call: t.CallExpression): string | undefined => { ) return undefined; if (propertyName(call.callee.property) !== "push") return undefined; - return findChunkRuntime(call.callee.object, 0); + return findChunkRuntime(call.callee.object); }; -const findChunkRuntime = (node: t.Node, depth: number): string | undefined => { - if (depth >= 128) return undefined; +const findChunkRuntime = (node: t.Node): string | undefined => { if (t.isIdentifier(node) && /(?:webpack|rspack)Chunk/iu.test(node.name)) return node.name; if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) { const property = propertyName(node.property); if (/(?:webpack|rspack)Chunk/iu.test(property)) return property; - return t.isNode(node.object) - ? findChunkRuntime(node.object, depth + 1) - : undefined; + return t.isNode(node.object) ? findChunkRuntime(node.object) : undefined; } if (t.isAssignmentExpression(node)) - return ( - findChunkRuntime(node.left, depth + 1) ?? - findChunkRuntime(node.right, depth + 1) - ); + return findChunkRuntime(node.left) ?? findChunkRuntime(node.right); if (t.isLogicalExpression(node) || t.isBinaryExpression(node)) - return ( - findChunkRuntime(node.left, depth + 1) ?? - findChunkRuntime(node.right, depth + 1) - ); + return findChunkRuntime(node.left) ?? findChunkRuntime(node.right); if (t.isParenthesizedExpression(node) || t.isTSAsExpression(node)) - return findChunkRuntime(node.expression, depth + 1); + return findChunkRuntime(node.expression); return undefined; }; @@ -145,38 +136,36 @@ export const staticArrayValues = ( /** Resolve a path composed only from inert literal syntax. */ export const staticPath = (node: t.Node): string | undefined => - staticPathAt(node, 0); + staticPathAt(node); /** Classify whether inert path syntax is a module specifier or file expression. */ export const staticPathResolutionContext = ( node: t.Node, ): JavaScriptStaticPathContext => - isFilesystemPathExpression(node, 0) + isFilesystemPathExpression(node) ? "filesystem-expression" : "module-specifier"; -const staticPathAt = (node: t.Node, depth: number): string | undefined => { - if (depth >= 128) return undefined; +const staticPathAt = (node: t.Node): string | undefined => { if (t.isStringLiteral(node)) return node.value; if (t.isTemplateLiteral(node) && node.expressions.length === 0) { const value = node.quasis[0]?.value.cooked ?? node.quasis[0]?.value.raw; return value; } if (t.isBinaryExpression(node, { operator: "+" })) { - const left = staticPathAt(node.left, depth + 1); - const right = staticPathAt(node.right, depth + 1); + const left = staticPathAt(node.left); + const right = staticPathAt(node.right); return left === undefined || right === undefined ? undefined : `${left}${right}`; } if (t.isCallExpression(node) || t.isNewExpression(node)) - return staticCallPath(node, depth); + return staticCallPath(node); return undefined; }; const staticCallPath = ( node: t.CallExpression | t.NewExpression, - depth: number, ): string | undefined => { const name = calleeName(node.callee); if (name === "URL" || name.endsWith(".URL")) @@ -185,9 +174,7 @@ const staticCallPath = ( : undefined; if (name === "fileURLToPath" || name.endsWith(".fileURLToPath")) { const argument = argumentNode(node.arguments[0]); - return argument === undefined - ? undefined - : staticPathAt(argument, depth + 1); + return argument === undefined ? undefined : staticPathAt(argument); } if ( (name === "dirname" || name.endsWith(".dirname")) && @@ -199,23 +186,20 @@ const staticCallPath = ( const parts: string[] = []; for (const argument of node.arguments) { if (isDirectoryIdentity(argumentNode(argument))) continue; - const value = t.isNode(argument) - ? staticPathAt(argument, depth + 1) - : undefined; + const value = t.isNode(argument) ? staticPathAt(argument) : undefined; if (value === undefined) return undefined; parts.push(value); } return parts.length === 0 ? undefined : posix.join(...parts); }; -const isFilesystemPathExpression = (node: t.Node, depth: number): boolean => { - if (depth >= 128) return false; +const isFilesystemPathExpression = (node: t.Node): boolean => { if (t.isStringLiteral(node)) return node.value.startsWith("/"); if (isFileIdentity(node) || isDirectoryIdentity(node)) return true; if (t.isBinaryExpression(node, { operator: "+" })) return ( - isFilesystemPathExpression(node.left, depth + 1) || - isFilesystemPathExpression(node.right, depth + 1) + isFilesystemPathExpression(node.left) || + isFilesystemPathExpression(node.right) ); if (t.isCallExpression(node) || t.isNewExpression(node)) { const name = calleeName(node.callee); @@ -224,15 +208,11 @@ const isFilesystemPathExpression = (node: t.Node, depth: number): boolean => { if (name === "fileURLToPath" || name.endsWith(".fileURLToPath")) return node.arguments.some((argument) => { const value = argumentNode(argument); - return ( - value !== undefined && isFilesystemPathExpression(value, depth + 1) - ); + return value !== undefined && isFilesystemPathExpression(value); }); return node.arguments.some((argument) => { const value = argumentNode(argument); - return ( - value !== undefined && isFilesystemPathExpression(value, depth + 1) - ); + return value !== undefined && isFilesystemPathExpression(value); }); } return false; @@ -306,16 +286,13 @@ export const storageKind = ( }; /** Produce a dotted callee name from member syntax. */ -export const calleeName = (node: t.Node): string => calleeNameAt(node, 0); +export const calleeName = (node: t.Node): string => calleeNameAt(node); -const calleeNameAt = (node: t.Node, depth: number): string => { - if (depth >= 128) return "[deep]"; +const calleeNameAt = (node: t.Node): string => { if (t.isIdentifier(node)) return node.name; if (t.isImport(node)) return "import"; if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) { - const object = t.isNode(node.object) - ? calleeNameAt(node.object, depth + 1) - : ""; + const object = t.isNode(node.object) ? calleeNameAt(node.object) : ""; const property = propertyName(node.property); return object === "" ? property : `${object}.${property}`; } diff --git a/src/domain/javascriptStaticAnalysisState.ts b/src/domain/javascriptStaticAnalysisState.ts index 8d2b059e0d..a137a704c6 100644 --- a/src/domain/javascriptStaticAnalysisState.ts +++ b/src/domain/javascriptStaticAnalysisState.ts @@ -49,7 +49,6 @@ export interface JavaScriptAnalysisAccumulator { readonly seen: Set; visitedNodes: number; unknownFindings: number; - truncated: boolean; } /** Shared source and accumulator for helper inspections. */ @@ -102,5 +101,4 @@ export const createJavaScriptAnalysisAccumulator = seen: new Set(), visitedNodes: 0, unknownFindings: 0, - truncated: false, }); diff --git a/src/domain/javascriptStaticAnalysisTypes.ts b/src/domain/javascriptStaticAnalysisTypes.ts index 6d8fbdc022..4b99316de5 100644 --- a/src/domain/javascriptStaticAnalysisTypes.ts +++ b/src/domain/javascriptStaticAnalysisTypes.ts @@ -11,7 +11,7 @@ export interface JavaScriptSourcePoint { readonly column: number; } -/** One exact source range within a bounded JavaScript artifact. */ +/** One exact source range within a JavaScript artifact. */ export interface JavaScriptSourceRange { readonly start: JavaScriptSourcePoint; readonly end: JavaScriptSourcePoint; @@ -67,19 +67,10 @@ interface JavaScriptBundlerModuleState { } /** One exact module factory recovered from a Webpack/Rspack registration. */ -export type JavaScriptBundlerModule = JavaScriptBundlerModuleState & - ( - | { - readonly structural_fingerprint_sha256: string; - readonly structural_fingerprint_algorithm: "babel-ast-v1"; - readonly structural_fingerprint_status: "complete"; - } - | { - readonly structural_fingerprint_sha256: null; - readonly structural_fingerprint_algorithm: null; - readonly structural_fingerprint_status: "truncated"; - } - ); +export interface JavaScriptBundlerModule extends JavaScriptBundlerModuleState { + readonly structural_fingerprint_sha256: string; + readonly structural_fingerprint_algorithm: "babel-ast-v1"; +} /** One statically recognized bundler chunk or module registration. */ export interface JavaScriptBundlerRegistration { @@ -109,7 +100,7 @@ export interface JavaScriptRolePath { /** Deterministic AST-only analysis of one JavaScript source file. */ export interface JavaScriptStaticAnalysis { - readonly parse_status: "complete" | "partial" | "failed" | "truncated"; + readonly parse_status: "complete" | "partial" | "failed"; readonly parse_error_count: number; readonly visited_ast_nodes: number; readonly references: readonly JavaScriptStaticReference[]; @@ -127,8 +118,3 @@ export interface JavaScriptStaticAnalysis { } /** Hard bounds applied to one AST-only JavaScript analysis. */ -export interface JavaScriptStaticAnalysisLimits { - readonly maxAstNodes: number; - readonly deadline: number; - readonly now: () => number; -} diff --git a/tests/acceptance/applications/applicationEvidenceMcp.test.ts b/tests/acceptance/applications/applicationEvidenceMcp.test.ts index a8adc946f9..bac843f6f3 100644 --- a/tests/acceptance/applications/applicationEvidenceMcp.test.ts +++ b/tests/acceptance/applications/applicationEvidenceMcp.test.ts @@ -88,7 +88,7 @@ async function runInlineEvidenceScenarios( reference: { root_sha256: SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE.reference.root_sha256, }, - scoring: { algorithm: "rea-source-to-bundle-signals/v1" }, + scoring: { algorithm: "rea-source-to-bundle-signals" }, }, }); expect(session.exportEvidenceBundle().records.length).toBeGreaterThan(2); diff --git a/tests/acceptance/applications/applicationWorkflowCli.test.ts b/tests/acceptance/applications/applicationWorkflowCli.test.ts index d6122d6999..5409492b5c 100644 --- a/tests/acceptance/applications/applicationWorkflowCli.test.ts +++ b/tests/acceptance/applications/applicationWorkflowCli.test.ts @@ -117,8 +117,8 @@ describe("application workflow CLI parity", () => { }, 20_000); }); -describe("application workflow CLI Evidence resolution", () => { - it("resolves Evidence IDs from an explicitly authorized bundle", async () => { +describe("application workflow CLI Evidence lookup", () => { + it("resolves Evidence IDs from a caller-supplied bundle", async () => { const root = await createTestTempDirectory("rea-application-bundle-cli-"); temporary.push(root); const bundlePath = join(root, "evidence.json"); @@ -198,7 +198,7 @@ describe("application workflow CLI Evidence resolution", () => { outsideBundlePath, serializeEvidenceBundle(createEvidenceBundle([])), ); - const denied = await runCli( + const outsideBundle = await runCli( [ "trace-application-feature", JSON.stringify(JAVASCRIPT_FEATURE_TRACE_EXAMPLE), @@ -208,7 +208,10 @@ describe("application workflow CLI Evidence resolution", () => { ], environment, ); - expect(denied).toMatchObject({ code: "permission_required" }); + expect(outsideBundle).toMatchObject({ + code: "evidence_integrity_mismatch", + details: { reason: "missing" }, + }); }, 30_000); });