diff --git a/docs/capability-migration.md b/docs/capability-migration.md index 4e4960fb41..568cf4411c 100644 --- a/docs/capability-migration.md +++ b/docs/capability-migration.md @@ -277,11 +277,23 @@ unchanged sources under `scripts/fixtures/javascript-recovery/`. Its default REA entrypoint still resolves to `scripts/rea.mjs`. Managed conformance imports the relocated compiled workflow; owning CI source filters follow the new paths. -This application move preserves the current directory/ASAR reader boundary, -including root/nested byte checks, UTF-8 diagnostics, cancellation and owned -close. Reader selection requires a separately traced increment. Runtime ports -retain their distinct Inspector, passive Electron and active Electron effects; -shipped Electron hooks remain at their existing provider boundary. +`src/artifacts/javascript/` owns directory/ASAR reader construction and relevant +file acquisition, including root/nested byte verification, UTF-8 diagnostics and +nested reader cleanup. Its narrow factory returns the existing `ArtifactReader`; +the application retains input admission, inventory, graph construction and its +original root-reader `finally` close. Pure file facts live in +`src/domain/javascript/javascriptArtifactFiles.ts`, and the shared immutable +inventory snapshot lives in `src/domain/artifactInventorySnapshot.ts`. Inventory +options and integrity policy remain application-owned. Shared stream hashing +lives in `src/artifacts/ArtifactHash.ts`, preserving complete byte counts and the +bounded classification prefix. Acquisition/hash guards reject imports from +application, composition or caller adapters; JavaScript workflows use the factory +instead of importing concrete ASAR/directory reader classes. + +ZIP, native-DMG and Mach-O inventory reader selection keep their existing +lifecycles. Runtime ports retain their distinct Inspector, passive Electron and +active Electron effects; shipped Electron hooks remain at their existing +provider boundary. ## Binary application ownership diff --git a/scripts/lib/module-boundaries.d.mts b/scripts/lib/module-boundaries.d.mts index abf8daf9b7..51d5957c6b 100644 --- a/scripts/lib/module-boundaries.d.mts +++ b/scripts/lib/module-boundaries.d.mts @@ -5,6 +5,7 @@ export interface ModuleBoundaryViolation { readonly specifier: string; readonly target: string; readonly boundary: + | "artifact-acquisition" | "pure-layer" | "application-composition" | "provider-construction" diff --git a/scripts/lib/module-boundaries.mjs b/scripts/lib/module-boundaries.mjs index 81bfce577e..e43ccd44cc 100644 --- a/scripts/lib/module-boundaries.mjs +++ b/scripts/lib/module-boundaries.mjs @@ -60,6 +60,22 @@ 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/application/javascript/") && + /^src\/artifacts\/(?:Asar|Directory)ArtifactReader\.(?:js|ts)$/u.test( + target, + ) + ) + return "provider-construction"; + if ( + (file.startsWith("src/artifacts/javascript/") || + /^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 ( PROVIDER_ROOTS.has(layer) && /^src\/generatedMcpToolCatalog\.(?:js|ts)$/u.test(target) diff --git a/scripts/rea.mjs b/scripts/rea.mjs index 0d64bd5524..4e310bf080 100755 --- a/scripts/rea.mjs +++ b/scripts/rea.mjs @@ -41,6 +41,7 @@ if (!(await compiledRuntimeExists(runtimeFiles))) { const { createCli } = await import("../dist/cli.js"); const { renderCliOutputArgumentError, + renderEmptyFilteredCliOutput, sanitizeCliOutput, validateCliOutputArguments, } = await import("../dist/cliOutput.js"); @@ -49,9 +50,16 @@ if (!(await compiledRuntimeExists(runtimeFiles))) { process.stdout.write(renderCliOutputArgumentError(outputArguments)); process.exitCode = 1; } else { + let wroteOutput = false; await createCli().serve(args, { - stdout: (output) => process.stdout.write(sanitizeCliOutput(output)), + stdout: (output) => { + const sanitized = sanitizeCliOutput(output); + if (sanitized.length > 0) wroteOutput = true; + process.stdout.write(sanitized); + }, }); + if (!wroteOutput) + process.stdout.write(renderEmptyFilteredCliOutput(args) ?? ""); } } diff --git a/src/application/ArtifactInventory.ts b/src/application/ArtifactInventory.ts index ea3a9becbf..a7b5b181f9 100644 --- a/src/application/ArtifactInventory.ts +++ b/src/application/ArtifactInventory.ts @@ -4,7 +4,7 @@ import { artifactInventoryResultSchema, type ArtifactInventoryResult, } from "../domain/artifactGraph.js"; -import { abortIfNeeded } from "./ArtifactInventory/hash.js"; +import { abortIfNeeded } from "../artifacts/ArtifactHash.js"; import { scanCanonicalArtifactInventory } from "./ArtifactInventory/scanCanonical.js"; import type { ArtifactIntegrityPolicy, diff --git a/src/application/ArtifactInventory/classify.ts b/src/application/ArtifactInventory/classify.ts index 8dcc0ce4d9..36e953712b 100644 --- a/src/application/ArtifactInventory/classify.ts +++ b/src/application/ArtifactInventory/classify.ts @@ -1,7 +1,7 @@ import { open } from "node:fs/promises"; import { classifyArtifactContent } from "../ArtifactGraphConstruction.js"; -import { ARTIFACT_CLASSIFICATION_PREFIX_BYTES } from "./hash.js"; +import { ARTIFACT_CLASSIFICATION_PREFIX_BYTES } from "../../artifacts/ArtifactHash.js"; import type { ArtifactNode } from "../../domain/artifactGraph.js"; import { hasZipSignature, diff --git a/src/application/ArtifactInventory/scanCanonical.ts b/src/application/ArtifactInventory/scanCanonical.ts index ea28fc56aa..b02005394b 100644 --- a/src/application/ArtifactInventory/scanCanonical.ts +++ b/src/application/ArtifactInventory/scanCanonical.ts @@ -23,7 +23,7 @@ import { } from "../ArtifactGraphConstruction.js"; import { canonicalDigest } from "../../domain/comparisonSemantics.js"; import { classifyRoot } from "./classify.js"; -import { hashReadable, type HashResult } from "./hash.js"; +import { hashReadable, type HashResult } from "../../artifacts/ArtifactHash.js"; import { createReader, inventoryLimitations } from "./reader.js"; import { scanReader, diff --git a/src/application/ArtifactInventory/scanReader.ts b/src/application/ArtifactInventory/scanReader.ts index c388a358be..fe24fe408e 100644 --- a/src/application/ArtifactInventory/scanReader.ts +++ b/src/application/ArtifactInventory/scanReader.ts @@ -22,7 +22,7 @@ import { STRICT_INTEGRITY_POLICY, type ArtifactIntegrityPolicy, } from "./types.js"; -import { hashReadable } from "./hash.js"; +import { hashReadable } from "../../artifacts/ArtifactHash.js"; export interface PendingIntegrityContradiction { readonly logicalPath: string; diff --git a/src/application/ArtifactInventory/types.ts b/src/application/ArtifactInventory/types.ts index 3c7004f060..741b070cfb 100644 --- a/src/application/ArtifactInventory/types.ts +++ b/src/application/ArtifactInventory/types.ts @@ -1,9 +1,3 @@ -import type { - ArtifactInventoryResult, - ArtifactNode, - IntegrityContradiction, -} from "../../domain/artifactGraph.js"; - /** Resolved integrity behavior admitted to the artifact scanner. */ export type ArtifactIntegrityPolicy = | { readonly mode: "fail" } @@ -19,15 +13,4 @@ export interface ArtifactInventoryOptions { readonly integrity?: ArtifactIntegrityPolicy | undefined; } -/** Immutable inventory produced by one complete artifact scan. */ -export interface ArtifactInventorySnapshot { - readonly manifest: ArtifactInventoryResult["manifest"]; - readonly nodes: readonly ArtifactNode[]; - readonly occurrences: ArtifactInventoryResult["occurrences"]; - readonly edges: ArtifactInventoryResult["edges"]; - readonly provenance: ReadonlyArray< - ArtifactInventoryResult["provenance"][number] - >; - readonly integrity_contradictions: readonly IntegrityContradiction[]; - readonly limitations: readonly string[]; -} +export type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js"; diff --git a/src/application/InterfaceBuilderAnalysis.ts b/src/application/InterfaceBuilderAnalysis.ts index 88cadea93f..ffcc412877 100644 --- a/src/application/InterfaceBuilderAnalysis.ts +++ b/src/application/InterfaceBuilderAnalysis.ts @@ -278,16 +278,14 @@ const projectNibArchive = (archive: NibArchiveDocument) => { : className.includes("Control") ? "action" : className; - const controlAction = className.includes("Control"); - const connectionSource = controlAction ? destination : source; - const connectionDestination = controlAction ? source : destination; - if (connectionSource === null) continue; - (connections[String(connectionSource)] ??= []).push({ + // NSSource is the outlet owner or the sending control; NSDestination is + // the outlet value or the action target, and a nil target is the first + // responder. + (connections[String(source)] ??= []).push({ type, - "destination-id": - connectionDestination === null ? null : String(connectionDestination), + "destination-id": destination === null ? null : String(destination), label, - source_id: String(connectionSource), + source_id: String(source), archive_object_id: String(object.id), }); } diff --git a/src/application/javascript/ElectronBoundaryAnalysis.ts b/src/application/javascript/ElectronBoundaryAnalysis.ts index eff377b710..99de494e2d 100644 --- a/src/application/javascript/ElectronBoundaryAnalysis.ts +++ b/src/application/javascript/ElectronBoundaryAnalysis.ts @@ -2,7 +2,7 @@ import { compareCodePoints } from "../../domain/canonicalOrdering.js"; import type { ElectronIpcFinding } from "../../domain/javascript/electronStaticAnalysisTypes.js"; import type { ElectronBoundarySummary } from "../../domain/javascript/javascriptApplicationAnalysis.js"; import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js"; -import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; +import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { resolveArtifactPathByContext } from "./JavaScriptArtifactPathResolution.js"; /** One IPC fact retaining its exact owning artifact for graph projection. */ diff --git a/src/application/javascript/ElectronBoundaryGraphContext.ts b/src/application/javascript/ElectronBoundaryGraphContext.ts index 78e59d74b2..460f849dd0 100644 --- a/src/application/javascript/ElectronBoundaryGraphContext.ts +++ b/src/application/javascript/ElectronBoundaryGraphContext.ts @@ -1,7 +1,7 @@ import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js"; import { sha256Text } from "../../domain/javascript/javascriptStaticAnalysisHelpers.js"; import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js"; -import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; +import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import type { JavaScriptArtifactGraphContext, JavaScriptArtifactGraphCoverage, diff --git a/src/application/javascript/ElectronBoundaryGraphIpc.ts b/src/application/javascript/ElectronBoundaryGraphIpc.ts index 51139b6d42..64a69c4aba 100644 --- a/src/application/javascript/ElectronBoundaryGraphIpc.ts +++ b/src/application/javascript/ElectronBoundaryGraphIpc.ts @@ -1,6 +1,6 @@ import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js"; import type { ElectronSenderValidationFinding } from "../../domain/javascript/electronStaticAnalysisTypes.js"; -import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; +import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { artifactLocalIdentity, javascriptAnalysisCoverage, diff --git a/src/application/javascript/ElectronBoundaryGraphNative.ts b/src/application/javascript/ElectronBoundaryGraphNative.ts index 27e6284282..fffb3f5fb1 100644 --- a/src/application/javascript/ElectronBoundaryGraphNative.ts +++ b/src/application/javascript/ElectronBoundaryGraphNative.ts @@ -1,5 +1,5 @@ import type { ElectronNativeAddonBindingFinding } from "../../domain/javascript/electronStaticAnalysisTypes.js"; -import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; +import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { javascriptAnalysisCoverage, resolveArtifactPath, diff --git a/src/application/javascript/ElectronBoundaryGraphWindows.ts b/src/application/javascript/ElectronBoundaryGraphWindows.ts index 2bce47d243..68ed49e177 100644 --- a/src/application/javascript/ElectronBoundaryGraphWindows.ts +++ b/src/application/javascript/ElectronBoundaryGraphWindows.ts @@ -4,7 +4,7 @@ import type { ElectronContextBridgeFinding, ElectronUtilityProcessFinding, } from "../../domain/javascript/electronStaticAnalysisTypes.js"; -import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; +import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { artifactLocalIdentity, createElectronRoleNode, diff --git a/src/application/javascript/JavaScriptArtifactAnalysis.ts b/src/application/javascript/JavaScriptArtifactAnalysis.ts index b7ef2ad696..9422eacff8 100644 --- a/src/application/javascript/JavaScriptArtifactAnalysis.ts +++ b/src/application/javascript/JavaScriptArtifactAnalysis.ts @@ -14,7 +14,7 @@ import { failedJavaScriptStaticAnalysis } from "../../domain/javascript/javascri import type { JavaScriptArtifactFile, JavaScriptArtifactFileSet, -} from "./JavaScriptArtifactFiles.js"; +} from "../../domain/javascript/javascriptArtifactFiles.js"; import type { AnalyzedJavaScriptArtifactFile, JavaScriptArtifactAnalysis, diff --git a/src/application/javascript/JavaScriptArtifactAnalysisTypes.ts b/src/application/javascript/JavaScriptArtifactAnalysisTypes.ts index df46f721bc..d1cdd2a6b5 100644 --- a/src/application/javascript/JavaScriptArtifactAnalysisTypes.ts +++ b/src/application/javascript/JavaScriptArtifactAnalysisTypes.ts @@ -3,7 +3,7 @@ import type { JavaScriptStaticAnalysis, } from "../../domain/javascript/javascriptStaticAnalysisTypes.js"; import type { JavaScriptSemanticIr } from "../../domain/javascript/javascriptSemanticIr.js"; -import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; +import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; interface JavaScriptStructuredObservation { readonly path: string; diff --git a/src/application/javascript/JavaScriptArtifactGraphBuilder.ts b/src/application/javascript/JavaScriptArtifactGraphBuilder.ts index 096adb50b0..6af56f1a75 100644 --- a/src/application/javascript/JavaScriptArtifactGraphBuilder.ts +++ b/src/application/javascript/JavaScriptArtifactGraphBuilder.ts @@ -4,7 +4,7 @@ import { type JavaScriptApplicationGraph, } from "../../domain/javascript/javascriptApplicationGraph.js"; import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js"; -import type { JavaScriptArtifactFileSet } from "./JavaScriptArtifactFiles.js"; +import type { JavaScriptArtifactFileSet } from "../../domain/javascript/javascriptArtifactFiles.js"; import { JavaScriptArtifactGraphAccumulator } from "./JavaScriptArtifactGraphAccumulator.js"; import type { JavaScriptArtifactGraphContext } from "./JavaScriptArtifactGraphContext.js"; import { diff --git a/src/application/javascript/JavaScriptArtifactGraphBundlers.ts b/src/application/javascript/JavaScriptArtifactGraphBundlers.ts index 49810f6c63..c23182d9bb 100644 --- a/src/application/javascript/JavaScriptArtifactGraphBundlers.ts +++ b/src/application/javascript/JavaScriptArtifactGraphBundlers.ts @@ -1,6 +1,6 @@ import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js"; import type { JavaScriptBundlerRegistration } from "../../domain/javascript/javascriptStaticAnalysisTypes.js"; -import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; +import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { addAstContainsEdge, artifactLocalIdentity, diff --git a/src/application/javascript/JavaScriptArtifactGraphContext.ts b/src/application/javascript/JavaScriptArtifactGraphContext.ts index 945dd47147..8e398bc77a 100644 --- a/src/application/javascript/JavaScriptArtifactGraphContext.ts +++ b/src/application/javascript/JavaScriptArtifactGraphContext.ts @@ -11,7 +11,7 @@ import type { JavaScriptArtifactFile, JavaScriptArtifactFileSet, JavaScriptArtifactFileKind, -} from "./JavaScriptArtifactFiles.js"; +} from "../../domain/javascript/javascriptArtifactFiles.js"; import { JavaScriptArtifactGraphAccumulator } from "./JavaScriptArtifactGraphAccumulator.js"; import { artifactObservationEvidence, diff --git a/src/application/javascript/JavaScriptArtifactGraphFindings.ts b/src/application/javascript/JavaScriptArtifactGraphFindings.ts index abce528274..e3bf4c6e81 100644 --- a/src/application/javascript/JavaScriptArtifactGraphFindings.ts +++ b/src/application/javascript/JavaScriptArtifactGraphFindings.ts @@ -1,6 +1,6 @@ import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js"; import type { JavaScriptStaticAnalysis } from "../../domain/javascript/javascriptStaticAnalysisTypes.js"; -import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; +import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { addStaticInferenceEdge, artifactLocalIdentity, diff --git a/src/application/javascript/JavaScriptArtifactGraphStructure.ts b/src/application/javascript/JavaScriptArtifactGraphStructure.ts index b2a1979367..7a135fdead 100644 --- a/src/application/javascript/JavaScriptArtifactGraphStructure.ts +++ b/src/application/javascript/JavaScriptArtifactGraphStructure.ts @@ -5,7 +5,7 @@ import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTyp import type { JavaScriptArtifactContainer, JavaScriptArtifactFile, -} from "./JavaScriptArtifactFiles.js"; +} from "../../domain/javascript/javascriptArtifactFiles.js"; import type { JavaScriptArtifactGraphAccumulator } from "./JavaScriptArtifactGraphAccumulator.js"; import { addArtifactContainsEdge, diff --git a/src/application/javascript/JavaScriptArtifactPathResolution.test.ts b/src/application/javascript/JavaScriptArtifactPathResolution.test.ts index 1536e32520..3f6232def5 100644 --- a/src/application/javascript/JavaScriptArtifactPathResolution.test.ts +++ b/src/application/javascript/JavaScriptArtifactPathResolution.test.ts @@ -4,7 +4,7 @@ import { resolveArtifactPathByContext, type ResolveArtifactPathInput, } from "./JavaScriptArtifactPathResolution.js"; -import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; +import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { artifactLocalIdentity } from "./JavaScriptArtifactGraphContext.js"; import { applicationNodeIdentitySchema } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js"; import { analyzeJavaScriptStaticSource } from "../../domain/javascript/javascriptStaticAnalysis.js"; diff --git a/src/application/javascript/JavaScriptArtifactPathResolution.ts b/src/application/javascript/JavaScriptArtifactPathResolution.ts index fcc102281d..57a5012b02 100644 --- a/src/application/javascript/JavaScriptArtifactPathResolution.ts +++ b/src/application/javascript/JavaScriptArtifactPathResolution.ts @@ -1,7 +1,7 @@ import { builtinModules } from "node:module"; import { posix } from "node:path"; -import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; +import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { admitsCanonicalPathSyntax, hasScheme, diff --git a/src/application/javascript/JavaScriptArtifactReconstruction.ts b/src/application/javascript/JavaScriptArtifactReconstruction.ts index e5871c2b36..7acf03b058 100644 --- a/src/application/javascript/JavaScriptArtifactReconstruction.ts +++ b/src/application/javascript/JavaScriptArtifactReconstruction.ts @@ -1,16 +1,12 @@ import { lstat, realpath } from "node:fs/promises"; -import { AsarArtifactReader } from "../../artifacts/AsarArtifactReader.js"; -import { - ArtifactReaderFailure, - type ArtifactReader, -} from "../../artifacts/ArtifactReader.js"; -import { DirectoryArtifactReader } from "../../artifacts/DirectoryArtifactReader.js"; +import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; +import { createJavaScriptArtifactReader as createReader } from "../../artifacts/javascript/JavaScriptArtifactReader.js"; import type { JavaScriptApplicationGraph } from "../../domain/javascript/javascriptApplicationGraph.js"; import type { JavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticGraph.js"; import type { ElectronBoundarySummary } from "../../domain/javascript/javascriptApplicationAnalysis.js"; import { analyzeJavaScriptArtifactFiles } from "./JavaScriptArtifactAnalysis.js"; -import { readJavaScriptArtifactFiles } from "./JavaScriptArtifactFiles.js"; +import { readJavaScriptArtifactFiles } from "../../artifacts/javascript/JavaScriptArtifactFiles.js"; import { buildJavaScriptArtifactGraph } from "./JavaScriptArtifactGraphBuilder.js"; import { javascriptArtifactReconstructionInputSchema, @@ -124,14 +120,6 @@ const resolveFormat = async ( return observed; }; -const createReader = ( - path: string, - format: "asar" | "directory", -): ArtifactReader => - format === "asar" - ? new AsarArtifactReader(path) - : new DirectoryArtifactReader(path); - const abortIfNeeded = (signal?: AbortSignal): void => { if (signal?.aborted === true) throw new ArtifactReaderFailure( diff --git a/src/application/javascript/JavaScriptHtmlBaseResolution.test.ts b/src/application/javascript/JavaScriptHtmlBaseResolution.test.ts index a5a9fe93cb..e225c0af17 100644 --- a/src/application/javascript/JavaScriptHtmlBaseResolution.test.ts +++ b/src/application/javascript/JavaScriptHtmlBaseResolution.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { resolveArtifactPathByContext } from "./JavaScriptArtifactPathResolution.js"; -import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; +import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; const paths = [ "renderer/index.html", "assets/app.js", diff --git a/src/application/javascript/JavaScriptJsonModules.ts b/src/application/javascript/JavaScriptJsonModules.ts index 4c0c825366..3733db5cb6 100644 --- a/src/application/javascript/JavaScriptJsonModules.ts +++ b/src/application/javascript/JavaScriptJsonModules.ts @@ -1,6 +1,6 @@ import { compareCodePoints } from "../../domain/canonicalOrdering.js"; import type { JavaScriptJsonModuleObservation } from "./JavaScriptArtifactAnalysisTypes.js"; -import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; +import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; /** Parse one approved JSON module without evaluating JavaScript or resolving imports. */ export const analyzeJavaScriptJsonModule = ( diff --git a/src/application/javascript/JavaScriptModuleRelationships.ts b/src/application/javascript/JavaScriptModuleRelationships.ts index c3f4ed5e9f..d3b688ca4b 100644 --- a/src/application/javascript/JavaScriptModuleRelationships.ts +++ b/src/application/javascript/JavaScriptModuleRelationships.ts @@ -11,7 +11,7 @@ import type { JavaScriptSemanticIr, JavaScriptSemanticModuleLink, } from "../../domain/javascript/javascriptSemanticIr.js"; -import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; +import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { addAstContainsEdge, artifactLocalIdentity, diff --git a/src/application/javascript/JavaScriptModuleSpecifierPaths.test.ts b/src/application/javascript/JavaScriptModuleSpecifierPaths.test.ts index cd5e3eedde..63f44337eb 100644 --- a/src/application/javascript/JavaScriptModuleSpecifierPaths.test.ts +++ b/src/application/javascript/JavaScriptModuleSpecifierPaths.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; +import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { resolveArtifactPathByContext } from "./JavaScriptArtifactPathResolution.js"; const filesFor = (paths: readonly string[]) => diff --git a/src/application/javascript/JavaScriptSemanticGraphBuilder.test.ts b/src/application/javascript/JavaScriptSemanticGraphBuilder.test.ts index 90a61aeb10..e7d1c1240a 100644 --- a/src/application/javascript/JavaScriptSemanticGraphBuilder.test.ts +++ b/src/application/javascript/JavaScriptSemanticGraphBuilder.test.ts @@ -2,7 +2,7 @@ import { expect, it } from "vitest"; import { buildJavaScriptSemanticGraph } from "./JavaScriptSemanticGraphBuilder.js"; import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js"; -import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; +import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { queryJavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticQuery.js"; import { analyzeJavaScriptSemantics } from "../../domain/javascript/javascriptSemanticAnalysis.js"; diff --git a/src/application/javascript/JavaScriptSemanticGraphBuilder.ts b/src/application/javascript/JavaScriptSemanticGraphBuilder.ts index c81418eedf..d82edde74f 100644 --- a/src/application/javascript/JavaScriptSemanticGraphBuilder.ts +++ b/src/application/javascript/JavaScriptSemanticGraphBuilder.ts @@ -14,7 +14,7 @@ import type { import { sourceRangesEqual as rangesEqual } from "../../domain/javascript/javascriptStaticAnalysisHelpers.js"; import type { JavaScriptApplicationGraph } from "../../domain/javascript/javascriptApplicationGraph.js"; import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js"; -import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; +import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { projectSemanticEvents, projectSemanticTimers, diff --git a/src/application/javascript/JavaScriptSemanticGraphConstruction.ts b/src/application/javascript/JavaScriptSemanticGraphConstruction.ts index faa46e7ee9..a19f749826 100644 --- a/src/application/javascript/JavaScriptSemanticGraphConstruction.ts +++ b/src/application/javascript/JavaScriptSemanticGraphConstruction.ts @@ -12,7 +12,7 @@ import type { JavaScriptSemanticGraphUnknown, } from "../../domain/javascript/javascriptSemanticGraphSchemas.js"; import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js"; -import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; +import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { inferredSemanticEvidence, observedSemanticEvidence, diff --git a/src/application/javascript/JavaScriptSemanticGraphEvidence.ts b/src/application/javascript/JavaScriptSemanticGraphEvidence.ts index 9cfaa88b48..4b339ec066 100644 --- a/src/application/javascript/JavaScriptSemanticGraphEvidence.ts +++ b/src/application/javascript/JavaScriptSemanticGraphEvidence.ts @@ -1,7 +1,7 @@ import type { ApplicationGraphEvidence } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js"; import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraph.js"; import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js"; -import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; +import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; const INFERENCE_LIMITATION = "This relationship is a conservative static candidate; it does not prove runtime execution or causal flow."; diff --git a/src/application/javascript/JavaScriptSemanticGraphFingerprintProjection.ts b/src/application/javascript/JavaScriptSemanticGraphFingerprintProjection.ts index 0634b341e9..06c6068208 100644 --- a/src/application/javascript/JavaScriptSemanticGraphFingerprintProjection.ts +++ b/src/application/javascript/JavaScriptSemanticGraphFingerprintProjection.ts @@ -4,7 +4,7 @@ import { type JavaScriptSemanticGraphNode, } from "../../domain/javascript/javascriptSemanticGraph.js"; import type { JavaScriptSemanticIr } from "../../domain/javascript/javascriptSemanticIr.js"; -import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; +import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { observedSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js"; /** Bind domain fingerprint components to exact semantic function nodes. */ diff --git a/src/application/javascript/JavaScriptSemanticGraphFlowProjection.ts b/src/application/javascript/JavaScriptSemanticGraphFlowProjection.ts index c31aef320d..fba83df6b0 100644 --- a/src/application/javascript/JavaScriptSemanticGraphFlowProjection.ts +++ b/src/application/javascript/JavaScriptSemanticGraphFlowProjection.ts @@ -3,7 +3,7 @@ import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javasc import type { JavaScriptSemanticIr } from "../../domain/javascript/javascriptSemanticIr.js"; import { sourceRangesEqual } from "../../domain/javascript/javascriptStaticAnalysisHelpers.js"; import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js"; -import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; +import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { addSemanticGraphNode, addSemanticGraphRelation, diff --git a/src/application/javascript/JavaScriptSemanticGraphResources.test.ts b/src/application/javascript/JavaScriptSemanticGraphResources.test.ts index fd14ef93c1..2f6efff32d 100644 --- a/src/application/javascript/JavaScriptSemanticGraphResources.test.ts +++ b/src/application/javascript/JavaScriptSemanticGraphResources.test.ts @@ -2,7 +2,7 @@ import { expect, it } from "vitest"; import { buildJavaScriptSemanticGraph } from "./JavaScriptSemanticGraphBuilder.js"; import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js"; -import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; +import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { queryJavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticQuery.js"; import { analyzeJavaScriptSemantics } from "../../domain/javascript/javascriptSemanticAnalysis.js"; diff --git a/src/application/javascript/JavaScriptSemanticGraphScope.test.ts b/src/application/javascript/JavaScriptSemanticGraphScope.test.ts index ec7b0341ef..cb4a3b7deb 100644 --- a/src/application/javascript/JavaScriptSemanticGraphScope.test.ts +++ b/src/application/javascript/JavaScriptSemanticGraphScope.test.ts @@ -2,7 +2,7 @@ import { expect, it } from "vitest"; import { buildJavaScriptSemanticGraph } from "./JavaScriptSemanticGraphBuilder.js"; import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js"; -import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; +import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { analyzeJavaScriptSemantics } from "../../domain/javascript/javascriptSemanticAnalysis.js"; import { topLevelBinding } from "../../domain/javascript/javascriptSemanticAnalysis.fixture.js"; diff --git a/src/application/ArtifactInventory/hash.ts b/src/artifacts/ArtifactHash.ts similarity index 78% rename from src/application/ArtifactInventory/hash.ts rename to src/artifacts/ArtifactHash.ts index c988717c1c..b3bf43bfab 100644 --- a/src/application/ArtifactInventory/hash.ts +++ b/src/artifacts/ArtifactHash.ts @@ -1,9 +1,10 @@ import { createHash } from "node:crypto"; import type { Readable } from "node:stream"; -import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; -import { streamChunkToBuffer } from "../../artifacts/StreamBytes.js"; +import { ArtifactReaderFailure } from "./ArtifactReader.js"; +import { streamChunkToBuffer } from "./StreamBytes.js"; +/** Full stream digest and byte count with bounded classification evidence. */ export type HashResult = { readonly sha256: string; readonly bytes: number; @@ -13,6 +14,7 @@ export type HashResult = { /** Bounded classification evidence; this is not an executable format size limit. */ export const ARTIFACT_CLASSIFICATION_PREFIX_BYTES = 8_192; +/** Preserve the inventory cancellation reason at a stream boundary. */ export const abortIfNeeded = (signal?: AbortSignal): void => { if (signal?.aborted === true) throw new ArtifactReaderFailure( @@ -21,6 +23,7 @@ export const abortIfNeeded = (signal?: AbortSignal): void => { ); }; +/** Hash every stream byte and retain its bounded classification prefix. */ export const hashReadable = async ( stream: Readable, signal?: AbortSignal, diff --git a/src/artifacts/NibArchive.fixture.ts b/src/artifacts/NibArchive.fixture.ts new file mode 100644 index 0000000000..09f2d6e2c5 --- /dev/null +++ b/src/artifacts/NibArchive.fixture.ts @@ -0,0 +1,94 @@ +/** Encode a minimal NIBArchive object table for decoder and projection tests. */ +export const encodeNibArchiveFixture = (input: { + readonly classes: readonly string[]; + readonly objects: readonly { + readonly classIndex: number; + readonly values: Readonly< + Record + >; + }[]; +}): Buffer => { + const keys = [ + ...new Set(input.objects.flatMap(({ values }) => Object.keys(values))), + ]; + const keyIndex = new Map(keys.map((key, index) => [key, index])); + const valueRecords = input.objects.flatMap(({ values }) => + Object.entries(values).map(([key, value]) => + Buffer.concat([varint(keyIndex.get(key) ?? 0), encodeValue(value)]), + ), + ); + let valueStart = 0; + const records = input.objects.map(({ classIndex, values }) => { + const count = Object.keys(values).length; + const result = Buffer.concat([ + varint(classIndex), + varint(valueStart), + varint(count), + ]); + valueStart += count; + return result; + }); + const objects = Buffer.concat(records); + const encodedKeys = Buffer.concat( + keys.map((key) => + Buffer.concat([varint(Buffer.byteLength(key)), Buffer.from(key)]), + ), + ); + const values = Buffer.concat(valueRecords); + const classes = Buffer.concat( + input.classes.map((name) => { + const bytes = Buffer.from(name); + return Buffer.concat([varint(bytes.length), varint(0), bytes]); + }), + ); + const objectsOffset = 50; + const keysOffset = objectsOffset + objects.length; + const valuesOffset = keysOffset + encodedKeys.length; + const classesOffset = valuesOffset + values.length; + const header = Buffer.alloc(50); + header.write("NIBArchive", 0, "ascii"); + header.writeUInt32LE(1, 10); + header.writeUInt32LE(10, 14); + header.writeUInt32LE(input.objects.length, 18); + header.writeUInt32LE(objectsOffset, 22); + header.writeUInt32LE(keys.length, 26); + header.writeUInt32LE(keysOffset, 30); + header.writeUInt32LE(valueStart, 34); + header.writeUInt32LE(valuesOffset, 38); + header.writeUInt32LE(input.classes.length, 42); + header.writeUInt32LE(classesOffset, 46); + return Buffer.concat([header, objects, encodedKeys, values, classes]); +}; + +const varint = (value: number): Buffer => { + const bytes: number[] = []; + let remaining = value; + do { + let byte = remaining & 0x7f; + remaining >>>= 7; + if (remaining === 0) byte |= 0x80; + bytes.push(byte); + } while (remaining > 0); + return Buffer.from(bytes); +}; + +const encodeValue = ( + value: number | string | boolean | null | { ref: number }, +): Buffer => { + if (typeof value === "boolean") return Buffer.from([value ? 4 : 5]); + if (value === null) return Buffer.from([9]); + if (typeof value === "number") { + const bytes = Buffer.alloc(5); + bytes[0] = 2; + bytes.writeInt32LE(value, 1); + return bytes; + } + if (typeof value === "object") { + const bytes = Buffer.alloc(5); + bytes[0] = 10; + bytes.writeUInt32LE(value.ref, 1); + return bytes; + } + const data = Buffer.from(value); + return Buffer.concat([Buffer.from([8]), varint(data.length), data]); +}; diff --git a/src/artifacts/NibArchive.test.ts b/src/artifacts/NibArchive.test.ts index f9b4fac922..a771217853 100644 --- a/src/artifacts/NibArchive.test.ts +++ b/src/artifacts/NibArchive.test.ts @@ -1,100 +1,7 @@ import { describe, expect, it } from "vitest"; import { decodeNibArchive } from "./NibArchive.js"; - -const encodeArchive = (input: { - readonly classes: readonly string[]; - readonly objects: readonly { - readonly classIndex: number; - readonly values: Readonly< - Record - >; - }[]; -}): Buffer => { - const keys = [ - ...new Set(input.objects.flatMap(({ values }) => Object.keys(values))), - ]; - const keyIndex = new Map(keys.map((key, index) => [key, index])); - const valueRecords = input.objects.flatMap(({ values }) => - Object.entries(values).map(([key, value]) => - Buffer.concat([varint(keyIndex.get(key) ?? 0), encodeValue(value)]), - ), - ); - let valueStart = 0; - const records = input.objects.map(({ classIndex, values }) => { - const count = Object.keys(values).length; - const result = Buffer.concat([ - varint(classIndex), - varint(valueStart), - varint(count), - ]); - valueStart += count; - return result; - }); - const objects = Buffer.concat(records); - const encodedKeys = Buffer.concat( - keys.map((key) => - Buffer.concat([varint(Buffer.byteLength(key)), Buffer.from(key)]), - ), - ); - const values = Buffer.concat(valueRecords); - const classes = Buffer.concat( - input.classes.map((name) => { - const bytes = Buffer.from(name); - return Buffer.concat([varint(bytes.length), varint(0), bytes]); - }), - ); - const objectsOffset = 50; - const keysOffset = objectsOffset + objects.length; - const valuesOffset = keysOffset + encodedKeys.length; - const classesOffset = valuesOffset + values.length; - const header = Buffer.alloc(50); - header.write("NIBArchive", 0, "ascii"); - header.writeUInt32LE(1, 10); - header.writeUInt32LE(10, 14); - header.writeUInt32LE(input.objects.length, 18); - header.writeUInt32LE(objectsOffset, 22); - header.writeUInt32LE(keys.length, 26); - header.writeUInt32LE(keysOffset, 30); - header.writeUInt32LE(valueStart, 34); - header.writeUInt32LE(valuesOffset, 38); - header.writeUInt32LE(input.classes.length, 42); - header.writeUInt32LE(classesOffset, 46); - return Buffer.concat([header, objects, encodedKeys, values, classes]); -}; - -const varint = (value: number): Buffer => { - const bytes: number[] = []; - let remaining = value; - do { - let byte = remaining & 0x7f; - remaining >>>= 7; - if (remaining === 0) byte |= 0x80; - bytes.push(byte); - } while (remaining > 0); - return Buffer.from(bytes); -}; - -const encodeValue = ( - value: number | string | boolean | null | { ref: number }, -): Buffer => { - if (typeof value === "boolean") return Buffer.from([value ? 4 : 5]); - if (value === null) return Buffer.from([9]); - if (typeof value === "number") { - const bytes = Buffer.alloc(5); - bytes[0] = 2; - bytes.writeInt32LE(value, 1); - return bytes; - } - if (typeof value === "object") { - const bytes = Buffer.alloc(5); - bytes[0] = 10; - bytes.writeUInt32LE(value.ref, 1); - return bytes; - } - const data = Buffer.from(value); - return Buffer.concat([Buffer.from([8]), varint(data.length), data]); -}; +import { encodeNibArchiveFixture as encodeArchive } from "./NibArchive.fixture.js"; describe("NIBArchive decoder", () => { it("decodes bounded object, key, class, and reference tables", () => { diff --git a/src/application/javascript/JavaScriptArtifactFiles.ts b/src/artifacts/javascript/JavaScriptArtifactFiles.ts similarity index 86% rename from src/application/javascript/JavaScriptArtifactFiles.ts rename to src/artifacts/javascript/JavaScriptArtifactFiles.ts index e70e765abc..6f9cdb845b 100644 --- a/src/application/javascript/JavaScriptArtifactFiles.ts +++ b/src/artifacts/javascript/JavaScriptArtifactFiles.ts @@ -3,61 +3,26 @@ import { isAbsolute } from "node:path"; import type { Readable } from "node:stream"; import { compareCodePoints } from "../../domain/canonicalOrdering.js"; -import { AsarArtifactReader } from "../../artifacts/AsarArtifactReader.js"; +import { AsarArtifactReader } from "../AsarArtifactReader.js"; import { ArtifactPathRegistry, normalizeArtifactPath, -} from "../../artifacts/ArtifactPaths.js"; +} from "../ArtifactPaths.js"; import { ArtifactReaderFailure, type ArtifactEntry, type ArtifactReader, -} from "../../artifacts/ArtifactReader.js"; -import { streamChunkToBuffer } from "../../artifacts/StreamBytes.js"; -import { hashReadable } from "../ArtifactInventory/hash.js"; -import type { ArtifactInventorySnapshot } from "../ArtifactInventory.js"; +} from "../ArtifactReader.js"; +import { streamChunkToBuffer } from "../StreamBytes.js"; +import { hashReadable } from "../ArtifactHash.js"; +import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js"; -/** Relevant file categories projected from the complete artifact inventory. */ -export type JavaScriptArtifactFileKind = - | "package-json" - | "json" - | "javascript" - | "html" - | "source-map" - | "native-addon"; - -/** One content-addressed local artifact file and its text availability. */ -export interface JavaScriptArtifactFile { - readonly path: string; - readonly container_sha256: string; - readonly sha256: string; - readonly bytes: number; - readonly inventory_artifact_id: string; - readonly kind: JavaScriptArtifactFileKind; - readonly unpacked: boolean; - readonly text: - | { readonly included: true; readonly value: string } - | { - readonly included: false; - readonly reason: "not-applicable" | "invalid-utf8"; - }; -} - -/** One filesystem-backed ASAR nested beneath a directory input. */ -export interface JavaScriptArtifactContainer { - readonly path: string; - readonly sha256: string; - readonly bytes: number; - readonly inventory_artifact_id: string; -} - -/** Deterministic relevant-file projection plus explicit omissions. */ -export interface JavaScriptArtifactFileSet { - readonly files: readonly JavaScriptArtifactFile[]; - readonly containers: readonly JavaScriptArtifactContainer[]; - readonly text_bytes_read: number; - readonly invalid_utf8_files: number; -} +import type { + JavaScriptArtifactFileKind, + JavaScriptArtifactFile, + JavaScriptArtifactContainer, + JavaScriptArtifactFileSet, +} from "../../domain/javascript/javascriptArtifactFiles.js"; interface ExpectedFile { readonly path: string; diff --git a/src/artifacts/javascript/JavaScriptArtifactReader.ts b/src/artifacts/javascript/JavaScriptArtifactReader.ts new file mode 100644 index 0000000000..4d2b88c528 --- /dev/null +++ b/src/artifacts/javascript/JavaScriptArtifactReader.ts @@ -0,0 +1,12 @@ +import { AsarArtifactReader } from "../AsarArtifactReader.js"; +import type { ArtifactReader } from "../ArtifactReader.js"; +import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js"; + +/** Create the reader for an admitted JavaScript artifact format. */ +export const createJavaScriptArtifactReader = ( + path: string, + format: "asar" | "directory", +): ArtifactReader => + format === "asar" + ? new AsarArtifactReader(path) + : new DirectoryArtifactReader(path); diff --git a/src/cliOutput.ts b/src/cliOutput.ts index 0d029337cc..96f65a7e7b 100644 --- a/src/cliOutput.ts +++ b/src/cliOutput.ts @@ -18,6 +18,12 @@ export type CliOutputArgumentValidation = type JsonRecord = Record; +interface ParsedCliOutputArguments { + readonly filterOutput: boolean; + readonly format: string; + readonly tokenWindow: boolean; +} + const isRecord = (value: unknown): value is JsonRecord => typeof value === "object" && value !== null && !Array.isArray(value); @@ -28,16 +34,16 @@ const validationError = (value: JsonRecord): JsonRecord | undefined => { return undefined; }; -/** Reject text-window controls that would corrupt a structured document. */ -export const validateCliOutputArguments = ( +const parseCliOutputArguments = ( arguments_: readonly string[], -): CliOutputArgumentValidation => { +): ParsedCliOutputArguments => { let format: string = "toon"; + let filterOutput = false; let tokenWindow = false; for (let index = 0; index < arguments_.length; index += 1) { const argument = arguments_[index]; - // Incur scans builtins across `--` and accepts only the spaced --format form. - // Mirror its effective format so malformed requests still emit complete JSON. + // Incur scans builtins across `--` and accepts only spaced value forms. + // Mirror those effective values at the executable output boundary. if (argument === "--json") { format = "json"; continue; @@ -50,6 +56,13 @@ export const validateCliOutputArguments = ( } continue; } + if (argument === "--filter-output") { + if (arguments_[index + 1] !== undefined) { + filterOutput = true; + index += 1; + } + continue; + } if (argument === "--token-limit" || argument === "--token-offset") { if (arguments_[index + 1] !== undefined) { tokenWindow = true; @@ -63,6 +76,14 @@ export const validateCliOutputArguments = ( ) tokenWindow = true; } + return { filterOutput, format, tokenWindow }; +}; + +/** Reject text-window controls that would corrupt a structured document. */ +export const validateCliOutputArguments = ( + arguments_: readonly string[], +): CliOutputArgumentValidation => { + const { format, tokenWindow } = parseCliOutputArguments(arguments_); if ( tokenWindow && (format === "json" || format === "jsonl" || format === "yaml") @@ -76,6 +97,19 @@ export const validateCliOutputArguments = ( return { ok: true }; }; +/** Preserve a complete document when Incur filters the entire result away. */ +export const renderEmptyFilteredCliOutput = ( + arguments_: readonly string[], +): string | undefined => { + const { filterOutput, format } = parseCliOutputArguments(arguments_); + if ( + filterOutput && + (format === "json" || format === "jsonl" || format === "yaml") + ) + return "{}\n"; + return undefined; +}; + /** Render one complete structured error before command execution. */ export const renderCliOutputArgumentError = ( error: Exclude, diff --git a/src/domain/artifactInventorySnapshot.ts b/src/domain/artifactInventorySnapshot.ts new file mode 100644 index 0000000000..a219727a12 --- /dev/null +++ b/src/domain/artifactInventorySnapshot.ts @@ -0,0 +1,18 @@ +import type { + ArtifactInventoryResult, + ArtifactNode, + IntegrityContradiction, +} from "./artifactGraph.js"; + +/** Immutable inventory produced by one complete artifact scan. */ +export interface ArtifactInventorySnapshot { + readonly manifest: ArtifactInventoryResult["manifest"]; + readonly nodes: readonly ArtifactNode[]; + readonly occurrences: ArtifactInventoryResult["occurrences"]; + readonly edges: ArtifactInventoryResult["edges"]; + readonly provenance: ReadonlyArray< + ArtifactInventoryResult["provenance"][number] + >; + readonly integrity_contradictions: readonly IntegrityContradiction[]; + readonly limitations: readonly string[]; +} diff --git a/src/domain/javascript/javascriptArtifactFiles.ts b/src/domain/javascript/javascriptArtifactFiles.ts new file mode 100644 index 0000000000..f0f03e0a08 --- /dev/null +++ b/src/domain/javascript/javascriptArtifactFiles.ts @@ -0,0 +1,41 @@ +/** Relevant file categories projected from the complete artifact inventory. */ +export type JavaScriptArtifactFileKind = + | "package-json" + | "json" + | "javascript" + | "html" + | "source-map" + | "native-addon"; + +/** One content-addressed local artifact file and its text availability. */ +export interface JavaScriptArtifactFile { + readonly path: string; + readonly container_sha256: string; + readonly sha256: string; + readonly bytes: number; + readonly inventory_artifact_id: string; + readonly kind: JavaScriptArtifactFileKind; + readonly unpacked: boolean; + readonly text: + | { readonly included: true; readonly value: string } + | { + readonly included: false; + readonly reason: "not-applicable" | "invalid-utf8"; + }; +} + +/** One filesystem-backed ASAR nested beneath a directory input. */ +export interface JavaScriptArtifactContainer { + readonly path: string; + readonly sha256: string; + readonly bytes: number; + readonly inventory_artifact_id: string; +} + +/** Deterministic relevant-file projection plus explicit omissions. */ +export interface JavaScriptArtifactFileSet { + readonly files: readonly JavaScriptArtifactFile[]; + readonly containers: readonly JavaScriptArtifactContainer[]; + readonly text_bytes_read: number; + readonly invalid_utf8_files: number; +} diff --git a/src/domain/javascript/javascriptAstFingerprint.ts b/src/domain/javascript/javascriptAstFingerprint.ts index 569a4895d5..00bbaefba0 100644 --- a/src/domain/javascript/javascriptAstFingerprint.ts +++ b/src/domain/javascript/javascriptAstFingerprint.ts @@ -2,6 +2,8 @@ import { createHash } from "node:crypto"; import * as t from "@babel/types"; +import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; + import { compareCodePoints } from "../canonicalOrdering.js"; import { propertyName } from "./javascriptAstValues.js"; import { stringValue } from "./javascriptStaticAnalysisHelpers.js"; @@ -14,9 +16,11 @@ export interface StaticExports { /** Derive a rename-resistant syntax fingerprint without code execution. */ export const fingerprintJavaScriptAst = (node: t.Node): string => { const tokens: string[] = []; - t.traverseFast(node, (current) => { - tokens.push(...semanticTokens(current)); - return undefined; + traverseJavaScriptAst(node, { + enter: (current) => { + tokens.push(...semanticTokens(current)); + return undefined; + }, }); return createHash("sha256").update(JSON.stringify(tokens)).digest("hex"); }; @@ -24,11 +28,13 @@ export const fingerprintJavaScriptAst = (node: t.Node): string => { /** Collect statically declared CommonJS/bundler export names from one factory. */ export const collectJavaScriptExports = (node: t.Node): StaticExports => { const exports = new Set(); - t.traverseFast(node, (current) => { - if (t.isAssignmentExpression(current)) - collectAssignmentExports(current.left, current.right, exports); - if (t.isCallExpression(current)) collectCallExports(current, exports); - return undefined; + traverseJavaScriptAst(node, { + enter: (current) => { + if (t.isAssignmentExpression(current)) + collectAssignmentExports(current.left, current.right, exports); + if (t.isCallExpression(current)) collectCallExports(current, exports); + return undefined; + }, }); return { values: [...exports].sort(compareCodePoints), diff --git a/src/domain/javascript/javascriptSemanticAnalysis.dataflow.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.dataflow.test.ts index fa9ab25969..384b8fe671 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.dataflow.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.dataflow.test.ts @@ -282,6 +282,9 @@ describe("primitive addition values", () => { ['"x" + false', "xfalse"], ['null + "x"', "nullx"], ["3 + 4", 7], + ['1 + 2 + "3"', "33"], + ['1 + (2 + "3")', "123"], + ['(1 + 2) + ("3" + 4)', "334"], ])("recovers %s using primitive coercion", (expression, expected) => { const ir = analyzeJavaScriptSemantics(`const answer = ${expression};`); expect(topLevelBinding(ir, "answer").value).toEqual({ diff --git a/src/domain/javascript/javascriptSemanticAnalysis.rejection.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.rejection.test.ts index 5f6c05f3a1..0df194a05f 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.rejection.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.rejection.test.ts @@ -1,7 +1,17 @@ +import * as t from "@babel/types"; import { fc, it } from "@fast-check/vitest"; import { describe, expect } from "vitest"; -import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; +import { + analyzeJavaScriptSemantics, + analyzeParsedJavaScriptSemantics, +} from "./javascriptSemanticAnalysis.js"; +import { parseJavaScriptSource } from "./javascriptSourceParser.js"; +import { analyzeParsedJavaScriptStaticSource } from "./javascriptStaticAnalysis.js"; +import { + collectJavaScriptExports, + fingerprintJavaScriptAst, +} from "./javascriptAstFingerprint.js"; import type { JavaScriptSemanticValue } from "./javascriptSemanticIr.js"; import { onlyCallable, @@ -9,6 +19,45 @@ import { } from "./javascriptSemanticAnalysis.fixture.js"; describe("JavaScript semantic analysis: rejection 1", () => { + it.each(["left", "right"])( + "retains semantic facts and values from a deeply %s-chained AST", + (direction) => { + // Build an admitted AST directly: Babel's own recursion limit varies + // with parser optimization and must not determine this regression. + const parsed = parseJavaScriptSource("const result = 1; result;"); + const declaration = parsed?.program.body[0]; + if (parsed === null || !t.isVariableDeclaration(declaration)) + throw new TypeError("Expected a parsed variable declaration"); + const declarator = declaration.declarations[0]; + if (declarator === undefined) + throw new TypeError("Expected a variable declarator"); + let expression: t.Expression = t.numericLiteral(1); + for (let index = 1; index < 12000; index += 1) + expression = + direction === "left" + ? t.binaryExpression("+", expression, t.numericLiteral(1)) + : t.binaryExpression("+", t.numericLiteral(1), expression); + declarator.init = expression; + const staticAnalysis = analyzeParsedJavaScriptStaticSource("", parsed); + expect(staticAnalysis.parse_status).toBe("complete"); + expect(staticAnalysis.visited_ast_nodes).toBe(24006); + expect(collectJavaScriptExports(parsed).values).toEqual([]); + expect(fingerprintJavaScriptAst(parsed)).toMatch(/^[a-f0-9]{64}$/u); + const ir = analyzeParsedJavaScriptSemantics(parsed); + expect(ir.coverage.status).toBe("complete"); + const binding = topLevelBinding(ir, "result"); + expect(binding.value).toEqual({ status: "literal", value: 12000 }); + expect(ir.references).toEqual([ + expect.objectContaining({ + name: "result", + role: "read", + resolution: "resolved", + bindingId: binding.bindingId, + }), + ]); + }, + ); + it("retains complete finite facts and deeply nested static values", () => { const names = Array.from( { length: 280 }, diff --git a/src/domain/javascript/javascriptSemanticProjection.ts b/src/domain/javascript/javascriptSemanticProjection.ts index 7df1464b68..578c53a139 100644 --- a/src/domain/javascript/javascriptSemanticProjection.ts +++ b/src/domain/javascript/javascriptSemanticProjection.ts @@ -100,9 +100,9 @@ export const collectSemanticReferences = ( const output: JavaScriptSemanticReference[] = []; const seen = new Set(); traverseJavaScriptAst(program, { - enter: (node, parent, ancestors) => { + enter: (node, parent, readAncestors) => { if (!t.isIdentifier(node) || parent === null) return; - const role = semanticIdentifierRole(node, parent, ancestors); + const role = semanticIdentifierRole(node, parent, readAncestors()); if (role === null) return; // A compound assignment or update reads before it writes; emit both so // reference consumers see the read they actually execute. diff --git a/src/domain/javascript/javascriptSemanticTraversal.test.ts b/src/domain/javascript/javascriptSemanticTraversal.test.ts new file mode 100644 index 0000000000..1d012c1df2 --- /dev/null +++ b/src/domain/javascript/javascriptSemanticTraversal.test.ts @@ -0,0 +1,53 @@ +import * as t from "@babel/types"; +import { expect, it } from "vitest"; + +import { parseJavaScriptSource } from "./javascriptSourceParser.js"; +import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; + +it("preserves source-tree enter/exit order and detached ancestor snapshots", () => { + const parsed = parseJavaScriptSource("const answer = first + second;"); + if (parsed === null) + throw new TypeError("Expected a parsed JavaScript fixture"); + const visits: string[] = []; + const snapshots: (readonly t.Node[])[] = []; + traverseJavaScriptAst(parsed.program, { + enter: (node, parent, readAncestors) => { + visits.push(`enter:${node.type}`); + const ancestors = readAncestors(); + expect(ancestors.at(-1) ?? null).toBe(parent); + if (t.isIdentifier(node)) snapshots.push(ancestors); + }, + exit: (node) => visits.push(`exit:${node.type}`), + }); + expect(visits).toEqual([ + "enter:Program", + "enter:VariableDeclaration", + "enter:VariableDeclarator", + "enter:Identifier", + "exit:Identifier", + "enter:BinaryExpression", + "enter:Identifier", + "exit:Identifier", + "enter:Identifier", + "exit:Identifier", + "exit:BinaryExpression", + "exit:VariableDeclarator", + "exit:VariableDeclaration", + "exit:Program", + ]); + expect(snapshots.map((nodes) => nodes.map(({ type }) => type))).toEqual([ + ["Program", "VariableDeclaration", "VariableDeclarator"], + [ + "Program", + "VariableDeclaration", + "VariableDeclarator", + "BinaryExpression", + ], + [ + "Program", + "VariableDeclaration", + "VariableDeclarator", + "BinaryExpression", + ], + ]); +}); diff --git a/src/domain/javascript/javascriptSemanticTraversal.ts b/src/domain/javascript/javascriptSemanticTraversal.ts index e782055323..d3a99f2707 100644 --- a/src/domain/javascript/javascriptSemanticTraversal.ts +++ b/src/domain/javascript/javascriptSemanticTraversal.ts @@ -3,7 +3,8 @@ import * as t from "@babel/types"; /** * Visit every Babel AST node in deterministic source-tree order. * - * `ancestors` runs outermost-first and ends with the direct parent. Callers + * `readAncestors` returns a snapshot during `enter`, outermost-first and + * ending with the direct parent. Callers * need it whenever meaning depends on where a node sits, not just what its * immediate parent is — a binding inside `catch ({message})` or `({a} = o)` * has an ObjectProperty parent, so the parent alone cannot say whether the @@ -15,24 +16,40 @@ export const traverseJavaScriptAst = ( readonly enter: ( node: t.Node, parent: t.Node | null, - ancestors: readonly t.Node[], + readAncestors: () => readonly t.Node[], ) => void; readonly exit?: (node: t.Node, parent: t.Node | null) => void; }, ): void => { - const visit = ( - node: t.Node, - parent: t.Node | null, - ancestors: readonly t.Node[], - ): void => { - visitor.enter(node, parent, ancestors); - for (const child of childNodes(node)) - visit(child, node, [...ancestors, node]); - visitor.exit?.(node, parent); + const ancestors: t.Node[] = []; + const enter = (node: t.Node, parent: t.Node | null): TraversalFrame => { + visitor.enter(node, parent, () => [...ancestors]); + ancestors.push(node); + return { node, parent, children: childNodes(node), nextIndex: 0 }; }; - visit(root, null, []); + const pending = [enter(root, null)]; + while (pending.length > 0) { + const current = pending.at(-1); + if (current === undefined) break; + const child = current.children[current.nextIndex]; + if (child !== undefined) { + current.nextIndex += 1; + pending.push(enter(child, current.node)); + } else { + visitor.exit?.(current.node, current.parent); + ancestors.pop(); + pending.pop(); + } + } }; +interface TraversalFrame { + readonly node: t.Node; + readonly parent: t.Node | null; + readonly children: readonly t.Node[]; + nextIndex: number; +} + const childNodes = (node: t.Node): t.Node[] => { const keys: readonly string[] = t.VISITOR_KEYS[node.type] ?? []; return keys.flatMap((key) => { diff --git a/src/domain/javascript/javascriptSemanticValues.ts b/src/domain/javascript/javascriptSemanticValues.ts index 8a55b74d45..78fc72c1c6 100644 --- a/src/domain/javascript/javascriptSemanticValues.ts +++ b/src/domain/javascript/javascriptSemanticValues.ts @@ -268,12 +268,38 @@ const evaluateAddition = ( node: t.BinaryExpression, context: EvaluationContext, ): JavaScriptSemanticValue => { - const left = primitiveCandidates( - evaluateExpression(node.left, nestedContext(context)), - ); - const right = primitiveCandidates( - evaluateExpression(node.right, nestedContext(context)), - ); + const pending: { + readonly node: t.BinaryExpression; + left: JavaScriptSemanticValue | undefined; + }[] = []; + let current: t.Node = node; + while (true) { + if (t.isBinaryExpression(current, { operator: "+" })) { + pending.push({ node: current, left: undefined }); + current = current.left; + continue; + } + let value = evaluateExpression(current, nestedContext(context)); + while (true) { + const parent = pending.at(-1); + if (parent === undefined) return value; + if (parent.left === undefined) { + parent.left = value; + current = parent.node.right; + break; + } + value = addPrimitiveValues(parent.left, value); + pending.pop(); + } + } +}; + +const addPrimitiveValues = ( + leftValue: JavaScriptSemanticValue, + rightValue: JavaScriptSemanticValue, +): JavaScriptSemanticValue => { + const left = primitiveCandidates(leftValue); + const right = primitiveCandidates(rightValue); if (left === null || right === null) return { status: "unknown", reason: "Non-primitive addition." }; const values = left.flatMap((leftValue) => diff --git a/src/domain/javascript/javascriptStaticAnalysis.ts b/src/domain/javascript/javascriptStaticAnalysis.ts index c1e0a56be5..72a462ba87 100644 --- a/src/domain/javascript/javascriptStaticAnalysis.ts +++ b/src/domain/javascript/javascriptStaticAnalysis.ts @@ -1,5 +1,7 @@ import * as t from "@babel/types"; +import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; + import { inspectElectronStaticNode } from "./electronStaticAnalysis.js"; import { detectVendors, @@ -56,10 +58,12 @@ const traverseStaticSource = ( file: ParsedJavaScriptSource, accumulator: AnalysisAccumulator, ): void => { - t.traverseFast(file, (node) => { - accumulator.visitedNodes += 1; - inspectNode(source, node, accumulator); - return undefined; + traverseJavaScriptAst(file, { + enter: (node) => { + accumulator.visitedNodes += 1; + inspectNode(source, node, accumulator); + return undefined; + }, }); }; diff --git a/src/domain/javascript/javascriptStaticAnalysisBundler.ts b/src/domain/javascript/javascriptStaticAnalysisBundler.ts index d5fb295580..3e964f332f 100644 --- a/src/domain/javascript/javascriptStaticAnalysisBundler.ts +++ b/src/domain/javascript/javascriptStaticAnalysisBundler.ts @@ -1,5 +1,7 @@ import * as t from "@babel/types"; +import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; + import { collectJavaScriptExports, fingerprintJavaScriptAst, @@ -251,13 +253,16 @@ const collectBundlerCallArgumentValues = ( ): StaticValues => { const values: string[] = []; let unknown = 0; - t.traverseFast(factory, (node) => { - if (!t.isCallExpression(node) && !t.isNewExpression(node)) return undefined; - if (calleeName(node.callee) !== callee) return undefined; - const value = argumentValue(node.arguments[0]); - if (value === undefined) unknown += 1; - else values.push(value); - return undefined; + traverseJavaScriptAst(factory, { + enter: (node) => { + if (!t.isCallExpression(node) && !t.isNewExpression(node)) + return undefined; + if (calleeName(node.callee) !== callee) return undefined; + const value = argumentValue(node.arguments[0]); + if (value === undefined) unknown += 1; + else values.push(value); + return undefined; + }, }); return uniqueValues(values, unknown); }; diff --git a/src/dotnet/ManagedMemberInspectorCore.test.ts b/src/dotnet/ManagedMemberInspectorCore.test.ts new file mode 100644 index 0000000000..2067e614be --- /dev/null +++ b/src/dotnet/ManagedMemberInspectorCore.test.ts @@ -0,0 +1,96 @@ +import { fc, it } from "@fast-check/vitest"; +import { describe, expect } from "vitest"; + +import { + createDeclaringTypeLookup, + declaringType, + type TypeRange, +} from "./ManagedMemberInspectorCore.js"; + +const range = ( + name: string, + fieldStart: number, + fieldEnd: number, + methodStart = fieldStart, + methodEnd = fieldEnd, +): TypeRange => ({ + token: name, + fullName: name, + fieldStart, + fieldEnd, + methodStart, + methodEnd, +}); + +describe("managed declaring type ownership", () => { + it("resolves field and method boundaries independently, including empty types and gaps", () => { + const ranges = [ + range("first", 1, 3, 1, 1), + range("empty", 3, 3, 1, 1), + range("last", 5, 7, 1, 4), + ]; + const field = createDeclaringTypeLookup(ranges, "field"); + const method = createDeclaringTypeLookup(ranges, "method"); + expect([0, 1, 2, 3, 4, 5, 6, 7].map(field)).toEqual([ + null, + { token: "first", fullName: "first" }, + { token: "first", fullName: "first" }, + null, + null, + { token: "last", fullName: "last" }, + { token: "last", fullName: "last" }, + null, + ]); + expect([0, 1, 2, 3, 4].map(method)).toEqual([ + null, + { token: "last", fullName: "last" }, + { token: "last", fullName: "last" }, + { token: "last", fullName: "last" }, + null, + ]); + }); + + it("retains the first observed owner for overlapping, reversed, or out-of-order ranges", () => { + const ranges = [ + range("first", 5, 9), + range("overlap", 1, 7), + range("reversed", 10, 2), + ]; + const lookup = createDeclaringTypeLookup(ranges, "method"); + expect(lookup(6)).toEqual({ token: "first", fullName: "first" }); + expect(lookup(2)).toEqual({ token: "overlap", fullName: "overlap" }); + expect(lookup(10)).toBeNull(); + expect(createDeclaringTypeLookup([], "method")(1)).toBeNull(); + }); + + it.prop([ + fc.array( + fc.record({ + fieldStart: fc.integer({ min: 0, max: 30 }), + fieldEnd: fc.integer({ min: 0, max: 30 }), + methodStart: fc.integer({ min: 0, max: 30 }), + methodEnd: fc.integer({ min: 0, max: 30 }), + }), + { maxLength: 20 }, + ), + ])( + "agrees with observed first-match ownership for arbitrary metadata ranges", + (raw) => { + const ranges = raw.map((value, index) => ({ + ...value, + token: String(index), + fullName: `Type${String(index)}`, + })); + for (const table of ["field", "method"] as const) { + const lookup = createDeclaringTypeLookup(ranges, table); + for (const row of [ + NaN, + -Infinity, + Infinity, + ...Array.from({ length: 32 }, (_, i) => i), + ]) + expect(lookup(row)).toEqual(declaringType(ranges, table, row)); + } + }, + ); +}); diff --git a/src/dotnet/ManagedMemberInspectorCore.ts b/src/dotnet/ManagedMemberInspectorCore.ts index 8302e7857b..218761fbc8 100644 --- a/src/dotnet/ManagedMemberInspectorCore.ts +++ b/src/dotnet/ManagedMemberInspectorCore.ts @@ -150,6 +150,42 @@ export const declaringType = ( return null; }; +/** Resolve member ownership without rescanning ordered type ranges per row. */ +export const createDeclaringTypeLookup = ( + ranges: readonly TypeRange[], + table: "field" | "method", +): ((row: number) => ReturnType) => { + const entries = ranges.map((range) => ({ + range, + start: table === "field" ? range.fieldStart : range.methodStart, + end: table === "field" ? range.fieldEnd : range.methodEnd, + })); + const ordered = entries.every((entry, index) => { + const previous = entries[index - 1]; + return ( + entry.start <= entry.end && + (previous === undefined || previous.end <= entry.start) + ); + }); + // Malformed metadata can overlap or reverse ranges. Retain the original + // first-match interpretation rather than guessing a different owner. + if (!ordered) return (row) => declaringType(ranges, table, row); + return (row) => { + let first = 0; + let end = entries.length; + while (first < end) { + const middle = first + Math.floor((end - first) / 2); + const entry = entries[middle]; + if (entry === undefined) return null; + if (row >= entry.start && row < entry.end) + return { token: entry.range.token, fullName: entry.range.fullName }; + if (row < entry.start) end = middle; + else first = middle + 1; + } + return null; + }; +}; + export const parseTypes = ( bytes: Buffer, layout: ManagedMetadataLayout, diff --git a/src/dotnet/ManagedMemberRows.ts b/src/dotnet/ManagedMemberRows.ts index 9eced54138..d948e54f76 100644 --- a/src/dotnet/ManagedMemberRows.ts +++ b/src/dotnet/ManagedMemberRows.ts @@ -1,7 +1,7 @@ import type { ManagedPeLayout } from "./ManagedPeReader.js"; import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js"; import { - declaringType, + createDeclaringTypeLookup, signature, type FieldCore, type ManagedCallEdge, @@ -33,6 +33,7 @@ export const parseFields = ( const fields: ManagedField[] = []; const core = new Map(); const table = layout.table(4); + const declaringType = createDeclaringTypeLookup(ranges, "field"); for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) { const cursor = metadataRowCursor(bytes, layout, 4, row); const flags = cursor.readUInt16(); @@ -48,7 +49,7 @@ export const parseFields = ( cursor.readIndex(layout.blobIndexSize), layout.blob.size, ); - const declared = declaringType(ranges, "field", row); + const declared = declaringType(row); const token = metadataToken(4, row); fields.push({ token, @@ -123,6 +124,7 @@ export const parseMethods = ({ const methods: ManagedMethod[] = []; const core = new Map(); const table = layout.table(6); + const declaringType = createDeclaringTypeLookup(ranges, "method"); for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) { const cursor = metadataRowCursor(bytes, layout, 6, row); const rva = cursor.readUInt32(); @@ -141,7 +143,7 @@ export const parseMethods = ({ layout.blob.size, ); cursor.readIndex(layout.tableIndexSize(8)); - const declared = declaringType(ranges, "method", row); + const declared = declaringType(row); const token = metadataToken(6, row); methods.push({ token, diff --git a/tests/boundary/cli/cliOutput.test.ts b/tests/boundary/cli/cliOutput.test.ts index 4ca3d913d0..22fa1b0a65 100644 --- a/tests/boundary/cli/cliOutput.test.ts +++ b/tests/boundary/cli/cliOutput.test.ts @@ -8,6 +8,7 @@ import { workspaceCliTest } from "../../support/cli/workspaceCliFixture.js"; import { renderCliOutputArgumentError, + renderEmptyFilteredCliOutput, sanitizeCliOutput, validateCliOutputArguments, } from "../../../src/cliOutput.js"; @@ -140,9 +141,85 @@ describe("CLI output argument and sanitization boundary", () => { meta: { command: "analyze" }, }); }); + + it("renders an explicit empty projection for structured filtered output", () => { + for (const format of ["json", "jsonl", "yaml"]) + expect( + renderEmptyFilteredCliOutput([ + "providers", + "--format", + format, + "--filter-output", + "missing", + ]), + ).toBe("{}\n"); + expect( + renderEmptyFilteredCliOutput(["providers", "--filter-output", "missing"]), + ).toBeUndefined(); + expect( + renderEmptyFilteredCliOutput(["providers", "--format", "json"]), + ).toBeUndefined(); + }); }); describe("compiled CLI output boundary", () => { + workspaceCliTest( + "emits valid JSON when an output filter misses the top-level result", + async ({ cli }) => { + const result = await cli.run({ + arguments: [ + "analyze-javascript-application", + "tests/conformance/readiness/javascript-cli", + "--format", + "json", + "--filter-output", + "summary", + ], + }); + expect(result).toMatchObject({ + exitCode: 0, + stdout: "{}\n", + stderr: "", + json: {}, + }); + const nested = await cli.run({ + arguments: [ + "analyze-javascript-application", + "tests/conformance/readiness/javascript-cli", + "--format", + "json", + "--filter-output", + "normalized_result.not_a_field", + ], + }); + expect(nested).toMatchObject({ + exitCode: 0, + stderr: "", + json: { normalized_result: {} }, + }); + const selected = await cli.run({ + arguments: [ + "analyze-javascript-application", + "tests/conformance/readiness/javascript-cli", + "--format", + "json", + "--filter-output", + "normalized_result.summary", + ], + }); + expect(selected).toMatchObject({ + exitCode: 0, + stderr: "", + json: { + normalized_result: { + summary: expect.objectContaining({ browser_windows: 0 }), + }, + }, + }); + }, + CLI_INTEGRATION_TIMEOUT_MS, + ); + workspaceCliTest( "sanitizes a real missing-argument dispatcher failure", async ({ cli }) => { diff --git a/tests/boundary/cli/dispatcher.test.ts b/tests/boundary/cli/dispatcher.test.ts index 7c0705405f..d94cb7ec75 100644 --- a/tests/boundary/cli/dispatcher.test.ts +++ b/tests/boundary/cli/dispatcher.test.ts @@ -27,7 +27,7 @@ describe("executable dispatcher", () => { ), workspace.write( "dist/cliOutput.js", - "export const sanitizeCliOutput = (output) => output; export const validateCliOutputArguments = () => ({ ok: true }); export const renderCliOutputArgumentError = () => '';\n", + "export const sanitizeCliOutput = (output) => output; export const validateCliOutputArguments = () => ({ ok: true }); export const renderCliOutputArgumentError = () => ''; export const renderEmptyFilteredCliOutput = () => undefined;\n", ), workspace.write( "dist/mcpDoctor.js", diff --git a/tests/boundary/filesystem/artifactMzInventory.test.ts b/tests/boundary/filesystem/artifactMzInventory.test.ts index 26f1b91a73..fd9884f957 100644 --- a/tests/boundary/filesystem/artifactMzInventory.test.ts +++ b/tests/boundary/filesystem/artifactMzInventory.test.ts @@ -5,7 +5,7 @@ import { describe, expect, it } from "vitest"; import { classifyArtifactContent } from "../../../src/application/ArtifactGraphConstruction.js"; import { classifyRoot } from "../../../src/application/ArtifactInventory/classify.js"; -import { ARTIFACT_CLASSIFICATION_PREFIX_BYTES } from "../../../src/application/ArtifactInventory/hash.js"; +import { ARTIFACT_CLASSIFICATION_PREFIX_BYTES } from "../../../src/artifacts/ArtifactHash.js"; import { scanCanonicalArtifactInventory } from "../../../src/application/ArtifactInventory/scanCanonical.js"; import { targetFormatSchema } from "../../../src/contracts/toolOutputSchemaPrimitives.js"; import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph.js"; diff --git a/tests/boundary/filesystem/interfaceBuilderBundle.test.ts b/tests/boundary/filesystem/interfaceBuilderBundle.test.ts index 5b8db634ce..0885aeea42 100644 --- a/tests/boundary/filesystem/interfaceBuilderBundle.test.ts +++ b/tests/boundary/filesystem/interfaceBuilderBundle.test.ts @@ -8,6 +8,7 @@ import { buildBinary } from "plist"; import { describe, expect, it } from "vitest"; import { analyzeInterfaceBuilderBundle } from "../../../src/application/InterfaceBuilderAnalysis.js"; +import { encodeNibArchiveFixture } from "../../../src/artifacts/NibArchive.fixture.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; const compile = promisify(execFile); @@ -71,6 +72,99 @@ describe("compiled Interface Builder bundle reader", () => { ); }); + it("projects compiled AppKit actions from the control to their target", async () => { + const root = await createTestTempDirectory("rea-ib-test-"); + const bundle = join(root, "Example.app"); + const resources = join(bundle, "Contents", "Resources"); + await mkdir(resources, { recursive: true }); + // NSNibControlConnector archives the sending control as NSSource and its + // target as NSDestination; a nil target is the first responder. + await writeFile( + join(resources, "Panel.nib"), + encodeNibArchiveFixture({ + classes: [ + "NSNibExternalObjectPlaceholder", + "NSButton", + "NSNibControlConnector", + "NSString", + "NSMenuItem", + ], + objects: [ + { classIndex: 0, values: {} }, + { classIndex: 1, values: {} }, + { + classIndex: 2, + values: { + NSSource: { ref: 1 }, + NSDestination: { ref: 0 }, + NSLabel: { ref: 3 }, + }, + }, + { classIndex: 3, values: { "NS.bytes": "doOK:" } }, + { classIndex: 4, values: {} }, + { + classIndex: 2, + values: { + NSSource: { ref: 4 }, + NSDestination: null, + NSLabel: { ref: 6 }, + }, + }, + { classIndex: 3, values: { "NS.bytes": "arrangeInFront:" } }, + ], + }), + ); + + const analysis = await analyzeInterfaceBuilderBundle({ + bundlePath: bundle, + targetSha256: "e".repeat(64), + }); + const byId = new Map(analysis.graph.nodes.map((node) => [node.id, node])); + const routes = analysis.graph.nodes + .filter(({ kind }) => kind === "action") + .map((action) => ({ + action: action.name, + from: analysis.graph.edges + .filter( + ({ relation, to }) => + relation === "target_action" && to === action.id, + ) + .map(({ from }) => byId.get(from)?.name), + to: analysis.graph.edges + .filter( + ({ relation, from, to }) => + relation === "target_action" && + from === action.id && + (to === null || byId.get(to)?.kind !== "objc_selector"), + ) + .map(({ to }) => (to === null ? null : byId.get(to)?.name)), + })) + .sort((left, right) => left.action.localeCompare(right.action)); + expect(routes).toEqual([ + { action: "arrangeInFront:", from: ["NSMenuItem"], to: [null] }, + { + action: "doOK:", + from: ["NSButton"], + to: ["NSNibExternalObjectPlaceholder"], + }, + ]); + }); + + it("honors cancellation during directory traversal", async () => { + const root = await createTestTempDirectory("rea-ib-test-"); + const controller = new AbortController(); + controller.abort(); + await expect( + analyzeInterfaceBuilderBundle({ + bundlePath: root, + targetSha256: "c".repeat(64), + signal: controller.signal, + }), + ).rejects.toMatchObject({ reason: "cancelled" }); + }); +}); + +describe("flat Interface Builder plist values", () => { it("decodes flat nib archives that hold data and date values", async () => { const root = await createTestTempDirectory("rea-ib-test-"); const bundle = join(root, "Example.app"); @@ -112,19 +206,6 @@ describe("compiled Interface Builder bundle reader", () => { ); expect(analysis.graph.nodes.map(({ name }) => name)).toContain("Build"); }); - - it("honors cancellation during directory traversal", async () => { - const root = await createTestTempDirectory("rea-ib-test-"); - const controller = new AbortController(); - controller.abort(); - await expect( - analyzeInterfaceBuilderBundle({ - bundlePath: root, - targetSha256: "c".repeat(64), - signal: controller.signal, - }), - ).rejects.toMatchObject({ reason: "cancelled" }); - }); }); describe("bounded Interface Builder archive decoding", () => { @@ -225,7 +306,18 @@ describe("bounded Interface Builder archive decoding", () => { analysis.graph.nodes.find(({ id }) => id === from)?.kind === "control", ); - expect(actionSource).toBeDefined(); + const describeNode = (id: string | null) => { + const node = analysis.graph.nodes.find((item) => item.id === id); + return node === undefined ? String(id) : `${node.kind}:${node.name}`; + }; + const actionRoutes = analysis.graph.edges + .filter( + ({ relation, from, to }) => + relation === "target_action" && + (from === action?.id || to === action?.id), + ) + .map(({ from, to }) => `${describeNode(from)} -> ${describeNode(to)}`); + expect(actionSource, actionRoutes.join("; ")).toBeDefined(); expect( analysis.graph.edges.some( ({ from, relation, to }) => diff --git a/tests/boundary/filesystem/javascriptArtifactContentIdentity.test.ts b/tests/boundary/filesystem/javascriptArtifactContentIdentity.test.ts index 93b1a7ab35..74fb9ab221 100644 --- a/tests/boundary/filesystem/javascriptArtifactContentIdentity.test.ts +++ b/tests/boundary/filesystem/javascriptArtifactContentIdentity.test.ts @@ -5,7 +5,7 @@ import { createPackage } from "@electron/asar"; import { describe, expect, it } from "vitest"; import { scanArtifactInventory } from "../../../src/application/ArtifactInventory.js"; -import { readJavaScriptArtifactFiles } from "../../../src/application/javascript/JavaScriptArtifactFiles.js"; +import { readJavaScriptArtifactFiles } from "../../../src/artifacts/javascript/JavaScriptArtifactFiles.js"; import { AsarArtifactReader } from "../../../src/artifacts/AsarArtifactReader.js"; import { DirectoryArtifactReader } from "../../../src/artifacts/DirectoryArtifactReader.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; diff --git a/tests/boundary/filesystem/javascriptArtifactReconstruction.test.ts b/tests/boundary/filesystem/javascriptArtifactReconstruction.test.ts index a510ea17f8..cfab06c6bb 100644 --- a/tests/boundary/filesystem/javascriptArtifactReconstruction.test.ts +++ b/tests/boundary/filesystem/javascriptArtifactReconstruction.test.ts @@ -6,7 +6,7 @@ import { expect, it } from "vitest"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; -import { readJavaScriptArtifactFiles } from "../../../src/application/javascript/JavaScriptArtifactFiles.js"; +import { readJavaScriptArtifactFiles } from "../../../src/artifacts/javascript/JavaScriptArtifactFiles.js"; import { reconstructJavaScriptArtifact } from "../../../src/application/javascript/JavaScriptArtifactReconstruction.js"; import { scanArtifactInventory } from "../../../src/application/ArtifactInventory.js"; import { diff --git a/tests/boundary/javascript/packageConditionalExports.test.ts b/tests/boundary/javascript/packageConditionalExports.test.ts index 88be2f269f..05b4465ef5 100644 --- a/tests/boundary/javascript/packageConditionalExports.test.ts +++ b/tests/boundary/javascript/packageConditionalExports.test.ts @@ -8,7 +8,7 @@ import { resolveArtifactPathByContext, type ArtifactPathResolution, } from "../../../src/application/javascript/JavaScriptArtifactPathResolution.js"; -import type { JavaScriptArtifactFile } from "../../../src/application/javascript/JavaScriptArtifactFiles.js"; +import type { JavaScriptArtifactFile } from "../../../src/domain/javascript/javascriptArtifactFiles.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; const execute = promisify(execFile); const TARGETS = [ diff --git a/tests/evaluation/moduleBoundaries.test.ts b/tests/evaluation/moduleBoundaries.test.ts index abf88802fd..b9b1146d81 100644 --- a/tests/evaluation/moduleBoundaries.test.ts +++ b/tests/evaluation/moduleBoundaries.test.ts @@ -18,6 +18,48 @@ describe("incremental module import boundaries", () => { ["src/contracts/probe.ts", "../android/Provider.js", false], ["src/contracts/probe.ts", "../cli.js", false], ["src/contracts/probe.ts", "../composition/android.js", false], + [ + "src/artifacts/javascript/probe.ts", + "../../application/ArtifactInventory.js", + false, + ], + [ + "src/artifacts/javascript/probe.ts", + "../../composition/android.js", + false, + ], + ["src/artifacts/javascript/probe.ts", "../../cli.js", false], + [ + "src/artifacts/ArtifactHash.ts", + "../application/ArtifactInventory/hash.js", + false, + ], + [ + "src/artifacts/javascript/probe.ts", + "../../domain/artifactInventorySnapshot.js", + true, + ], + ["src/artifacts/javascript/probe.ts", "../AsarArtifactReader.js", true], + [ + "src/application/javascript/probe.ts", + "../../artifacts/AsarArtifactReader.js", + false, + ], + [ + "src/application/javascript/probe.ts", + "../../artifacts/DirectoryArtifactReader.js", + false, + ], + [ + "src/application/javascript/probe.ts", + "../../artifacts/javascript/JavaScriptArtifactReader.js", + true, + ], + [ + "src/application/ArtifactInventory/probe.ts", + "../../artifacts/AsarArtifactReader.js", + true, + ], ["src/domain/probe.test.ts", "../composition/firmware.js", false], ["src/application/probe.ts", "../android/JadxProvider.js", false], ["src/application/probe.ts", "../composition/android.js", false], diff --git a/tests/fixtures/interface-builder/MacFixture.storyboard b/tests/fixtures/interface-builder/MacFixture.storyboard index 995dc1d128..f9fe157f92 100644 --- a/tests/fixtures/interface-builder/MacFixture.storyboard +++ b/tests/fixtures/interface-builder/MacFixture.storyboard @@ -11,7 +11,7 @@ <connections> - <action selector="buildTapped:" destination="vc1" id="action1"/> + <action selector="buildTapped:" target="vc1" id="action1"/> </connections> </button> </subviews>