2026-10-09 22:45:18 +08:00
import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js" ;
2026-10-11 07:22:50 +08:00
import type { JsonValue } from "../../domain/jsonValue.js" ;
2026-10-20 17:25:19 +08:00
import {
compositeKey ,
compareUnicodeCodePoints ,
} from "../../domain/unicodeCodePointOrder.js" ;
2026-08-10 14:57:44 +08:00
import {
completeApplicationCoverage ,
partialApplicationCoverage ,
2026-10-14 08:46:50 +08:00
truncatedApplicationCoverage ,
2026-08-10 14:57:44 +08:00
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-14 08:46:50 +08:00
import type {
JavaScriptHtmlInlineScriptsObservation ,
JavaScriptModuleArtifactAnalysis ,
2026-10-16 03:46:00 +08:00
JavaScriptArtifactAnalysisScope ,
2026-10-14 08:46:50 +08:00
} from "./JavaScriptArtifactAnalysisTypes.js" ;
import type { JavaScriptUnanalyzedArtifactLookup } from "../../domain/javascript/javascriptArtifactFiles.js" ;
import { HTML_INLINE_SCRIPT_LOCATION_LIMIT } from "../../domain/javascript/htmlArtifactReferences.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 ,
2026-10-16 03:46:00 +08:00
unavailableStaticEvidence ,
2026-07-16 05:29:40 +08:00
} from "./JavaScriptArtifactGraphEvidence.js" ;
2026-07-17 21:30:24 +08:00
import {
2026-10-14 09:39:17 +11:00
artifactPathResolutionProperties ,
2026-07-17 21:30:24 +08:00
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-10-14 08:46:50 +08:00
readonly directoryPaths : ReadonlySet < string >;
2026-07-16 05:29:40 +08:00
readonly root : ApplicationNode ;
readonly filesByPath : ReadonlyMap < string , JavaScriptArtifactFile >;
2026-10-14 08:46:50 +08:00
/** Borrowed inventory lookup for present files outside the source projection. */
readonly unanalyzedFiles : JavaScriptUnanalyzedArtifactLookup | undefined ;
2026-07-16 05:29:40 +08:00
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 >;
2026-10-20 17:25:19 +08:00
/** Resolved entrypoints/imports retain unique or competing application owners. */
readonly applicationRootsBySource : Map < string , Set < string >>;
/** Incomplete package facts can hide competing launch declarations. */
readonly applicationOwnershipUnknownContainers : ReadonlySet < string >;
/** Gap origins retain possible owners separately from proven source owners. */
readonly applicationModuleGapSources : Map < string , Set < string >>;
readonly applicationPossibleRootsByContainer : Map < string , Set < string >>;
/** Sources with retained references; unchecked loads carry their own gaps. */
readonly applicationModuleReferencesRetained : ReadonlySet < string >;
2026-07-16 05:29:40 +08:00
}
2026-10-20 17:25:19 +08:00
/** Application manifests exclude dependency packages but retain nested app roots. */
export const dependencyManifest = ( path : string ) : boolean =>
path . split ( "/" ). slice ( 0 , - 1 ). includes ( "node_modules" );
/** Two distinct roots prove ambiguity; further roots cannot restore uniqueness. */
export const addApplicationRootCandidate = (
roots : Set < string >,
root : string ,
) : void => {
if ( roots . size < 2 ) roots . add ( root );
};
const applicationContainerRoot = (
fileSet : JavaScriptArtifactFileSet ,
source : Pick < JavaScriptArtifactFile , "path" | "container_sha256" >,
) : string =>
fileSet . containers . find (
({ sha256 , path }) =>
sha256 === source . container_sha256 && source . path . startsWith ( ` ${ path } /` ),
) ? . path ?? "" ;
/** Equal archive bytes at different logical paths are distinct ownership scopes. */
export const applicationContainerIdentity = (
fileSet : JavaScriptArtifactFileSet ,
source : Pick < JavaScriptArtifactFile , "path" | "container_sha256" >,
) : string =>
compositeKey ([
source . container_sha256 ,
applicationContainerRoot ( fileSet , source ),
]);
/** Select the inventory's display root; this does not establish runtime ownership. */
export const applicationManifestPath = (
context : JavaScriptArtifactGraphContext ,
) : string | undefined =>
context . analysis . packages
. map (({ path }) => path )
. filter (( path ) => {
const file = context . filesByPath . get ( path );
return file !== undefined && ! dependencyManifest ( path );
})
. sort (
( left , right ) =>
left . split ( "/" ). length - right . split ( "/" ). length ||
compareUnicodeCodePoints ( left , right ),
)[ 0 ];
/** Inspect inventoried ancestor packages; absent parsing is not absent ownership.
* Entry-bearing packages are candidates; multiple candidates stay unknown.
*/
export const applicationRootPath = (
context : JavaScriptArtifactGraphContext ,
source : JavaScriptArtifactFile ,
) : string | null => {
// Unread launch metadata can name a main in another inventoried container.
// Archive occurrence identity does not prove isolation from the Node loader.
if ( context . applicationOwnershipUnknownContainers . size > 0 ) return null ;
const entryRoots = context . applicationRootsBySource . get ( source . path );
const container = applicationContainerIdentity ( context . fileSet , source );
for ( const root of context . applicationPossibleRootsByContainer . get (
container ,
) ?? [])
if ( ! entryRoots ? . has ( root )) return null ;
if (
entryRoots === undefined &&
context . applicationModuleGapSources . has ( container )
)
return null ;
const roots = new Set < string >( entryRoots );
const directories = source . path . split ( "/" ). slice ( 0 , - 1 );
for ( let depth = directories . length ; depth >= 0 ; depth -= 1 ) {
const root = directories . slice ( 0 , depth ). join ( "/" );
const prefix = root === "" ? "" : ` ${ root } /` ;
const path = ` ${ prefix } package.json` ;
if ( dependencyManifest ( path )) continue ;
const file =
context . filesByPath . get ( path ) ?? context . unanalyzedFiles ? . get ( path );
if ( file ? . container_sha256 !== source . container_sha256 ) continue ;
const metadata = context . analysis . packages . find (
( value ) => value . path === path ,
);
if ( metadata ? . status !== "included" ) return null ;
const defaultEntry = context . filesByPath . get ( ` ${ prefix } index.js` );
if (
entryRoots === undefined &&
( metadata . main_status === "declared" ||
defaultEntry ? . container_sha256 === source . container_sha256 )
)
addApplicationRootCandidate ( roots , root );
}
if ( roots . size > 1 ) return null ;
for ( const root of roots ) return root ;
return applicationContainerRoot ( context . fileSet , source );
};
2026-07-16 05:29:40 +08:00
/** 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 {
2026-10-13 15:58:25 +08:00
readonly limitations? : readonly string [];
2026-10-14 09:47:30 +08:00
readonly confidence ?: "high" | "medium" | "low" ;
2026-07-16 05:29:40 +08:00
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 ;
}
2026-10-16 03:46:00 +08:00
/** An explicit processing/selection gap with the applicable phase's authority. */
2026-07-16 05:29:40 +08:00
export interface UnavailableParseInput {
readonly file : JavaScriptArtifactFile ;
readonly asset : ApplicationNode ;
readonly operation : string ;
2026-10-16 03:46:00 +08:00
readonly authority : JavaScriptArtifactAnalysisScope [ "authority" ];
2026-07-16 05:29:40 +08:00
readonly limitation : string ;
2026-10-11 07:22:50 +08:00
readonly failure? : JsonValue ;
readonly limits? : JavaScriptArtifactGraphCoverage [ "limits" ];
2026-10-14 08:46:50 +08:00
/** Items a named limit dropped from this gap's own record, when known. */
readonly omittedCount? : number ;
/** Structured details of the gap, such as the exact omitted locations. */
readonly details? : Readonly < Record < string , JsonValue >>;
2026-07-16 05:29:40 +08:00
}
2026-10-14 08:46:50 +08:00
/** The per-file bound on retained inline HTML script locations. */
export const HTML_INLINE_SCRIPT_LOCATION_COVERAGE_LIMIT = {
name : "html_inline_script_locations_per_file" ,
value : HTML_INLINE_SCRIPT_LOCATION_LIMIT ,
unit : "items" ,
} as const ;
/** Count inline HTML script locations dropped by the per-file bound. */
export const omittedHtmlInlineScriptLocations = (
files : readonly JavaScriptHtmlInlineScriptsObservation [],
) : number =>
files . reduce (
( omitted , { script_count , scripts }) =>
omitted + script_count - scripts . length ,
0 ,
);
2026-07-16 05:29:40 +08:00
/** 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 ,
2026-10-14 09:47:30 +08:00
...( input . confidence === undefined
? {}
: { confidence : input.confidence }),
2026-10-13 15:58:25 +08:00
...( input . limitations === undefined
? {}
: { limitations : input.limitations }),
2026-07-16 05:29:40 +08:00
}),
});
};
/** 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 : {
2026-10-14 08:46:50 +08:00
// Details come first so they cannot replace the gap's own fields.
... input . details ,
2026-07-16 05:29:40 +08:00
path : input.file.path ,
operation : input.operation ,
limitation : input.limitation ,
2026-10-11 07:22:50 +08:00
...( input . failure === undefined
? {}
: { analysis_failure : input.failure }),
2026-07-16 05:29:40 +08:00
},
2026-10-16 03:46:00 +08:00
evidence : unavailableStaticEvidence ({
authority : input.authority ,
2026-07-16 05:29:40 +08:00
sha256 : input.file.sha256 ,
path : input.file.path ,
operation : input.operation ,
2026-10-14 08:46:50 +08:00
coverage :
input.omittedCount !== undefined && input . omittedCount > 0
? truncatedApplicationCoverage (
input . limits ?? [],
input . omittedCount ,
)
: partialApplicationCoverage ( input . limits ?? [], 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 ,
2026-10-20 17:25:19 +08:00
) : ApplicationNode => {
const target =
input . resolution . resolved_path ??
( input . resolution . runtime_path_base_proven === true
? input.resolution.candidate_path
: undefined ) ??
input . resolution . declared_path ;
// Raw application-relative declarations keep their source occurrence even
// when identical source bytes appear under separate application prefixes.
const identityPath =
input . resolution . runtime_path_base === "application-root"
? compositeKey ([ input . anchor . path , target ])
: target ;
return context . accumulator . addNode ({
2026-07-16 05:29:40 +08:00
kind : input.kind ,
identity : artifactLocalIdentity (
input . anchor . sha256 ,
"electron-role" ,
2026-10-20 17:25:19 +08:00
` ${ input . kind } : ${ input . resolution . resolution_context } : ${ identityPath } ` ,
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-10-14 08:46:50 +08:00
... artifactPathResolutionProperties ( input . resolution ),
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 }),
}),
},
],
});
2026-10-20 17:25:19 +08:00
};
2026-07-16 05:29:40 +08:00
/** 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-10-13 15:58:25 +08:00
moduleKind ?: "import" | "require" | "compiler-dependent" ,
2026-10-14 08:46:50 +08:00
directoryPaths? : ReadonlySet < string >,
2026-07-17 21:30:24 +08:00
) : string | null =>
resolveArtifactPathByContext ({
declaredPath : specifier ,
sourcePath ,
context : "module-specifier" ,
files ,
2026-10-14 08:46:50 +08:00
...( directoryPaths === undefined ? {} : { directoryPaths }),
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 ));