2026-10-09 22:45:18 +08:00
import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js" ;
2026-10-11 00:03:20 +08:00
import { compositeKey } from "../../domain/unicodeCodePointOrder.js" ;
2026-08-10 14:57:44 +08:00
import {
completeApplicationCoverage ,
partialApplicationCoverage ,
type ApplicationGraphEvidence ,
2026-10-07 17:16:58 +08:00
} from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js" ;
2026-10-09 22:45:18 +08:00
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js" ;
2026-10-07 17:16:58 +08:00
import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js" ;
2026-10-08 16:15:35 +08:00
import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js" ;
2026-07-16 05:29:40 +08:00
import type {
JavaScriptArtifactFile ,
JavaScriptArtifactFileSet ,
2026-07-17 23:57:23 +08:00
JavaScriptArtifactFileKind ,
2026-10-07 17:57:33 +08:00
} from "../../domain/javascript/javascriptArtifactFiles.js" ;
2026-07-16 05:29:40 +08:00
import { JavaScriptArtifactGraphAccumulator } from "./JavaScriptArtifactGraphAccumulator.js" ;
import {
artifactObservationEvidence ,
astObservationEvidence ,
staticInferenceEvidence ,
unavailableAstEvidence ,
} from "./JavaScriptArtifactGraphEvidence.js" ;
2026-07-17 21:30:24 +08:00
import {
resolveArtifactPathByContext ,
type ArtifactPathResolution ,
} from "./JavaScriptArtifactPathResolution.js" ;
2026-07-16 05:29:40 +08:00
/** Mutable graph-construction state scoped to one reconstruction. */
export interface JavaScriptArtifactGraphContext {
readonly accumulator : JavaScriptArtifactGraphAccumulator ;
readonly snapshot : ArtifactInventorySnapshot ;
readonly fileSet : JavaScriptArtifactFileSet ;
2026-10-08 16:15:35 +08:00
readonly analysis : JavaScriptModuleArtifactAnalysis ;
2026-07-16 05:29:40 +08:00
readonly root : ApplicationNode ;
readonly filesByPath : ReadonlyMap < string , JavaScriptArtifactFile >;
readonly fileNodes : Map < string , ApplicationNode >;
readonly assetNodes : Map < string , ApplicationNode >;
2026-07-17 23:57:23 +08:00
readonly sourceModuleNodes : Map < string , ApplicationNode >;
2026-07-18 00:31:51 +08:00
readonly chunkNodes : Map < string , ApplicationNode >;
2026-07-16 05:29:40 +08:00
readonly moduleNodes : Map < string , ApplicationNode >;
readonly containerNodes : Map < string , ApplicationNode >;
}
/** Evidence coverage shape shared by reconstruction graph helpers. */
export type JavaScriptArtifactGraphCoverage =
ApplicationGraphEvidence [ "coverage" ];
/** Direct artifact-containment edge values. */
export interface ContainsEdgeInput {
readonly source : ApplicationNode ;
readonly target : ApplicationNode ;
readonly file : JavaScriptArtifactFile ;
readonly operation : string ;
}
/** Exact AST containment edge values. */
export interface AstEdgeInput {
readonly source : ApplicationNode ;
readonly target : ApplicationNode ;
readonly file : JavaScriptArtifactFile ;
readonly range : JavaScriptSourceRange ;
readonly coverage : JavaScriptArtifactGraphCoverage ;
readonly properties : Readonly < Record < string , unknown >>;
}
/** Inferred static relationship edge values. */
export interface InferenceEdgeInput extends AstEdgeInput {
readonly relation :
| "imports"
| "loads"
| "maps_to"
| "exposes"
| "calls"
| "persists_to" ;
}
/** Electron role identity and static discovery evidence. */
export interface RoleNodeInput {
readonly kind : "electron-main" | "electron-preload" | "electron-renderer" ;
readonly anchor : JavaScriptArtifactFile ;
2026-07-17 21:30:24 +08:00
readonly resolution : ArtifactPathResolution ;
2026-07-16 05:29:40 +08:00
readonly mechanism : string ;
readonly range? : JavaScriptSourceRange ;
readonly coverage? : JavaScriptArtifactGraphCoverage ;
}
/** Resolution from an inferred Electron role to an artifact asset. */
export interface RoleAssetLinkInput {
readonly role : ApplicationNode ;
readonly anchor : JavaScriptArtifactFile ;
2026-07-17 21:30:24 +08:00
readonly resolution : ArtifactPathResolution ;
2026-07-16 05:29:40 +08:00
readonly range? : JavaScriptSourceRange ;
readonly coverage? : JavaScriptArtifactGraphCoverage ;
}
/** Explicit package, JavaScript, or source-map parse gap. */
export interface UnavailableParseInput {
readonly file : JavaScriptArtifactFile ;
readonly asset : ApplicationNode ;
readonly operation : string ;
readonly limitation : string ;
}
/** Add a direct artifact-containment edge. */
export const addArtifactContainsEdge = (
context : JavaScriptArtifactGraphContext ,
input : ContainsEdgeInput ,
) : void => {
context . accumulator . addEdge ({
source_node_id : input.source.node_id ,
target_node_id : input.target.node_id ,
relation : "contains" ,
properties : { path : input.file.path },
evidence : artifactObservationEvidence ({
sha256 :
input.operation === "inventory-entry"
? input.file.container_sha256
: input.file.sha256 ,
path : input.file.path ,
operation : input.operation ,
2026-08-10 14:57:44 +08:00
coverage : completeApplicationCoverage (),
2026-07-16 05:29:40 +08:00
}),
});
};
/** Add an exact AST containment edge. */
export const addAstContainsEdge = (
context : JavaScriptArtifactGraphContext ,
input : AstEdgeInput ,
) : void => {
context . accumulator . addEdge ({
source_node_id : input.source.node_id ,
target_node_id : input.target.node_id ,
relation : "contains" ,
properties : input.properties ,
evidence : astObservationEvidence ({
sha256 : input.file.sha256 ,
path : input.file.path ,
range : input.range ,
operation : "map-bundler-structure" ,
coverage : input.coverage ,
}),
});
};
/** Add an explicitly inferred static relationship edge. */
export const addStaticInferenceEdge = (
context : JavaScriptArtifactGraphContext ,
input : InferenceEdgeInput ,
) : void => {
context . accumulator . addEdge ({
source_node_id : input.source.node_id ,
target_node_id : input.target.node_id ,
relation : input.relation ,
properties : input.properties ,
evidence : staticInferenceEvidence ({
sha256 : input.file.sha256 ,
path : input.file.path ,
range : input.range ,
operation : "map-static-relationship" ,
coverage : input.coverage ,
}),
});
};
/** Preserve a static parse or approval gap as an explicit unknown scope. */
export const addUnavailableStaticParseScope = (
context : JavaScriptArtifactGraphContext ,
input : UnavailableParseInput ,
) : void => {
const node = context . accumulator . addNode ({
kind : "unknown" ,
identity : artifactLocalIdentity (
input . file . sha256 ,
"static-parse-scope" ,
` ${ input . operation } : ${ input . file . path } ` ,
),
observations : [
{
label : ` ${ input . file . path } parse scope` ,
properties : {
path : input.file.path ,
operation : input.operation ,
limitation : input.limitation ,
},
evidence : unavailableAstEvidence ({
sha256 : input.file.sha256 ,
path : input.file.path ,
operation : input.operation ,
2026-08-10 14:57:44 +08:00
coverage : partialApplicationCoverage ([], null ),
2026-07-16 05:29:40 +08:00
limitation : input.limitation ,
}),
},
],
});
context . accumulator . addEdge ({
source_node_id : input.asset.node_id ,
target_node_id : node.node_id ,
relation : "contains" ,
properties : { scope : "unavailable-static-analysis" },
evidence : artifactObservationEvidence ({
sha256 : input.file.sha256 ,
path : input.file.path ,
operation : "associate-unavailable-parse-scope" ,
2026-08-10 14:57:44 +08:00
coverage : completeApplicationCoverage (),
2026-07-16 05:29:40 +08:00
}),
});
};
/** Coverage for one bounded JavaScript AST analysis. */
export const javascriptAnalysisCoverage = (
analysis : NonNullable <
2026-10-08 16:15:35 +08:00
JavaScriptModuleArtifactAnalysis [ "files" ][ number ][ "javascript" ]
2026-07-16 05:29:40 +08:00
> ,
) : JavaScriptArtifactGraphCoverage =>
analysis . parse_status === "complete"
2026-09-29 14:53:50 +08:00
? completeApplicationCoverage ()
: partialApplicationCoverage ([], null );
2026-07-16 05:29:40 +08:00
/** Stable identity for a fact scoped to one exact artifact. */
export const artifactLocalIdentity = (
sha256 : string ,
namespace : string ,
key : string ,
) => ({
strategy : "artifact-local-key" as const ,
stability : "artifact-version" as const ,
artifact_sha256 : sha256 ,
2026-09-29 14:53:50 +08:00
namespace ,
key ,
2026-07-16 05:29:40 +08:00
});
2026-07-17 23:57:23 +08:00
/** Select the graph node kind for one inventoried relevant file. */
export const artifactFileNodeKind = (
kind : JavaScriptArtifactFileKind ,
) : ApplicationNode [ "kind" ] =>
kind === "javascript"
? "javascript-asset"
: kind === "source-map"
? "source-map"
: kind === "native-addon"
? "native-addon"
: "artifact" ;
2026-07-16 05:29:40 +08:00
/** Create or merge an Electron role inferred from static syntax or metadata. */
export const createElectronRoleNode = (
context : JavaScriptArtifactGraphContext ,
input : RoleNodeInput ,
) : ApplicationNode =>
context . accumulator . addNode ({
kind : input.kind ,
identity : artifactLocalIdentity (
input . anchor . sha256 ,
"electron-role" ,
2026-07-17 21:30:24 +08:00
` ${ input . kind } : ${ input . resolution . resolution_context } : ${ input . resolution . resolved_path ?? input . resolution . declared_path } ` ,
2026-07-16 05:29:40 +08:00
),
observations : [
{
2026-07-17 21:30:24 +08:00
label : input.resolution.resolved_path ?? input . resolution . declared_path ,
2026-07-16 05:29:40 +08:00
properties : {
2026-07-17 21:30:24 +08:00
declared_path : input.resolution.declared_path ,
resolution_context : input.resolution.resolution_context ,
resolved_path : input.resolution.resolved_path ,
resolution_status : input.resolution.resolution_status ,
limitations : input.resolution.limitations ,
2026-07-16 05:29:40 +08:00
mechanism : input.mechanism ,
},
evidence : staticInferenceEvidence ({
sha256 : input.anchor.sha256 ,
path : input.anchor.path ,
operation : "discover-electron-entrypoint" ,
2026-08-10 14:57:44 +08:00
coverage : input.coverage ?? completeApplicationCoverage (),
2026-07-17 21:30:24 +08:00
limitations : input.resolution.limitations ,
2026-07-16 05:29:40 +08:00
...( input . range === undefined ? {} : { range : input.range }),
}),
},
],
});
/** Resolve an Electron role to an inventoried asset when available. */
export const linkElectronRoleToAsset = (
context : JavaScriptArtifactGraphContext ,
input : RoleAssetLinkInput ,
) : void => {
2026-07-17 21:30:24 +08:00
const path = input . resolution . resolved_path ;
if ( path === null ) return ;
const asset = context . assetNodes . get ( path ) ?? context . fileNodes . get ( path );
2026-07-16 05:29:40 +08:00
if ( asset === undefined ) return ;
context . accumulator . addEdge ({
source_node_id : input.role.node_id ,
target_node_id : asset.node_id ,
relation : "maps_to" ,
2026-07-17 21:30:24 +08:00
properties : {
declared_path : input.resolution.declared_path ,
resolution_context : input.resolution.resolution_context ,
resolved_path : path ,
resolution_status : input.resolution.resolution_status ,
},
2026-07-16 05:29:40 +08:00
evidence : staticInferenceEvidence ({
sha256 : input.anchor.sha256 ,
path : input.anchor.path ,
operation : "resolve-electron-entrypoint" ,
2026-08-10 14:57:44 +08:00
coverage : input.coverage ?? completeApplicationCoverage (),
2026-07-17 21:30:24 +08:00
limitations : input.resolution.limitations ,
2026-07-16 05:29:40 +08:00
...( input . range === undefined ? {} : { range : input.range }),
}),
});
};
2026-10-08 19:01:59 -07:00
/**
* Disclose relationships omitted because they resolved to their own source;
* the application graph forbids self-referential edges.
*/
export const selfReferenceOmissions = (
count : number ,
noun : readonly [ singular : string , plural : string ],
owner : string ,
) : string [] =>
count === 0
? []
: [
` ${ String ( count ) } ${ count === 1 ? noun [ 0 ] : noun [ 1 ] } resolved back to ${ owner } itself and ${ count === 1 ? "was" : "were" } omitted; application graph edges cannot be self-referential.` ,
];
2026-07-16 05:29:40 +08:00
/** Deterministic key for one recovered module inside a bundle asset. */
2026-10-10 21:42:21 +08:00
export const moduleLookupKey = (
path : string ,
runtime : string ,
moduleKey : string ,
) : string => compositeKey ([ path , runtime , moduleKey ]);
2026-07-16 05:29:40 +08:00
2026-07-18 00:31:51 +08:00
/** Deterministic key for one recovered bundler chunk inside a bundle asset. */
export const chunkLookupKey = (
path : string ,
runtime : string ,
chunkKey : string ,
2026-10-11 00:03:20 +08:00
) : string => compositeKey ([ path , runtime , chunkKey ]);
2026-07-18 00:31:51 +08:00
2026-07-16 05:29:40 +08:00
/** Resolve a relative static specifier without escaping the artifact root. */
export const resolveArtifactPath = (
specifier : string ,
sourcePath : string ,
files : ReadonlyMap < string , JavaScriptArtifactFile >,
2026-07-17 23:57:23 +08:00
moduleKind ?: "import" | "require" ,
2026-07-17 21:30:24 +08:00
) : string | null =>
resolveArtifactPathByContext ({
declaredPath : specifier ,
sourcePath ,
context : "module-specifier" ,
files ,
2026-07-17 23:57:23 +08:00
...( moduleKind === undefined ? {} : { moduleKind }),
2026-07-17 21:30:24 +08:00
}). resolved_path ;
2026-07-16 05:29:40 +08:00
/** Locate the recovered module that owns a static finding. */
export const sourceNodeFor = (
context : JavaScriptArtifactGraphContext ,
path : string ,
moduleKey : string | null ,
2026-10-10 21:42:21 +08:00
runtime? : string ,
2026-07-16 05:29:40 +08:00
) : ApplicationNode | undefined =>
moduleKey === null
2026-07-17 23:57:23 +08:00
? context . sourceModuleNodes . get ( path )
2026-10-10 21:42:21 +08:00
: runtime === undefined
? undefined
: context . moduleNodes . get ( moduleLookupKey ( path , runtime , moduleKey ));