chore: merge main and preserve plist and AppKit regressions
This commit is contained in:
commit
071cfd1bb6
66 files changed
+876
-288
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface ModuleBoundaryViolation {
|
||||
readonly specifier: string;
|
||||
readonly target: string;
|
||||
readonly boundary:
|
||||
| "artifact-acquisition"
|
||||
| "pure-layer"
|
||||
| "application-composition"
|
||||
| "provider-construction"
|
||||
|
||||
@@ -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)
|
||||
|
||||
+9
-1
@@ -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) ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
@@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
JavaScriptArtifactFile,
|
||||
JavaScriptArtifactFileSet,
|
||||
JavaScriptArtifactFileKind,
|
||||
} from "./JavaScriptArtifactFiles.js";
|
||||
} from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import { JavaScriptArtifactGraphAccumulator } from "./JavaScriptArtifactGraphAccumulator.js";
|
||||
import {
|
||||
artifactObservationEvidence,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[]) =>
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -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<string, number | string | boolean | null | { ref: number }>
|
||||
>;
|
||||
}[];
|
||||
}): 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]);
|
||||
};
|
||||
@@ -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<string, number | string | boolean | null | { ref: number }>
|
||||
>;
|
||||
}[];
|
||||
}): 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", () => {
|
||||
|
||||
+12
-47
@@ -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;
|
||||
@@ -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);
|
||||
+39
-5
@@ -18,6 +18,12 @@ export type CliOutputArgumentValidation =
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
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<CliOutputArgumentValidation, { readonly ok: true }>,
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<string>();
|
||||
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),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -100,9 +100,9 @@ export const collectSemanticReferences = (
|
||||
const output: JavaScriptSemanticReference[] = [];
|
||||
const seen = new Set<string>();
|
||||
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.
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
]);
|
||||
});
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -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<typeof declaringType>) => {
|
||||
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,
|
||||
|
||||
@@ -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<string, FieldCore>();
|
||||
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<string, MethodCore>();
|
||||
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,
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 }) =>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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],
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
<rect key="frame" x="120" y="400" width="150" height="44"/>
|
||||
<title key="title" string="Build"/>
|
||||
<connections>
|
||||
<action selector="buildTapped:" destination="vc1" id="action1"/>
|
||||
<action selector="buildTapped:" target="vc1" id="action1"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
|
||||
Reference in new issue
Block a user