feat(javascript): expose bounded semantic tracing

This commit is contained in:
morluto 2026-07-22 23:07:51 +00:00
parent 8678b57b5e
commit 508a9ccde9
56 changed files with 2455 additions and 307 deletions

View File

@ -380,7 +380,7 @@ REA handles the app analysis in steps 1–5. The agent performs step 6 with its
| Managed PE/CLI | 8 | PE/CLI identity, metadata members, CIL hashes, P/Invoke/native-boundary declarations and verification, application-graph projection, decompiler reconstruction import, token remapping, runtime-correlation plans, and version comparison |
| Browser observation | 8 | exact-origin CDP capture, bundle and source-map analysis, WebMCP discovery, session timelines, capture diff, and visual evidence |
| Electron analysis | 4 | passive root-confined observation, bounded static application mapping, and evidence-backed static/runtime reconciliation |
| Application workflows | 8 | bounded cross-layer traces, unique-only version matching, static export return-shape comparison, approved Linux-isolated extracted-module replay, managed-runtime characterization, and reconstruction coverage closure |
| Application workflows | 9 | bounded cross-layer traces, unique-only version matching, static export return-shape comparison, approved Linux-isolated extracted-module replay, managed-runtime characterization, and reconstruction coverage closure |
| Workspace and observation | 19 | target lifecycle, Evidence v2 bundles, direct finite replay-machine evaluation, process/artifact/function comparison, evidence-linked residual-unknown lifecycle |
The public interface describes what the agent is trying to learn. Providers decide how to answer. macOS utilities handle common semantic inspection without launching Hopper; Hopper handles deeper native analysis; the process harness implements controlled behavioral capture.

View File

@ -168,7 +168,7 @@ rea uninstall --purge-data
| Managed PE/CLI | 8 | هوية PE/CLI، أعضاء البيانات الوصفية، بصمات CIL، تصريحات P/Invoke/الحدود الأصلية والتحقق منها، إسقاط رسم التطبيق، استيراد إعادة البناء من فك الترجمة، إعادة ربط الرموز، خطط ترابط وقت التشغيل ومقارنة الإصدارات |
| مراقبة المتصفح | 8 | التقاط CDP محدود الأصل وتحليل الحزم وخرائط المصدر واكتشاف WebMCP والفروق والأدلة المرئية |
| تحليل Electron | 4 | مراقبة سلبية ضمن جذور canonical، ورسم ثابت ومحدود للتطبيق، ومطابقة ثابتة/وقت تشغيل قائمة على Evidence |
| سير عمل التطبيقات | 8 | تتبع محدود ومقارنة إصدارات فريدة ومقارنة ثابتة لأشكال قيم إرجاع الصادرات وإعادة تشغيل وحدات معزولة ومعتمدة على Linux وتوصيف وقت التشغيل المُدار وإغلاق تغطية إعادة البناء |
| سير عمل التطبيقات | 9 | تتبع محدود ومقارنة إصدارات فريدة ومقارنة ثابتة لأشكال قيم إرجاع الصادرات وإعادة تشغيل وحدات معزولة ومعتمدة على Linux وتوصيف وقت التشغيل المُدار وإغلاق تغطية إعادة البناء |
| جلسة الملف التنفيذي | 19 | فتح الأهداف وحزم الأدلة وتشغيل آلة إعادة محدودة ومقارنة العمليات والقطع والدوال وسجل المجهولات المتبقية |
## استخدام REA مع وكلاء برمجة آخرين

View File

@ -158,7 +158,7 @@ REA は手順 1〜5 のバイナリ解析を処理し、手順 6 はエージェ
| Managed PE/CLI | 8 | PE/CLI 識別、メタデータメンバー、CIL ハッシュ、P/Invoke/ネイティブ境界宣言と検証、アプリケーショングラフ投影、逆コンパイル再構成インポート、構造的 token 再マッピング、ランタイム相関計画、バージョン比較 |
| ブラウザ観察 | 8 | origin 限定 CDP 取得、bundle/source map 解析、WebMCP 検出、セッション、capture diff、視覚証拠 |
| Electron 解析 | 4 | canonical ファイルルート内の受動観察、有界な静的アプリマッピング、Evidence に基づく静的/実行時の照合 |
| アプリケーション | 8 | 有界なクロスレイヤー追跡、一意限定のバージョン比較、静的なエクスポート戻り値形状比較、承認済み Linux 隔離 extracted-module replay、マネージドランタイム特性評価、再構築カバレッジ閉包 |
| アプリケーション | 9 | 有界なクロスレイヤー追跡、一意限定のバージョン比較、静的なエクスポート戻り値形状比較、承認済み Linux 隔離 extracted-module replay、マネージドランタイム特性評価、再構築カバレッジ閉包 |
| バイナリセッション | 19 | `open_binary`、`binary_session`、証拠バンドル、有限リプレイ状態機械、プロセス/成果物/関数比較、残余未知レジストリ |
## 他のコーディングエージェントで使う

View File

@ -158,7 +158,7 @@ REA는 1–5단계의 바이너리 분석을 처리합니다. 6단계는 에이
| Managed PE/CLI | 8 | PE/CLI ID, 메타데이터 멤버, CIL 해시, P/Invoke/네이티브 경계 선언 및 검증, 애플리케이션 그래프 투영, 디컴파일 재구성 가져오기, 구조적 token 재매핑, 런타임 상관 계획, 버전 비교 |
| 브라우저 관찰 | 8 | origin 제한 CDP 캡처, bundle/source map 분석, WebMCP 검색, 세션 타임라인, capture diff 및 시각 증거 |
| Electron 분석 | 4 | canonical 파일 루트 내 수동 관찰, 제한된 정적 앱 매핑, Evidence 기반 정적/런타임 조정 |
| 애플리케이션 | 8 | 제한된 교차 계층 추적, 고유 일치 버전 비교, 정적 export 반환 형태 비교, 승인된 Linux 격리 extracted-module replay, 관리형 런타임 특성화, 재구성 커버리지 폐쇄 |
| 애플리케이션 | 9 | 제한된 교차 계층 추적, 고유 일치 버전 비교, 정적 export 반환 형태 비교, 승인된 Linux 격리 extracted-module replay, 관리형 런타임 특성화, 재구성 커버리지 폐쇄 |
| 바이너리 세션 | 19 | `open_binary`, `binary_session`, 증거 번들, 유한 replay 상태 머신, 프로세스·아티팩트·함수 비교, 잔여 미지수 레지스트리 |
## 다른 코딩 에이전트에서 사용하기

View File

@ -158,7 +158,7 @@ REA 负责第 1–5 步中的二进制分析。第 6 步由智能体使用其常
| Managed PE/CLI | 8 | PE/CLI 身份、元数据成员、CIL 哈希、P/Invoke/原生边界声明与验证、应用图谱投影、反编译重建导入、结构化 token 重映射、运行时关联计划与跨版本比较 |
| 浏览器观察 | 8 | 精确来源 CDP 抓取、bundle/source map 分析、WebMCP 发现、会话时间线、capture diff 与视觉证据 |
| Electron 分析 | 4 | 限定 canonical 文件根的被动观察、有界静态应用映射,以及基于 Evidence 的静态/运行时对账 |
| 应用工作流 | 8 | 有界跨层追踪、唯一匹配跨版本图比较、静态导出返回形状比较、经批准的 Linux 隔离 extracted-module replay、托管运行时表征和重建覆盖闭合 |
| 应用工作流 | 9 | 有界跨层追踪、唯一匹配跨版本图比较、静态导出返回形状比较、经批准的 Linux 隔离 extracted-module replay、托管运行时表征和重建覆盖闭合 |
| 二进制会话 | 19 | `open_binary`、`binary_session`、证据包、有限重放状态机、进程、制品与函数比较、残余未知项注册表 |
## 与其他编程智能体一起使用

View File

@ -2,10 +2,9 @@
REA derives a bounded feature trace from one authenticated JavaScript
Application Graph and compares two authenticated graph versions. The MCP tools
are `trace_application_feature`, `compare_application_versions`, and
`compare_javascript_export_shapes`; their CLI equivalents are
`rea trace-application-feature`, `rea compare-application-versions`, and
`rea compare-javascript-export-shapes`.
are `trace_application_feature`, `trace_javascript_semantics`,
`compare_application_versions`, and `compare_javascript_export_shapes`; their
CLI equivalents use the same names with hyphens.
Both workflows consume Evidence v2 produced by
`analyze_javascript_application` or `reconcile_javascript_runtime`. They do not
@ -14,6 +13,13 @@ native-analysis provider. Static artifact observations, passive runtime
observations, relationship inferences, and unknown or unavailable facts retain
their original graph authority.
Current `analyze_javascript_application` Evidence uses result schema v2. It
retains the structural JavaScript Application Graph and a separate semantic
relation graph bound to the same root artifact digest and structural graph ID.
Legacy v1 Evidence remains valid for structural workflows, but semantic tracing
returns an actionable request to reanalyze it rather than treating missing
semantic data as an empty graph.
## Feature tracing
Select one literal seed kind: node ID, route, string, API, IPC channel, module,
@ -29,6 +35,33 @@ matches exactly. Otherwise the result recommends provider-neutral follow-up
tools and reports `requires-provider-analysis`; it never starts or switches a
provider implicitly.
## Semantic relation tracing
`trace_javascript_semantics` queries the authenticated companion graph without
rereading mutable files. A seed may identify a semantic or application node,
literal, function fingerprint, property, endpoint, event, or boundary field.
The query declares backward provenance, forward influence, callers, or
ownership and can restrict admitted relation kinds. Node, relation, depth,
function, module, seed-match, and page limits are validated against exact
published ranges. A committed cursor returns the next deterministic relation
page only for the same graph, query, and limits.
The initial extractor covers lexical definitions and reads, closure captures,
and uniquely resolved local calls with argument-to-parameter and direct
return-to-call candidates. Dynamic calls/properties, parser recovery, and bound
frontiers stay explicit. Promise, event, timer, child-process, request,
configuration, boundary, resource, and fingerprint families are represented by
the versioned schema but report unsupported or unknown coverage until their
extractors supply facts. Missing relations in those families are never reported
as proven absence.
Semantic relations use static-inference authority. `resolved` means the static
target identity was unique within admitted analysis; it does not mean the path
executed. Candidate edges are excluded by default, require explicit opt-in, and
keep the result ambiguous. Runtime Evidence can later corroborate an exact
mapped candidate, but structural reachability, semantic influence, runtime
observation, and causal proof remain separate claims.
## Version comparison
REA pairs entities only when a tier produces one unique candidate on each side.
@ -75,8 +108,8 @@ replay recommendation; it does not execute JavaScript.
## CLI and verification
All three CLI commands accept inline JSON or a JSON file up to 64 MiB. The input is
the same object used by the corresponding MCP tool.
All four CLI commands accept inline JSON or a JSON file up to 64 MiB. The input
is the same object used by the corresponding MCP tool.
For two operator-provided directories or ASARs, run:

View File

@ -11,7 +11,7 @@
"skill_version": "23"
},
"tools": {
"total": 97,
"total": 98,
"families": [
{
"id": "direct",
@ -132,7 +132,7 @@
{
"id": "application",
"surface": "application-workflow",
"count": 8,
"count": 9,
"tools": [
"commit_reconstruction_coverage",
"compare_application_versions",
@ -141,7 +141,8 @@
"prepare_node_characterization",
"query_reconstruction_coverage",
"run_controlled_replay",
"trace_application_feature"
"trace_application_feature",
"trace_javascript_semantics"
]
},
{
@ -318,7 +319,8 @@
"prepare_node_characterization",
"query_reconstruction_coverage",
"run_controlled_replay",
"trace_application_feature"
"trace_application_feature",
"trace_javascript_semantics"
]
},
{
@ -427,7 +429,7 @@
},
{
"id": "javascript_application_analysis",
"version": 1
"version": 2
},
{
"id": "javascript_application_graph",
@ -441,6 +443,18 @@
"id": "javascript_feature_trace",
"version": 1
},
{
"id": "javascript_semantic_graph",
"version": 1
},
{
"id": "javascript_semantic_query",
"version": 1
},
{
"id": "javascript_semantic_trace",
"version": 1
},
{
"id": "managed_artifact_inspection",
"version": 1
@ -503,7 +517,7 @@
}
],
"cli": {
"primary_count": 57,
"primary_count": 58,
"commands": [
"analyze",
"analyze-javascript-application",
@ -558,6 +572,7 @@
"setup",
"trace",
"trace-application-feature",
"trace-javascript-semantics",
"uninstall",
"upgrade",
"verify-managed-native-boundaries",
@ -572,17 +587,17 @@
},
"runtime_catalog": {
"counts": {
"cli_commands": 57,
"mcp_tools": 97,
"cli_commands": 58,
"mcp_tools": 98,
"mcp_prompts": 6,
"mcp_resources": 2,
"mcp_resource_templates": 9
},
"digests": {
"tools_sha256": "073b2d48443034a05ba56b649057f9e5f0ab21afcb39be2c380e5a05bc0cecae",
"tools_sha256": "501d405efbf8fc3537d13791f87ac81f3b97f3f328b62710ca0885de7213a62f",
"prompts_sha256": "f6da3ad38a1000c11364d1710f122ac4990551f04efd8c53ffb2e819b5cf5707",
"resources_sha256": "94bebbab1a1b3cf47644c38d3de6469de568caa7bd7b842f529ead8ad320a912",
"combined_sha256": "a555f03ab0881d6e140b15d59ba8d75ba596a16a9690d26a8341d6b32395da5f"
"combined_sha256": "71de8a7174176823fd82c2666130233b5a1b4a1613583270d268774775b410bc"
}
}
}

View File

@ -1,6 +1,6 @@
{
"schema_version": 1,
"manifest_id": "ecm_a77e4a40338b12af5108a59c7323c22a0f2367ff2cd925c8c3fb4213b5eba480",
"manifest_id": "ecm_e04a25d07a540df3bec2fd5ba6d8a158e1f582dc94fc59225c7ba329cbabbbf6",
"verifier": {
"id": "rea-managed-conformance",
"version": "1"
@ -14,7 +14,7 @@
"skill_digests": [
{
"skill_id": "reverse-engineer-anything",
"sha256": "6308c7735c8b1f16f58f9d7020102fdf0a2492d3f78d979f4537d49e8e83fdc9"
"sha256": "a8ffb68f043fc9dfd0eb401caa6e548adce991d1e228b4539333cf90be17a755"
}
],
"claims": [

View File

@ -60,6 +60,9 @@ const SOURCE_PATHS = {
webScreenshot: "dist/domain/webScreenshot.js",
javascriptApplicationGraph: "dist/domain/javascriptApplicationGraph.js",
javascriptApplicationAnalysis: "dist/domain/javascriptApplicationAnalysis.js",
javascriptSemanticGraph: "dist/domain/javascriptSemanticGraph.js",
javascriptSemanticQuery: "dist/domain/javascriptSemanticQuerySchemas.js",
javascriptSemanticTrace: "dist/domain/javascriptSemanticTraceSchemas.js",
javascriptFeatureTrace: "dist/domain/javascriptFeatureTraceSchemas.js",
javascriptVersionComparison:
"dist/domain/javascriptApplicationVersionComparisonSchemas.js",

View File

@ -92,6 +92,21 @@ const durableSchemaDefinitions = (sources) => [
.applicationVersionComparisonResultSchema,
["schema_version"],
],
[
"javascript_semantic_graph",
sources.javascriptSemanticGraph.javaScriptSemanticGraphSchema,
["schema_version"],
],
[
"javascript_semantic_query",
sources.javascriptSemanticQuery.javaScriptSemanticQueryResultSchema,
["schema_version"],
],
[
"javascript_semantic_trace",
sources.javascriptSemanticTrace.javaScriptSemanticTraceResultSchema,
["schema_version"],
],
[
"javascript_feature_trace",
sources.javascriptFeatureTrace.applicationFeatureTraceResultSchema,
@ -158,7 +173,7 @@ const observationSchemaDefinitions = (sources) => [
[
"javascript_application_analysis",
sources.javascriptApplicationAnalysis
.javascriptApplicationAnalysisResultSchema,
.javaScriptApplicationAnalysisResultV2Schema,
["schema_version"],
],
[

View File

@ -3,8 +3,8 @@ name: reverse-engineer-anything
description: Reverse engineer native, managed, Electron/JavaScript, packaged, and browser applications with REA. Use shipped-artifact or approved runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis.
metadata:
version: "23"
tool_count: 97
catalog_digest: "a555f03ab0881d6e140b15d59ba8d75ba596a16a9690d26a8341d6b32395da5f"
tool_count: 98
catalog_digest: "71de8a7174176823fd82c2666130233b5a1b4a1613583270d268774775b410bc"
---
# REA

View File

@ -4,15 +4,18 @@ import {
compareApplicationVersionsRequestSchema,
compareJavaScriptExportShapesRequestSchema,
traceApplicationFeatureRequestSchema,
traceJavaScriptSemanticsRequestSchema,
type CompareApplicationVersionsRequest,
type CompareJavaScriptExportShapesRequest,
type TraceApplicationFeatureRequest,
type TraceJavaScriptSemanticsRequest,
} from "../contracts/applicationWorkflowInputContracts.js";
import { AnalysisInputError, type AnalysisError } from "../domain/errors.js";
import { projectInputIssues } from "../domain/inputIssueProjection.js";
import { compareApplicationVersionsInputSchema } from "../domain/javascriptApplicationVersionComparisonSchemas.js";
import { compareJavaScriptExportShapesInputSchema } from "../domain/javascriptExportShapeComparisonSchemas.js";
import { traceApplicationFeatureInputSchema } from "../domain/javascriptFeatureTraceSchemas.js";
import { traceJavaScriptSemanticsInputSchema } from "../domain/javascriptSemanticTraceSchemas.js";
import { err, ok, type Result } from "../domain/result.js";
import {
resolveEvidenceReferences,
@ -25,6 +28,10 @@ const APPLICATION_GRAPH_IDENTITIES = [
operation: "analyze_javascript_application",
predicate: "rea.javascript-application-analysis/v1",
},
{
operation: "analyze_javascript_application",
predicate: "rea.javascript-application-analysis/v2",
},
{
operation: "reconcile_javascript_runtime",
predicate: "rea.javascript-runtime-reconciliation/v1",
@ -36,6 +43,7 @@ const APPLICATION_GRAPH_IDENTITIES = [
] as const satisfies readonly EvidenceSemanticIdentity[];
type TraceInput = z.output<typeof traceApplicationFeatureInputSchema>;
type SemanticTraceInput = z.output<typeof traceJavaScriptSemanticsInputSchema>;
type ComparisonInput = z.output<typeof compareApplicationVersionsInputSchema>;
type ExportShapeComparisonInput = z.output<
typeof compareJavaScriptExportShapesInputSchema
@ -86,6 +94,40 @@ export const resolveTraceApplicationFeatureRequestValidated = (
: invalid("trace_application_feature", parsed.error, raw);
};
/** Parse and resolve one semantic trace adapter request. */
export const resolveTraceJavaScriptSemanticsRequest = (
input: unknown,
lookup?: EvidenceLookup,
): Result<SemanticTraceInput, AnalysisError> => {
const parsed = traceJavaScriptSemanticsRequestSchema.safeParse(input);
return parsed.success
? resolveTraceJavaScriptSemanticsRequestValidated(parsed.data, lookup)
: invalid("trace_javascript_semantics", parsed.error, input);
};
/** Resolve a semantic trace request already parsed by its public contract. */
export const resolveTraceJavaScriptSemanticsRequestValidated = (
input: TraceJavaScriptSemanticsRequest,
lookup?: EvidenceLookup,
): Result<SemanticTraceInput, AnalysisError> => {
const application =
input.application === undefined
? resolveEvidenceReferences(
lookup,
input.application_evidence_id === undefined
? []
: [input.application_evidence_id],
APPLICATION_GRAPH_IDENTITIES,
)
: ok([input.application]);
if (!application.ok) return application;
const raw = { application: application.value[0], query: input.query };
const parsed = traceJavaScriptSemanticsInputSchema.safeParse(raw);
return parsed.success
? ok(parsed.data)
: invalid("trace_javascript_semantics", parsed.error, raw);
};
/** Parse and resolve one comparison adapter request into canonical input. */
export const resolveCompareApplicationVersionsRequest = (
input: unknown,

View File

@ -23,7 +23,7 @@ export const createJavaScriptApplicationEvidence = (
},
JAVASCRIPT_APPLICATION_PROVIDER,
{
predicateType: "rea.javascript-application-analysis/v1",
predicateType: `rea.javascript-application-analysis/v${String(result.schema_version)}`,
operation: "analyze_javascript_application",
parameters: parameters(input),
result: jsonValueSchema.parse(result),

View File

@ -10,6 +10,7 @@ import {
type JavaScriptApplicationAnalysisResult,
} from "../domain/javascriptApplicationAnalysis.js";
import type { JavaScriptApplicationGraph } from "../domain/javascriptApplicationGraph.js";
import type { JavaScriptSemanticGraph } from "../domain/javascriptSemanticGraph.js";
import { javascriptRuntimeReconciliationResultSchema } from "../domain/javascriptRuntimeReconciliationSchemas.js";
import { managedApplicationGraphResultSchema } from "../domain/managedApplicationGraph.js";
@ -22,6 +23,7 @@ export interface ApplicationGraphEvidenceSource {
| "static-runtime-reconciliation"
| "managed-application";
readonly rootArtifactSha256: string;
readonly semanticGraph: JavaScriptSemanticGraph | null;
}
/** Parse and authenticate one REA-produced JavaScript Application Graph Evidence. */
@ -31,7 +33,10 @@ export const parseApplicationGraphEvidence = (
const evidence = parseEvidence(input);
if (
evidence.operation === "analyze_javascript_application" &&
evidence.predicate_type === "rea.javascript-application-analysis/v1" &&
[
"rea.javascript-application-analysis/v1",
"rea.javascript-application-analysis/v2",
].includes(evidence.predicate_type) &&
providerMatches(evidence, JAVASCRIPT_APPLICATION_PROVIDER)
)
return staticApplicationSource(evidence);
@ -65,6 +70,13 @@ const staticApplicationSource = (
const result = javascriptApplicationAnalysisResultSchema.parse(
evidence.normalized_result,
);
if (
evidence.predicate_type !==
`rea.javascript-application-analysis/v${String(result.schema_version)}`
)
throw new TypeError(
"JavaScript application Evidence predicate does not match its result schema version",
);
analyzeJavaScriptApplicationInputSchema.parse({
input_path: result.input_path,
...evidence.parameters,
@ -75,6 +87,7 @@ const staticApplicationSource = (
graph: result.graph,
kind: "static-application",
rootArtifactSha256: result.root_artifact_sha256,
semanticGraph: result.schema_version === 2 ? result.semantic_graph : null,
};
};
@ -109,6 +122,7 @@ const staticRuntimeSource = (
graph: result.graph,
kind: "static-runtime-reconciliation",
rootArtifactSha256: applicationLayer.root_artifact_sha256,
semanticGraph: null,
};
};
@ -138,6 +152,7 @@ const managedApplicationSource = (
graph: result.graph,
kind: "managed-application",
rootArtifactSha256: result.root_artifact_sha256,
semanticGraph: null,
};
};

View File

@ -96,7 +96,7 @@ export const analyzeJavaScriptApplicationValidated = async (
);
const { electron_summary: summary, ...application } = reconstructed;
const result = javascriptApplicationAnalysisResultSchema.parse({
schema_version: 1,
schema_version: 2,
...application,
summary,
limitations: reconstructed.graph.limitations,

View File

@ -6,6 +6,7 @@ import {
import type { ApplicationVersionComparisonResult } from "../domain/javascriptApplicationVersionComparisonSchemas.js";
import type { JavaScriptExportShapeComparisonResult } from "../domain/javascriptExportShapeComparisonSchemas.js";
import type { ApplicationFeatureTraceResult } from "../domain/javascriptFeatureTraceSchemas.js";
import type { JavaScriptSemanticTraceResult } from "../domain/javascriptSemanticTraceSchemas.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER } from "./InvestigationProviders.js";
@ -27,6 +28,24 @@ export const createApplicationFeatureTraceEvidence = (
evidenceLinks: result.evidence_links,
});
/** Create derived Evidence for one bounded JavaScript semantic trace. */
export const createJavaScriptSemanticTraceEvidence = (
parameters: EvidenceObservation["parameters"],
result: JavaScriptSemanticTraceResult,
): Evidence =>
createEvidence(undefined, JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER, {
predicateType: "rea.javascript-semantic-trace/v1",
operation: "trace_javascript_semantics",
parameters,
result: jsonValueSchema.parse(result),
rawResult: null,
confidence: "inferred",
authority: "analyst-inference",
environment: null,
limitations: result.limitations,
evidenceLinks: result.evidence_links,
});
/** Create derived Evidence for one tiered application version comparison. */
export const createApplicationVersionComparisonEvidence = (
parameters: EvidenceObservation["parameters"],

View File

@ -8,6 +8,7 @@ import {
} from "../artifacts/ArtifactReader.js";
import { DirectoryArtifactReader } from "../artifacts/DirectoryArtifactReader.js";
import type { JavaScriptApplicationGraph } from "../domain/javascriptApplicationGraph.js";
import type { JavaScriptSemanticGraph } from "../domain/javascriptSemanticGraph.js";
import type { ElectronBoundarySummary } from "../domain/javascriptApplicationAnalysis.js";
import { analyzeJavaScriptArtifactFiles } from "./JavaScriptArtifactAnalysis.js";
import { readJavaScriptArtifactFiles } from "./JavaScriptArtifactFiles.js";
@ -19,6 +20,7 @@ import {
} from "./JavaScriptArtifactReconstructionInput.js";
import { scanCanonicalArtifactInventory } from "./ArtifactInventory.js";
import { summarizeElectronBoundaries } from "./ElectronBoundaryAnalysis.js";
import { buildJavaScriptSemanticGraph } from "./JavaScriptSemanticGraphBuilder.js";
/** Application-layer result retaining local diagnostics outside the canonical graph. */
export interface JavaScriptArtifactReconstructionResult {
@ -28,6 +30,7 @@ export interface JavaScriptArtifactReconstructionResult {
readonly inventory_manifest_id: string;
readonly inventory_graph_sha256: string;
readonly graph: JavaScriptApplicationGraph;
readonly semantic_graph: JavaScriptSemanticGraph;
readonly electron_summary: ElectronBoundarySummary;
readonly statistics: {
readonly relevant_files: number;
@ -85,6 +88,11 @@ export const reconstructJavaScriptArtifact = async (
analysis,
input,
);
const semanticGraph = buildJavaScriptSemanticGraph({
rootArtifactSha256: snapshot.manifest.root_sha256,
applicationGraph: graph,
analysis,
});
return {
input_path: path,
format,
@ -92,6 +100,7 @@ export const reconstructJavaScriptArtifact = async (
inventory_manifest_id: snapshot.manifest.manifest_id,
inventory_graph_sha256: snapshot.manifest.graph_sha256,
graph,
semantic_graph: semanticGraph,
electron_summary: summarizeElectronBoundaries(analysis),
statistics: {
relevant_files: files.files.length,

View File

@ -0,0 +1,462 @@
import {
createJavaScriptSemanticGraph,
type JavaScriptSemanticGraph,
type JavaScriptSemanticGraphNode,
} from "../domain/javascriptSemanticGraph.js";
import {
JAVASCRIPT_SEMANTIC_RELATION_FAMILIES,
JAVASCRIPT_SEMANTIC_RELATION_FAMILY,
} from "../domain/javascriptSemanticGraphSchemas.js";
import type {
JavaScriptSemanticCallArgument,
JavaScriptSemanticIr,
} from "../domain/javascriptSemanticIr.js";
import type { JavaScriptApplicationGraph } from "../domain/javascriptApplicationGraph.js";
import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js";
import { inferredSemanticEvidenceAt } from "./JavaScriptSemanticGraphEvidence.js";
import {
projectSemanticClosureCaptures,
projectSemanticFrontiers,
projectSemanticReturnValues,
semanticRecoveryLimits,
type SemanticFlowProjectionContext,
} from "./JavaScriptSemanticGraphFlowProjection.js";
import {
owningSemanticCallableNode,
semanticFamilyStatus,
semanticNodesWithinRange,
semanticRangesEqual as rangesEqual,
} from "./JavaScriptSemanticGraphProjection.js";
import {
addSemanticFallbackRoot as addFallbackRoot,
addSemanticGraphNode as addNode,
addSemanticGraphRelation as addRelation,
constructSemanticGraphNode as semanticNode,
createSemanticGraphProjectionState as emptyState,
MAX_APPLICATION_NODE_IDS_PER_SEMANTIC_NODE,
MAX_SEMANTIC_GRAPH_NODES,
MAX_SEMANTIC_GRAPH_RELATIONS,
MAX_SEMANTIC_GRAPH_UNKNOWNS,
type SemanticGraphProjectionState as BuilderState,
} from "./JavaScriptSemanticGraphConstruction.js";
interface BuilderInput {
readonly rootArtifactSha256: string;
readonly applicationGraph: Pick<
JavaScriptApplicationGraph,
"graph_id" | "nodes"
>;
readonly analysis: JavaScriptArtifactAnalysis;
}
interface FileContext extends SemanticFlowProjectionContext {
readonly file: JavaScriptArtifactFile;
readonly ir: JavaScriptSemanticIr;
readonly state: BuilderState;
readonly moduleNode: JavaScriptSemanticGraphNode;
readonly bindingNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>;
readonly callableNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>;
readonly callSiteNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>;
readonly returnSiteNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>;
readonly argumentNodes: Map<string, JavaScriptSemanticGraphNode>;
readonly referenceNodes: JavaScriptSemanticGraphNode[];
readonly callResolutions: ReadonlyMap<string, "candidate" | "resolved">;
}
/** Project bounded per-file semantic IR into an artifact-bound companion graph. */
export const buildJavaScriptSemanticGraph = ({
rootArtifactSha256,
applicationGraph,
analysis,
}: BuilderInput): JavaScriptSemanticGraph => {
const state = emptyState(applicationGraph);
for (const analyzed of analysis.files) {
if (analyzed.semantic === null) continue;
projectFile(analyzed.file, analyzed.semantic.ir, state);
}
if (state.roots.size === 0) addFallbackRoot(rootArtifactSha256, state);
const semanticTruncated = analysis.files.some(
({ semantic }) => semantic?.ir.coverage.status === "truncated",
);
const truncated = state.limitsReached.size > 0 || semanticTruncated;
const unknowns = [...state.unknowns.values()];
return createJavaScriptSemanticGraph({
schema: "JavaScriptSemanticRelationGraph",
schema_version: 1,
root_artifact_sha256: rootArtifactSha256,
application_graph_id: applicationGraph.graph_id,
root_node_ids: [...state.roots],
nodes: [...state.nodes.values()],
relations: [...state.relations.values()],
fingerprints: [],
unknowns,
coverage: {
status: truncated ? "partial" : "unknown",
truncated,
omitted_nodes: truncated ? null : 0,
omitted_relations: truncated ? null : 0,
limits: [
{
name: "max_nodes",
value: MAX_SEMANTIC_GRAPH_NODES,
unit: "items",
},
{
name: "max_relations",
value: MAX_SEMANTIC_GRAPH_RELATIONS,
unit: "items",
},
{
name: "max_unknowns",
value: MAX_SEMANTIC_GRAPH_UNKNOWNS,
unit: "items",
},
{
name: "max_application_node_ids_per_semantic_node",
value: MAX_APPLICATION_NODE_IDS_PER_SEMANTIC_NODE,
unit: "items",
},
...semanticRecoveryLimits(analysis),
],
families: JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.map((family) => ({
family,
status: semanticFamilyStatus(family, analysis, truncated),
retained_relations: [...state.relations.values()].filter(
(relation) =>
JAVASCRIPT_SEMANTIC_RELATION_FAMILY[relation.relation] === family,
).length,
omitted_relations: truncated ? null : 0,
unknown_ids: unknowns
.filter((unknown) => unknown.family === family)
.map(({ unknown_id: identifier }) => identifier),
})),
},
limitations: [
"The semantic graph contains static syntax observations and conservative relationship candidates; it does not claim runtime execution.",
"Local data flow does not claim control-flow-sensitive reaching definitions or arbitrary dynamic property resolution.",
"Function fingerprints and promise, event, process, request, boundary, timer, configuration, and resource extraction are not available in this graph version's current extractor coverage.",
...(truncated
? [
"Semantic graph projection reached one or more explicit hard limits.",
]
: []),
],
});
};
const projectFile = (
file: JavaScriptArtifactFile,
ir: JavaScriptSemanticIr,
state: BuilderState,
): void => {
const moduleNode = addNode(
state,
semanticNode(
file,
{
kind: "module",
roleKey: "module",
location: null,
label: file.path,
functionNodeId: null,
},
state,
),
);
if (moduleNode === null) return;
state.roots.add(moduleNode.node_id);
const callableNodes = new Map(
ir.callables.flatMap((callable) => {
const node = addNode(
state,
semanticNode(
file,
{
kind: "function",
roleKey: `callable:${callable.callableId}`,
location: callable.location,
label: callable.name,
functionNodeId: null,
},
state,
),
);
return node === null ? [] : [[callable.callableId, node] as const];
}),
);
const bindingNodes = new Map(
ir.bindings.flatMap((binding) => {
const location = binding.definitions[0]?.location ?? null;
const kind = binding.kind === "parameter" ? "parameter" : "binding";
const owner = owningSemanticCallableNode(location, ir, callableNodes);
const node = addNode(
state,
semanticNode(
file,
{
kind,
roleKey: `binding:${binding.bindingId}`,
location,
label: binding.name,
functionNodeId: owner?.node_id ?? null,
},
state,
),
);
return node === null ? [] : [[binding.bindingId, node] as const];
}),
);
const returnSiteNodes = createReturnSiteNodes(file, ir, callableNodes, state);
const callSiteNodes = createCallSiteNodes(file, ir, callableNodes, state);
const context: FileContext = {
file,
ir,
state,
moduleNode,
bindingNodes,
callableNodes,
callSiteNodes,
returnSiteNodes,
argumentNodes: new Map(),
referenceNodes: [],
callResolutions: new Map(
ir.callSites.map((call) => [
call.callSiteId,
call.resolution === "exact" ? "resolved" : "candidate",
]),
),
};
projectDefinitionsAndReferences(context);
projectCalls(context);
projectSemanticClosureCaptures(context);
projectSemanticFrontiers(context);
};
const createReturnSiteNodes = (
file: JavaScriptArtifactFile,
ir: JavaScriptSemanticIr,
callables: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
state: BuilderState,
): Map<string, JavaScriptSemanticGraphNode> => {
const result = new Map<string, JavaScriptSemanticGraphNode>();
for (const callable of ir.callables) {
const owner = callables.get(callable.callableId);
if (owner === undefined) continue;
for (const site of callable.returnSites) {
const identifier = site.returnSiteId;
const node = addNode(
state,
semanticNode(
file,
{
kind: "return-site",
roleKey: identifier,
location: site.location,
label: "return",
functionNodeId: owner.node_id,
},
state,
),
);
if (node !== null) result.set(identifier, node);
}
}
return result;
};
const createCallSiteNodes = (
file: JavaScriptArtifactFile,
ir: JavaScriptSemanticIr,
callables: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
state: BuilderState,
): Map<string, JavaScriptSemanticGraphNode> => {
const result = new Map<string, JavaScriptSemanticGraphNode>();
for (const call of ir.callSites) {
const owner =
call.callerCallableId === null
? null
: (callables.get(call.callerCallableId)?.node_id ?? null);
const node = addNode(
state,
semanticNode(
file,
{
kind: "call-site",
roleKey: `call:${call.callSiteId}`,
location: call.location,
label: call.kind,
functionNodeId: owner,
},
state,
),
);
if (node !== null) result.set(call.callSiteId, node);
}
return result;
};
const projectDefinitionsAndReferences = (context: FileContext): void => {
for (const binding of context.ir.bindings) {
const bindingNode = context.bindingNodes.get(binding.bindingId);
if (bindingNode === undefined) continue;
for (const [index, definition] of binding.definitions.entries()) {
const definitionNode = addNode(
context.state,
semanticNode(
context.file,
{
kind: "expression",
roleKey: `definition:${binding.bindingId}:${String(index)}`,
location: definition.location,
label: definition.kind,
functionNodeId: bindingNode.function_node_id,
},
context.state,
),
);
addRelation(context.state, {
source: definitionNode,
target: bindingNode,
relation: "defines",
resolution: "resolved",
});
}
}
for (const [index, reference] of context.ir.references.entries()) {
const expression = addNode(
context.state,
semanticNode(
context.file,
{
kind: "expression",
roleKey: `reference:${String(index)}:${reference.role}:${reference.name}`,
location: reference.location,
label: reference.name,
functionNodeId:
owningSemanticCallableNode(
reference.location,
context.ir,
context.callableNodes,
)?.node_id ?? null,
},
context.state,
),
);
const binding =
reference.bindingId === null
? undefined
: context.bindingNodes.get(reference.bindingId);
if (expression !== null) context.referenceNodes.push(expression);
if (binding === undefined || expression === null) continue;
if (reference.role === "read")
addRelation(context.state, {
source: binding,
target: expression,
relation: "reads",
resolution: "resolved",
evidence: inferredSemanticEvidenceAt(context.file, reference.location),
});
else
addRelation(context.state, {
source: expression,
target: binding,
relation: "writes",
resolution: "resolved",
});
}
};
const projectCalls = (context: FileContext): void => {
for (const call of context.ir.callSites) {
const callNode = context.callSiteNodes.get(call.callSiteId);
if (callNode === undefined) continue;
for (const callableId of call.calleeCallableIds) {
const callable = context.callableNodes.get(callableId);
addRelation(context.state, {
source: callNode,
target: callable,
relation: "calls",
resolution: call.resolution === "exact" ? "resolved" : "candidate",
});
}
for (const argument of call.arguments) {
const argumentNode = createArgumentNode(
context,
callNode,
call.callSiteId,
argument,
);
if (argumentNode !== null)
context.argumentNodes.set(
`${call.callSiteId}\u0000${String(argument.index)}`,
argumentNode,
);
const references = semanticNodesWithinRange(
context.referenceNodes,
argument.location,
);
for (const reference of references)
addRelation(context.state, {
source: reference,
target: argumentNode,
relation: "aliases",
resolution:
references.length === 1 &&
rangesEqual(reference.identity.source_range, argument.location)
? "resolved"
: "candidate",
});
}
}
for (const flow of context.ir.argumentFlows) {
const argument = context.argumentNodes.get(
`${flow.callSiteId}\u0000${String(flow.argumentIndex)}`,
);
const parameter = context.bindingNodes.get(flow.parameterBindingId);
addRelation(context.state, {
source: argument,
target: parameter,
relation: "argument-to-parameter",
resolution: context.callResolutions.get(flow.callSiteId) ?? "candidate",
});
}
for (const flow of context.ir.callReturnFlows)
addRelation(context.state, {
source: context.returnSiteNodes.get(flow.returnSiteId),
target: context.callSiteNodes.get(flow.callSiteId),
relation: "returns-to-call",
resolution: context.callResolutions.get(flow.callSiteId) ?? "candidate",
});
for (const flow of context.ir.callResultFlows)
addRelation(context.state, {
source: context.callSiteNodes.get(flow.callSiteId),
target: context.bindingNodes.get(flow.bindingId),
relation: "aliases",
resolution: context.callResolutions.get(flow.callSiteId) ?? "candidate",
});
projectSemanticReturnValues(context);
};
const createArgumentNode = (
context: FileContext,
callNode: JavaScriptSemanticGraphNode,
callSiteId: string,
argument: JavaScriptSemanticCallArgument,
): JavaScriptSemanticGraphNode | null =>
addNode(
context.state,
semanticNode(
context.file,
{
kind: "expression",
roleKey: `argument:${callSiteId}:${String(argument.index)}`,
location: argument.location,
label: argument.spread
? "spread argument"
: `argument ${String(argument.index)}`,
functionNodeId: callNode.function_node_id,
},
context.state,
),
);

View File

@ -0,0 +1,256 @@
import type { JavaScriptApplicationGraph } from "../domain/javascriptApplicationGraph.js";
import {
createJavaScriptSemanticGraphNode,
createJavaScriptSemanticGraphRelation,
type JavaScriptSemanticGraphNode,
} from "../domain/javascriptSemanticGraph.js";
import type { ApplicationGraphEvidence } from "../domain/javascriptApplicationEvidenceSchemas.js";
import type {
JavaScriptSemanticGraphRelation,
JavaScriptSemanticGraphUnknown,
} from "../domain/javascriptSemanticGraphSchemas.js";
import type { JavaScriptSourceRange } from "../domain/javascriptStaticAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js";
import {
inferredSemanticEvidence,
observedSemanticEvidence,
unavailableSemanticRootEvidence,
} from "./JavaScriptSemanticGraphEvidence.js";
/** Hard semantic graph node bound enforced during artifact projection. */
export const MAX_SEMANTIC_GRAPH_NODES = 100_000;
/** Hard semantic graph relation bound enforced during artifact projection. */
export const MAX_SEMANTIC_GRAPH_RELATIONS = 200_000;
/** Hard unresolved-frontier bound enforced across all analyzed files. */
export const MAX_SEMANTIC_GRAPH_UNKNOWNS = 100_000;
/** Maximum structural nodes linked to one exact semantic location. */
export const MAX_APPLICATION_NODE_IDS_PER_SEMANTIC_NODE = 64;
type SemanticGraphProjectionLimit =
| "max_application_node_ids_per_semantic_node"
| "max_nodes"
| "max_relations"
| "max_unknowns";
/** Mutable local projection state hidden from graph callers. */
export interface SemanticGraphProjectionState {
readonly nodes: Map<string, JavaScriptSemanticGraphNode>;
readonly relations: Map<string, JavaScriptSemanticGraphRelation>;
readonly unknowns: Map<string, JavaScriptSemanticGraphUnknown>;
readonly roots: Set<string>;
readonly limitsReached: Set<SemanticGraphProjectionLimit>;
readonly applicationNodeIdsByLocation: ReadonlyMap<string, readonly string[]>;
}
/** Input for one exact artifact-version semantic node. */
export interface SemanticNodeConstructionInput {
readonly kind: JavaScriptSemanticGraphNode["kind"];
readonly roleKey: string;
readonly location: JavaScriptSourceRange | null;
readonly label: string | null;
readonly functionNodeId: string | null;
}
/** Input for one directed static semantic relationship. */
export interface SemanticRelationConstructionInput {
readonly source: JavaScriptSemanticGraphNode | undefined | null;
readonly target: JavaScriptSemanticGraphNode | undefined | null;
readonly relation: JavaScriptSemanticGraphRelation["relation"];
readonly resolution?: JavaScriptSemanticGraphRelation["resolution"];
readonly evidence?: ApplicationGraphEvidence;
}
/** Create empty projection state with structural application-node mappings. */
export const createSemanticGraphProjectionState = (
applicationGraph: Pick<JavaScriptApplicationGraph, "nodes">,
): SemanticGraphProjectionState => {
const index = indexApplicationNodes(applicationGraph.nodes);
return {
nodes: new Map(),
relations: new Map(),
unknowns: new Map(),
roots: new Set(),
limitsReached: new Set(
index.truncated ? ["max_application_node_ids_per_semantic_node"] : [],
),
applicationNodeIdsByLocation: index.identifiers,
};
};
/** Construct one canonical semantic node backed by an exact artifact file. */
export const constructSemanticGraphNode = (
file: JavaScriptArtifactFile,
input: SemanticNodeConstructionInput,
state: SemanticGraphProjectionState,
): JavaScriptSemanticGraphNode =>
createJavaScriptSemanticGraphNode({
kind: input.kind,
identity: {
artifact_sha256: file.sha256,
module_path: file.path,
source_range: input.location,
role_key: input.roleKey,
},
function_node_id: input.functionNodeId,
application_node_ids: matchingApplicationNodeIds(file, input, state),
label: input.label,
properties: {},
evidence: observedSemanticEvidence(file, input.location),
});
/** Retain one canonical node unless the hard graph bound was reached. */
export const addSemanticGraphNode = (
state: SemanticGraphProjectionState,
node: JavaScriptSemanticGraphNode,
): JavaScriptSemanticGraphNode | null => {
const existing = state.nodes.get(node.node_id);
if (existing !== undefined) return existing;
if (state.nodes.size >= MAX_SEMANTIC_GRAPH_NODES) {
state.limitsReached.add("max_nodes");
return null;
}
state.nodes.set(node.node_id, node);
return node;
};
/** Retain one non-self semantic relationship unless its hard bound was reached. */
export const addSemanticGraphRelation = (
state: SemanticGraphProjectionState,
input: SemanticRelationConstructionInput,
): void => {
if (
input.source === undefined ||
input.source === null ||
input.target === undefined ||
input.target === null ||
input.source.node_id === input.target.node_id
)
return;
const value = createJavaScriptSemanticGraphRelation({
source_node_id: input.source.node_id,
target_node_id: input.target.node_id,
relation: input.relation,
resolution: input.resolution ?? "candidate",
properties: {},
evidence: input.evidence ?? inferredSemanticEvidence(input.source),
});
if (state.relations.has(value.relation_id)) return;
if (state.relations.size >= MAX_SEMANTIC_GRAPH_RELATIONS) {
state.limitsReached.add("max_relations");
return;
}
state.relations.set(value.relation_id, value);
};
/** Retain one unresolved frontier unless the global graph bound was reached. */
export const addSemanticGraphUnknown = (
state: SemanticGraphProjectionState,
unknown: JavaScriptSemanticGraphUnknown,
): void => {
if (state.unknowns.has(unknown.unknown_id)) return;
if (state.unknowns.size >= MAX_SEMANTIC_GRAPH_UNKNOWNS) {
state.limitsReached.add("max_unknowns");
return;
}
state.unknowns.set(unknown.unknown_id, unknown);
};
/** Add a truthful root when no source file could produce semantic IR. */
export const addSemanticFallbackRoot = (
rootArtifactSha256: string,
state: SemanticGraphProjectionState,
): void => {
const node = addSemanticGraphNode(
state,
createJavaScriptSemanticGraphNode({
kind: "module",
identity: {
artifact_sha256: rootArtifactSha256,
module_path: "unknown-semantic-root",
source_range: null,
role_key: "artifact-root",
},
function_node_id: null,
application_node_ids: [],
label: "unavailable semantic root",
properties: {},
evidence: unavailableSemanticRootEvidence(rootArtifactSha256),
}),
);
if (node !== null) state.roots.add(node.node_id);
};
const indexApplicationNodes = (
nodes: JavaScriptApplicationGraph["nodes"],
): {
readonly identifiers: ReadonlyMap<string, readonly string[]>;
readonly truncated: boolean;
} => {
const identifiers = new Map<string, Set<string>>();
for (const node of nodes) {
for (const observation of node.observations) {
const { artifact, location } = observation.evidence;
if (!artifact.available || !location.available) continue;
if (location.value.kind === "artifact-path") {
addApplicationNodeIdentifier(
identifiers,
applicationLocationKey(artifact.sha256, location.value.path, null),
node.node_id,
);
}
if (location.value.kind === "source-range") {
addApplicationNodeIdentifier(
identifiers,
applicationLocationKey(artifact.sha256, location.value.source, {
start: location.value.start,
end: location.value.end,
}),
node.node_id,
);
}
}
}
let truncated = false;
const indexed = new Map(
[...identifiers].map(([key, values]) => {
const sorted = [...values].sort();
if (sorted.length > MAX_APPLICATION_NODE_IDS_PER_SEMANTIC_NODE)
truncated = true;
return [key, sorted.slice(0, MAX_APPLICATION_NODE_IDS_PER_SEMANTIC_NODE)];
}),
);
return { identifiers: indexed, truncated };
};
const matchingApplicationNodeIds = (
file: JavaScriptArtifactFile,
input: SemanticNodeConstructionInput,
state: SemanticGraphProjectionState,
): string[] => [
...(state.applicationNodeIdsByLocation.get(
applicationLocationKey(file.sha256, file.path, input.location),
) ?? []),
];
const addApplicationNodeIdentifier = (
identifiers: Map<string, Set<string>>,
key: string,
nodeId: string,
): void => {
const values = identifiers.get(key) ?? new Set<string>();
values.add(nodeId);
identifiers.set(key, values);
};
const applicationLocationKey = (
artifactSha256: string,
path: string,
range: JavaScriptSourceRange | null,
): string =>
[
artifactSha256,
path,
range === null
? "artifact-path"
: `${range.start.line}:${range.start.column}-${range.end.line}:${range.end.column}`,
].join("\u0000");

View File

@ -0,0 +1,111 @@
import type { ApplicationGraphEvidence } from "../domain/javascriptApplicationEvidenceSchemas.js";
import type { JavaScriptSemanticGraphNode } from "../domain/javascriptSemanticGraph.js";
import type { JavaScriptSourceRange } from "../domain/javascriptStaticAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js";
const INFERENCE_LIMITATION =
"This relationship is a bounded static candidate; it does not prove runtime execution or causal flow.";
/** Exact artifact-backed syntax evidence for one semantic graph node. */
export const observedSemanticEvidence = (
file: JavaScriptArtifactFile,
location: JavaScriptSourceRange | null,
): ApplicationGraphEvidence => ({
authority: "ast-static-analysis",
state: "observed",
confidence: "exact",
artifact: {
available: true,
artifact_id: `art_${file.sha256}`,
sha256: file.sha256,
},
location:
location === null
? { available: true, value: { kind: "artifact-path", path: file.path } }
: {
available: true,
value: { kind: "source-range", source: file.path, ...location },
},
extractor: {
name: "rea-javascript-semantic-analysis",
version: "1",
operation: "analyze_javascript_application",
executable_sha256: null,
},
coverage: {
status: "complete",
truncated: false,
omitted_count: 0,
limits: [],
},
limitations: [],
evidence_ids: [],
});
/** Conservative static relationship evidence derived from one syntax node. */
export const inferredSemanticEvidence = (
source: JavaScriptSemanticGraphNode,
): ApplicationGraphEvidence => ({
...source.evidence,
authority: "static-relationship-inference",
state: "inferred",
confidence: "high",
limitations: [INFERENCE_LIMITATION],
});
/** Static relationship evidence anchored at the syntax that proves the link. */
export const inferredSemanticEvidenceAt = (
file: JavaScriptArtifactFile,
location: JavaScriptSourceRange,
): ApplicationGraphEvidence => ({
...observedSemanticEvidence(file, location),
authority: "static-relationship-inference",
state: "inferred",
confidence: "high",
limitations: [INFERENCE_LIMITATION],
});
/** Explicit absence of semantic syntax for an artifact-level fallback root. */
export const unavailableSemanticRootEvidence = (
rootArtifactSha256: string,
): ApplicationGraphEvidence => ({
authority: "unknown",
state: "unavailable",
confidence: "unknown",
artifact: {
available: true,
artifact_id: `art_${rootArtifactSha256}`,
sha256: rootArtifactSha256,
},
location: {
available: false,
reason: "not-observed",
detail: "No admitted JavaScript source produced semantic IR.",
},
extractor: {
name: "rea-javascript-semantic-analysis",
version: "1",
operation: "analyze_javascript_application",
executable_sha256: null,
},
coverage: {
status: "unavailable",
truncated: false,
omitted_count: null,
limits: [],
},
limitations: ["This synthetic graph root does not claim a source location."],
evidence_ids: [],
});
/** Explicit unknown evidence at one unresolved dynamic syntax location. */
export const unknownSemanticEvidence = (
file: JavaScriptArtifactFile,
location: JavaScriptSourceRange,
): ApplicationGraphEvidence => ({
...observedSemanticEvidence(file, location),
authority: "unknown",
state: "unknown",
confidence: "unknown",
limitations: ["Dynamic syntax prevents an exact static relationship."],
});

View File

@ -0,0 +1,127 @@
import { createJavaScriptSemanticGraphUnknown } from "../domain/javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphNode } from "../domain/javascriptSemanticGraph.js";
import type { JavaScriptSemanticIr } from "../domain/javascriptSemanticIr.js";
import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js";
import {
addSemanticGraphRelation,
addSemanticGraphUnknown,
type SemanticGraphProjectionState,
} from "./JavaScriptSemanticGraphConstruction.js";
import {
inferredSemanticEvidenceAt,
unknownSemanticEvidence,
} from "./JavaScriptSemanticGraphEvidence.js";
import { semanticNodesWithinRange } from "./JavaScriptSemanticGraphProjection.js";
/** File-local graph state needed by return, capture, and frontier projection. */
export interface SemanticFlowProjectionContext {
readonly file: JavaScriptArtifactFile;
readonly ir: JavaScriptSemanticIr;
readonly state: SemanticGraphProjectionState;
readonly moduleNode: JavaScriptSemanticGraphNode;
readonly bindingNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>;
readonly callableNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>;
readonly returnSiteNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>;
readonly referenceNodes: readonly JavaScriptSemanticGraphNode[];
}
/** Link direct return expressions to their retained semantic references. */
export const projectSemanticReturnValues = (
context: SemanticFlowProjectionContext,
): void => {
for (const callable of context.ir.callables)
for (const site of callable.returnSites) {
const returnNode = context.returnSiteNodes.get(site.returnSiteId);
const references = semanticNodesWithinRange(
context.referenceNodes,
site.location,
);
for (const reference of references)
addSemanticGraphRelation(context.state, {
source: reference,
target: returnNode,
relation: "aliases",
resolution:
site.identityReferenceLocation !== null &&
rangesEqual(
reference.identity.source_range,
site.identityReferenceLocation,
)
? "resolved"
: "candidate",
});
}
};
const rangesEqual = (
left: JavaScriptSemanticGraphNode["identity"]["source_range"],
right: JavaScriptSemanticGraphNode["identity"]["source_range"],
): boolean =>
left !== null &&
right !== null &&
left.start.line === right.start.line &&
left.start.column === right.start.column &&
left.end.line === right.end.line &&
left.end.column === right.end.column;
/** Link each captured binding to the callable at the exact reference site. */
export const projectSemanticClosureCaptures = (
context: SemanticFlowProjectionContext,
): void => {
for (const capture of context.ir.closureCaptures)
addSemanticGraphRelation(context.state, {
source: context.bindingNodes.get(capture.bindingId),
target: context.callableNodes.get(capture.callableId),
relation: "captures",
resolution: "resolved",
evidence: inferredSemanticEvidenceAt(
context.file,
capture.referenceLocation,
),
});
};
/** Retain bounded unresolved dynamic-call and dynamic-property frontiers. */
export const projectSemanticFrontiers = (
context: SemanticFlowProjectionContext,
): void => {
for (const frontier of context.ir.frontiers) {
const unknown = createJavaScriptSemanticGraphUnknown({
node_id:
frontier.callableId === null
? context.moduleNode.node_id
: (context.callableNodes.get(frontier.callableId)?.node_id ??
context.moduleNode.node_id),
family: frontier.kind === "dynamic-call" ? "call-flow" : "object-flow",
relation_kinds:
frontier.kind === "dynamic-call"
? ["calls"]
: ["reads-property", "writes-property"],
reason: frontier.kind,
detail: frontier.reason,
candidate_node_ids: [],
evidence: unknownSemanticEvidence(context.file, frontier.location),
});
addSemanticGraphUnknown(context.state, unknown);
}
};
/** Publish exact semantic analyzer limits reached by any admitted source file. */
export const semanticRecoveryLimits = (
analysis: JavaScriptArtifactAnalysis,
): {
readonly name: string;
readonly value: number;
readonly unit: "items";
}[] => {
const limits = new Map<string, number>();
for (const { semantic } of analysis.files) {
if (semantic === null) continue;
for (const name of semantic.ir.coverage.limitsReached)
limits.set(`semantic.${name}`, semantic.limits[name]);
}
return [...limits]
.sort(([left], [right]) => left.localeCompare(right))
.map(([name, value]) => ({ name, value, unit: "items" }));
};

View File

@ -0,0 +1,74 @@
import { compareCodePoints } from "../domain/javascriptApplicationGraph.js";
import type { JavaScriptSemanticGraphNode } from "../domain/javascriptSemanticGraph.js";
import { JAVASCRIPT_SEMANTIC_RELATION_FAMILIES } from "../domain/javascriptSemanticGraphSchemas.js";
import type { JavaScriptSemanticIr } from "../domain/javascriptSemanticIr.js";
import type { JavaScriptSourceRange } from "../domain/javascriptStaticAnalysisTypes.js";
import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
/** Find the innermost callable that contains one exact source range. */
export const owningSemanticCallableNode = (
location: JavaScriptSourceRange | null,
ir: JavaScriptSemanticIr,
nodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
): JavaScriptSemanticGraphNode | undefined => {
if (location === null) return undefined;
return ir.callables
.filter((callable) => contains(callable.location, location))
.sort((left, right) =>
compareCodePoints(
positionKey(right.location),
positionKey(left.location),
),
)
.map(({ callableId }) => nodes.get(callableId))
.find((node) => node !== undefined);
};
/** Find retained semantic nodes whose exact ranges are within one expression. */
export const semanticNodesWithinRange = (
nodes: readonly JavaScriptSemanticGraphNode[],
range: JavaScriptSourceRange,
): JavaScriptSemanticGraphNode[] =>
nodes.filter(
(node) =>
node.identity.source_range !== null &&
contains(range, node.identity.source_range),
);
/** Report extractor support without treating missing families as absence. */
export const semanticFamilyStatus = (
family: (typeof JAVASCRIPT_SEMANTIC_RELATION_FAMILIES)[number],
analysis: JavaScriptArtifactAnalysis,
truncated: boolean,
): "complete" | "partial" | "unknown" | "unsupported" => {
if (!["call-flow", "closure", "data-flow", "object-flow"].includes(family))
return "unsupported";
if (truncated) return "unknown";
return analysis.truncated_scopes === 0 ? "partial" : "unknown";
};
/** Compare two exact semantic source ranges. */
export const semanticRangesEqual = (
left: JavaScriptSourceRange | null,
right: JavaScriptSourceRange,
): boolean =>
left !== null &&
left.start.line === right.start.line &&
left.start.column === right.start.column &&
left.end.line === right.end.line &&
left.end.column === right.end.column;
const contains = (
outer: JavaScriptSourceRange,
inner: JavaScriptSourceRange,
): boolean =>
pointCompare(outer.start, inner.start) <= 0 &&
pointCompare(outer.end, inner.end) >= 0;
const pointCompare = (
left: JavaScriptSourceRange["start"],
right: JavaScriptSourceRange["start"],
): number => left.line - right.line || left.column - right.column;
const positionKey = (range: JavaScriptSourceRange): string =>
`${String(range.start.line).padStart(12, "0")}:${String(range.start.column).padStart(12, "0")}`;

View File

@ -0,0 +1,72 @@
import { z } from "zod";
import {
AnalysisInputError,
AnalysisProtocolError,
type AnalysisError,
} from "../domain/errors.js";
import type { Evidence } from "../domain/evidence.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { queryJavaScriptSemanticGraph } from "../domain/javascriptSemanticQuery.js";
import { JavaScriptSemanticQueryCursorError } from "../domain/javascriptSemanticQueryIdentity.js";
import {
javaScriptSemanticTraceResultSchema,
traceJavaScriptSemanticsInputSchema,
} from "../domain/javascriptSemanticTraceSchemas.js";
import { err, ok, type Result } from "../domain/result.js";
import { parseApplicationGraphEvidence } from "./JavaScriptApplicationEvidenceGraph.js";
import { createJavaScriptSemanticTraceEvidence } from "./JavaScriptApplicationWorkflowEvidence.js";
const OPERATION = "trace_javascript_semantics" as const;
/** Trace semantic relations from input already parsed by a trusted adapter. */
export const traceJavaScriptSemanticsEvidenceValidated = (
input: z.output<typeof traceJavaScriptSemanticsInputSchema>,
): Result<Evidence, AnalysisError> => {
try {
const source = parseApplicationGraphEvidence(input.application);
if (source.semanticGraph === null)
return err(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["application"],
reason: "invalid_value",
expected:
"analyze_javascript_application v2 Evidence; reanalyze the artifact with the current REA version",
},
]),
);
const query = queryJavaScriptSemanticGraph(
source.semanticGraph,
input.query,
);
const result = javaScriptSemanticTraceResultSchema.parse({
...query,
source_evidence_id: source.evidence.evidence_id,
evidence_links: [source.evidence.evidence_id],
});
return ok(
createJavaScriptSemanticTraceEvidence(
{
application_evidence_id: source.evidence.evidence_id,
query: jsonValueSchema.parse(input.query),
},
result,
),
);
} catch (cause: unknown) {
if (cause instanceof JavaScriptSemanticQueryCursorError)
return err(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["query", "cursor"],
reason: "invalid_value",
expected: "a cursor returned by the same graph, query, and limits",
},
]),
);
return err(
new AnalysisProtocolError("JavaScript semantic trace failed", { cause }),
);
}
};

View File

@ -5,10 +5,12 @@ import {
compareJavaScriptExportShapesEvidenceValidated,
traceApplicationFeatureEvidenceValidated,
} from "./application/JavaScriptApplicationWorkflowService.js";
import { traceJavaScriptSemanticsEvidenceValidated } from "./application/JavaScriptSemanticTraceService.js";
import {
resolveCompareApplicationVersionsRequest,
resolveCompareJavaScriptExportShapesRequest,
resolveTraceApplicationFeatureRequest,
resolveTraceJavaScriptSemanticsRequest,
} from "./application/ApplicationWorkflowEvidenceResolver.js";
import type { EvidenceLookup } from "./application/EvidenceReferenceResolver.js";
import { readEvidenceBundle } from "./application/EvidenceBundleFiles.js";
@ -60,6 +62,15 @@ export const registerApplicationCommands = (
resolveInput: resolveTraceApplicationFeatureRequest,
workflow: traceApplicationFeatureEvidenceValidated,
});
registerJsonCommand({
cli,
logger,
name: CLI_COMMANDS.traceJavaScriptSemantics,
description:
"Trace bounded static semantic relations through authenticated application Evidence",
resolveInput: resolveTraceJavaScriptSemanticsRequest,
workflow: traceJavaScriptSemanticsEvidenceValidated,
});
registerJsonCommand({
cli,
logger,

View File

@ -49,6 +49,7 @@ export const CLI_COMMANDS = Object.freeze({
analyzeJavaScriptApplication: "analyze-javascript-application",
reconcileJavaScriptRuntime: "reconcile-javascript-runtime",
traceApplicationFeature: "trace-application-feature",
traceJavaScriptSemantics: "trace-javascript-semantics",
compareApplicationVersions: "compare-application-versions",
compareJavaScriptExportShapes: "compare-javascript-export-shapes",
runReplayMachine: "run-replay-machine",

View File

@ -1,10 +1,12 @@
import { applicationVersionComparisonResultSchema } from "../domain/javascriptApplicationVersionComparisonSchemas.js";
import { applicationFeatureTraceResultSchema } from "../domain/javascriptFeatureTraceSchemas.js";
import { javaScriptSemanticTraceResultSchema } from "../domain/javascriptSemanticTraceSchemas.js";
import { javaScriptExportShapeComparisonResultSchema } from "../domain/javascriptExportShapeComparisonSchemas.js";
import {
compareApplicationVersionsRequestSchema,
compareJavaScriptExportShapesRequestSchema,
traceApplicationFeatureRequestSchema,
traceJavaScriptSemanticsRequestSchema,
} from "./applicationWorkflowInputContracts.js";
import {
controlledReplayInputSchema,
@ -33,6 +35,9 @@ import {
} from "./javascriptApplicationWorkflowExamples.js";
const traceOutputSchema = evidenceResultOf(applicationFeatureTraceResultSchema);
const semanticTraceOutputSchema = evidenceResultOf(
javaScriptSemanticTraceResultSchema,
);
const comparisonOutputSchema = evidenceResultOf(
applicationVersionComparisonResultSchema,
);
@ -120,6 +125,28 @@ export const APPLICATION_TOOL_CONTRACTS = [
},
],
},
{
name: "trace_javascript_semantics",
...toolContractMetadata("trace_javascript_semantics"),
description:
"Trace bounded static JavaScript data-flow, direct call/return, and closure relations from authenticated analyze_javascript_application v2 Evidence. Queries declare direction and exact node, relation, depth, function, module, and page limits. Dynamic or unsupported semantics remain explicit unknowns; static reachability never claims runtime execution.",
kind: "application",
inputSchema: traceJavaScriptSemanticsRequestSchema,
outputSchema: semanticTraceOutputSchema,
examples: [
{
title: "Trace backward provenance from one semantic node",
input: {
application_evidence_id: `ev_${HASH}`,
query: {
seed: { kind: "semantic-node", node_id: `jsrg_node_${HASH}` },
direction: "backward-provenance",
source_map_authority: { authority: "none" },
},
},
},
],
},
{
name: "compare_application_versions",
...toolContractMetadata("compare_application_versions"),

View File

@ -4,6 +4,7 @@ import { evidenceSchema } from "../domain/evidence.js";
import { compareApplicationVersionsInputSchema } from "../domain/javascriptApplicationVersionComparisonSchemas.js";
import { compareJavaScriptExportShapesInputSchema } from "../domain/javascriptExportShapeComparisonSchemas.js";
import { traceApplicationFeatureInputSchema } from "../domain/javascriptFeatureTraceSchemas.js";
import { javaScriptSemanticQueryInputSchema } from "../domain/javascriptSemanticQuerySchemas.js";
const evidenceIdSchema = z
.string()
@ -47,6 +48,22 @@ export const traceApplicationFeatureRequestSchema = z
]);
});
/** MCP/CLI semantic trace request accepting full Evidence or a ledger reference. */
export const traceJavaScriptSemanticsRequestSchema = z
.strictObject({
application: evidenceSchema.optional(),
application_evidence_id: evidenceIdSchema.optional(),
query: javaScriptSemanticQueryInputSchema,
})
.superRefine((input, context) => {
requireExactlyOne(context, [
"application",
input.application,
"application_evidence_id",
input.application_evidence_id,
]);
});
/** MCP/CLI comparison request accepting full Evidence or ledger references. */
export const compareApplicationVersionsRequestSchema = z
.strictObject({
@ -122,6 +139,9 @@ export const compareJavaScriptExportShapesRequestSchema = z
export type TraceApplicationFeatureRequest = z.output<
typeof traceApplicationFeatureRequestSchema
>;
export type TraceJavaScriptSemanticsRequest = z.output<
typeof traceJavaScriptSemanticsRequestSchema
>;
export type CompareApplicationVersionsRequest = z.output<
typeof compareApplicationVersionsRequestSchema
>;

View File

@ -11,6 +11,7 @@ import {
} from "../domain/electronObservation.js";
import {
analyzeJavaScriptApplicationInputSchema,
javaScriptApplicationAnalysisResultV1Schema,
javascriptApplicationAnalysisResultSchema,
} from "../domain/javascriptApplicationAnalysis.js";
import { JAVASCRIPT_APPLICATION_NODE_KINDS } from "../domain/javascriptApplicationGraphSchemas.js";
@ -35,44 +36,70 @@ export const analyzeJavaScriptApplicationToolInputSchema =
});
/** Bounded default projection for agent-facing JavaScript application analysis. */
export const javascriptApplicationAnalysisSummarySchema =
javascriptApplicationAnalysisResultSchema.omit({ graph: true }).extend({
unknowns: z.array(z.string().min(1).max(4_096)).max(1_000),
graph: z.strictObject({
graph_id: z.string().regex(/^jag_[a-f0-9]{64}$/u),
node_count: z.number().int().min(0),
edge_count: z.number().int().min(0),
roots: z.strictObject({
items: z.array(z.string().regex(/^jag_node_[a-f0-9]{64}$/u)),
total: z.number().int().min(0),
truncated: z.boolean(),
}),
node_kinds: z.array(
z.strictObject({
kind: z.enum(JAVASCRIPT_APPLICATION_NODE_KINDS),
count: z.number().int().min(1),
}),
),
top_findings: z.array(
z.strictObject({
node_id: z.string().regex(/^jag_node_[a-f0-9]{64}$/u),
kind: z.enum(JAVASCRIPT_APPLICATION_NODE_KINDS),
label: z.string().nullable(),
authority: z.string(),
state: z.string(),
confidence: z.string(),
location: z.json(),
}),
),
coverage: applicationCoverageSchema,
limitations: z.array(z.string()),
pages: z.strictObject({
nodes: z.string().startsWith("rea://evidence/"),
edges: z.string().startsWith("rea://evidence/"),
page_limit: z.number().int().min(1).max(500),
}),
const applicationGraphSummarySchema = z.strictObject({
graph_id: z.string().regex(/^jag_[a-f0-9]{64}$/u),
node_count: z.number().int().min(0),
edge_count: z.number().int().min(0),
roots: z.strictObject({
items: z.array(z.string().regex(/^jag_node_[a-f0-9]{64}$/u)),
total: z.number().int().min(0),
truncated: z.boolean(),
}),
node_kinds: z.array(
z.strictObject({
kind: z.enum(JAVASCRIPT_APPLICATION_NODE_KINDS),
count: z.number().int().min(1),
}),
});
),
top_findings: z.array(
z.strictObject({
node_id: z.string().regex(/^jag_node_[a-f0-9]{64}$/u),
kind: z.enum(JAVASCRIPT_APPLICATION_NODE_KINDS),
label: z.string().nullable(),
authority: z.string(),
state: z.string(),
confidence: z.string(),
location: z.json(),
}),
),
coverage: applicationCoverageSchema,
limitations: z.array(z.string()),
pages: z.strictObject({
nodes: z.string().startsWith("rea://evidence/"),
edges: z.string().startsWith("rea://evidence/"),
page_limit: z.number().int().min(1).max(500),
}),
});
const semanticGraphSummarySchema = z.strictObject({
graph_id: z.string().regex(/^jsrg_[a-f0-9]{64}$/u),
nodes: z.number().int().min(0),
relations: z.number().int().min(0),
unknown_frontiers: z.number().int().min(0),
fingerprints: z.number().int().min(0),
coverage: z.enum(["complete", "partial", "unknown", "unavailable"]),
query_tool: z.literal("trace_javascript_semantics"),
});
const summaryFields = {
unknowns: z.array(z.string().min(1).max(4_096)).max(1_000),
graph: applicationGraphSummarySchema,
};
/** Bounded default projection for agent-facing JavaScript application analysis. */
export const javascriptApplicationAnalysisSummarySchema = z.discriminatedUnion(
"schema_version",
[
javaScriptApplicationAnalysisResultV1Schema
.omit({ graph: true })
.extend(summaryFields),
javaScriptApplicationAnalysisResultV1Schema.omit({ graph: true }).extend({
schema_version: z.literal(2),
...summaryFields,
semantic_graph: semanticGraphSummarySchema,
}),
],
);
const applicationOutputSchema = evidenceResultOf(
z.union([

View File

@ -115,6 +115,7 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
analyze_javascript_application: evidence,
reconcile_javascript_runtime: evidence,
trace_application_feature: evidence,
trace_javascript_semantics: evidence,
compare_application_versions: evidence,
compare_javascript_export_shapes: evidence,
run_replay_machine: effects(),

View File

@ -3,6 +3,7 @@ import { isAbsolute } from "node:path";
import { z } from "zod";
import { javascriptApplicationGraphSchema } from "./javascriptApplicationGraph.js";
import { javaScriptSemanticGraphSchema } from "./javascriptSemanticGraph.js";
const MAX_GRAPH_NODES = 100_000;
const MAX_GRAPH_EDGES = 200_000;
@ -155,8 +156,8 @@ const reconstructionStatisticsSchema = z.strictObject({
truncated_scopes: countSchema,
});
/** Strict high-level result returned inside Evidence v2. */
export const javascriptApplicationAnalysisResultSchema = z.strictObject({
/** Legacy structural JavaScript application analysis result. */
export const javaScriptApplicationAnalysisResultV1Schema = z.strictObject({
schema_version: z.literal(1),
input_path: z.string().min(1).max(16_384),
format: z.enum(["asar", "directory"]),
@ -169,6 +170,61 @@ export const javascriptApplicationAnalysisResultSchema = z.strictObject({
limitations: z.array(z.string().min(1).max(4_096)).max(1_000),
});
/** JavaScript application analysis with an authenticated semantic companion. */
export const javaScriptApplicationAnalysisResultV2Schema =
javaScriptApplicationAnalysisResultV1Schema
.extend({
schema_version: z.literal(2),
semantic_graph: javaScriptSemanticGraphSchema,
})
.superRefine((result, context) => {
if (result.semantic_graph.application_graph_id !== result.graph.graph_id)
context.addIssue({
code: "custom",
path: ["semantic_graph", "application_graph_id"],
message:
"Semantic graph must commit the containing application graph",
});
if (
result.semantic_graph.root_artifact_sha256 !==
result.root_artifact_sha256
)
context.addIssue({
code: "custom",
path: ["semantic_graph", "root_artifact_sha256"],
message: "Semantic graph must commit the containing root artifact",
});
const applicationNodeIds = new Set(
result.graph.nodes.map(({ node_id }) => node_id),
);
for (const [nodeIndex, node] of result.semantic_graph.nodes.entries())
for (const [
identifierIndex,
identifier,
] of node.application_node_ids.entries())
if (!applicationNodeIds.has(identifier))
context.addIssue({
code: "custom",
path: [
"semantic_graph",
"nodes",
nodeIndex,
"application_node_ids",
identifierIndex,
],
message: "Semantic node references an absent application node",
});
});
/** Strict versioned high-level result returned inside Evidence v2. */
export const javascriptApplicationAnalysisResultSchema = z.discriminatedUnion(
"schema_version",
[
javaScriptApplicationAnalysisResultV1Schema,
javaScriptApplicationAnalysisResultV2Schema,
],
);
/** Parsed public JavaScript application analysis request. */
export type AnalyzeJavaScriptApplicationInput = z.infer<
typeof analyzeJavaScriptApplicationInputSchema

View File

@ -82,18 +82,18 @@ export const digestCanonical = (value: unknown): string => {
const parseStaticLayer = (layer: StaticLayerInput): ParsedStaticLayer => {
const evidence = parseEvidence(layer.analysis);
const result = javascriptApplicationAnalysisResultSchema.parse(
evidence.normalized_result,
);
assertEvidenceIdentity(evidence, {
operation: "analyze_javascript_application",
predicate: "rea.javascript-application-analysis/v1",
predicate: `rea.javascript-application-analysis/v${String(result.schema_version)}`,
providerId: "rea-javascript-application",
providerName: "REA JavaScript application analyzer",
providerVersion: "1",
authority: "shipped-artifact",
confidence: "derived",
});
const result = javascriptApplicationAnalysisResultSchema.parse(
evidence.normalized_result,
);
if (
evidence.subject === null ||
evidence.subject.digest.sha256 !== result.root_artifact_sha256 ||

View File

@ -3,6 +3,7 @@ import * as t from "@babel/types";
import type {
JavaScriptSemanticArgumentFlow,
JavaScriptSemanticCallable,
JavaScriptSemanticCallResultFlow,
JavaScriptSemanticCallReturnFlow,
JavaScriptSemanticCallSite,
JavaScriptSemanticClosureCapture,
@ -32,6 +33,7 @@ export interface JavaScriptSemanticCallAnalysis {
readonly callSites: readonly JavaScriptSemanticCallSite[];
readonly argumentFlows: readonly JavaScriptSemanticArgumentFlow[];
readonly callReturnFlows: readonly JavaScriptSemanticCallReturnFlow[];
readonly callResultFlows: readonly JavaScriptSemanticCallResultFlow[];
readonly closureCaptures: readonly JavaScriptSemanticClosureCapture[];
readonly frontiers: readonly JavaScriptSemanticFrontier[];
}
@ -40,6 +42,7 @@ interface MutableCallAnalysis {
readonly callSites: JavaScriptSemanticCallSite[];
readonly argumentFlows: JavaScriptSemanticArgumentFlow[];
readonly callReturnFlows: JavaScriptSemanticCallReturnFlow[];
readonly callResultFlows: JavaScriptSemanticCallResultFlow[];
readonly closureCaptures: JavaScriptSemanticClosureCapture[];
readonly frontiers: JavaScriptSemanticFrontier[];
retainedArguments: number;
@ -66,6 +69,7 @@ export const collectJavaScriptSemanticCalls = (
callSites: [],
argumentFlows: [],
callReturnFlows: [],
callResultFlows: [],
closureCaptures: [],
frontiers: [],
retainedArguments: 0,
@ -95,7 +99,7 @@ export const collectJavaScriptSemanticCalls = (
t.isOptionalCallExpression(node) ||
t.isNewExpression(node)
)
collectCallSite(node, owner, context);
collectCallSite(node, parent, owner, context);
},
exit: (node) => {
const callableId = semanticCallableIdForNode(node);
@ -110,6 +114,7 @@ export const collectJavaScriptSemanticCalls = (
callSites: output.callSites,
argumentFlows: output.argumentFlows,
callReturnFlows: output.callReturnFlows,
callResultFlows: output.callResultFlows,
closureCaptures: output.closureCaptures,
frontiers: output.frontiers,
};
@ -117,6 +122,7 @@ export const collectJavaScriptSemanticCalls = (
const collectCallSite = (
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
parent: t.Node | null,
owner: JavaScriptSemanticCallable | undefined,
context: CallCollectionContext,
): void => {
@ -151,6 +157,7 @@ const collectCallSite = (
arguments: argumentsValue,
};
output.callSites.push(site);
collectCallResultFlow(site, node, parent, context);
if (site.resolution !== "exact")
addFrontier(
{
@ -167,6 +174,44 @@ const collectCallSite = (
collectReturnFlows(site, callableById, state, output);
};
const collectCallResultFlow = (
site: JavaScriptSemanticCallSite,
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
parent: t.Node | null,
context: CallCollectionContext,
): void => {
const identifier =
t.isVariableDeclarator(parent) &&
parent.init === node &&
t.isIdentifier(parent.id)
? parent.id
: t.isAssignmentExpression(parent) &&
parent.right === node &&
t.isIdentifier(parent.left)
? parent.left
: null;
if (identifier === null) return;
const binding = resolveSemanticBindingState(
context.state,
identifier,
identifier.name,
);
const identifierRange = range(identifier);
const definition = binding?.definitions.find(
({ location }) =>
location.start.line === identifierRange.start.line &&
location.start.column === identifierRange.start.column &&
location.end.line === identifierRange.end.line &&
location.end.column === identifierRange.end.column,
);
if (binding === undefined || definition === undefined) return;
context.output.callResultFlows.push({
callSiteId: site.callSiteId,
bindingId: binding.bindingId,
definitionLocation: definition.location,
});
};
const retainedArguments = (
nodes: readonly (
| t.Expression

View File

@ -53,13 +53,10 @@ const normalizeEvidence = (
evidence_ids: uniqueSorted(evidence.evidence_ids),
});
const nodeIdentity = (
node: Pick<JavaScriptSemanticGraphNode, "kind" | "identity">,
) => ({ kind: node.kind, identity: node.identity });
const nodeId = (
node: Pick<JavaScriptSemanticGraphNode, "kind" | "identity">,
): string => `jsrg_node_${digestCanonical(nodeIdentity(node))}`;
): string =>
`jsrg_node_${digestCanonical({ kind: node.kind, identity: node.identity })}`;
/** Normalize one semantic entity and derive its artifact-version identifier. */
export const createJavaScriptSemanticGraphNode = (
@ -362,6 +359,12 @@ const checkUnknowns = (
path: ["unknowns", index, "node_id"],
message: "Unknown frontier node is absent",
});
if (unknown.candidate_node_ids.some((nodeId) => !nodes.has(nodeId)))
context.addIssue({
code: "custom",
path: ["unknowns", index, "candidate_node_ids"],
message: "Unknown frontier candidate node is absent",
});
if (
unknown.relation_kinds.some(
(kind) => JAVASCRIPT_SEMANTIC_RELATION_FAMILY[kind] !== unknown.family,
@ -516,31 +519,4 @@ export const createJavaScriptSemanticGraph = (
});
};
/** Parse a stored semantic graph and reject unsupported versions or stale IDs. */
export const parseJavaScriptSemanticGraph = (
input: unknown,
): JavaScriptSemanticGraph => {
const envelope = z
.object({ schema: z.string(), schema_version: z.number() })
.passthrough()
.safeParse(input);
if (
envelope.success &&
envelope.data.schema === "JavaScriptSemanticRelationGraph" &&
envelope.data.schema_version !== 1
)
throw new TypeError(
`Unsupported JavaScript Semantic Relation Graph schema version: ${String(envelope.data.schema_version)}`,
);
return javaScriptSemanticGraphSchema.parse(input);
};
/** Serialize a verified semantic graph as canonical JSON. */
export const serializeJavaScriptSemanticGraph = (input: unknown): string =>
canonicalJson(parseJavaScriptSemanticGraph(input));
/** Compute the canonical SHA-256 commitment of a verified semantic graph. */
export const computeJavaScriptSemanticGraphSha256 = (input: unknown): string =>
digestCanonical(parseJavaScriptSemanticGraph(input));
export type { JavaScriptSemanticGraphNode, JavaScriptSemanticGraphRelation };

View File

@ -0,0 +1,39 @@
import canonicalize from "canonicalize";
import { z } from "zod";
import {
javaScriptSemanticGraphSchema,
type JavaScriptSemanticGraph,
} from "./javascriptSemanticGraph.js";
const canonicalJson = (value: unknown): string => {
const encoded = canonicalize(value);
if (encoded === undefined)
throw new TypeError(
"JavaScript semantic graph could not canonicalize data",
);
return encoded;
};
/** Parse a stored semantic graph and reject unsupported versions or stale IDs. */
export const parseJavaScriptSemanticGraph = (
input: unknown,
): JavaScriptSemanticGraph => {
const envelope = z
.object({ schema: z.string(), schema_version: z.number() })
.passthrough()
.safeParse(input);
if (
envelope.success &&
envelope.data.schema === "JavaScriptSemanticRelationGraph" &&
envelope.data.schema_version !== 1
)
throw new TypeError(
`Unsupported JavaScript Semantic Relation Graph schema version: ${String(envelope.data.schema_version)}`,
);
return javaScriptSemanticGraphSchema.parse(input);
};
/** Serialize a verified semantic graph as canonical JSON. */
export const serializeJavaScriptSemanticGraph = (input: unknown): string =>
canonicalJson(parseJavaScriptSemanticGraph(input));

View File

@ -153,6 +153,7 @@ export interface JavaScriptSemanticCallable {
export interface JavaScriptSemanticReturnSite {
readonly returnSiteId: string;
readonly location: JavaScriptSourceRange;
readonly identityReferenceLocation: JavaScriptSourceRange | null;
readonly value: JavaScriptSemanticValue;
}
@ -193,6 +194,13 @@ export interface JavaScriptSemanticCallReturnFlow {
readonly returnLocation: JavaScriptSourceRange;
}
/** One local binding initialized or assigned from a retained call expression. */
export interface JavaScriptSemanticCallResultFlow {
readonly callSiteId: string;
readonly bindingId: string;
readonly definitionLocation: JavaScriptSourceRange;
}
/** One resolved outer lexical binding referenced by a nested callable. */
export interface JavaScriptSemanticClosureCapture {
readonly callableId: string;
@ -251,6 +259,7 @@ export interface JavaScriptSemanticIr {
readonly callSites: readonly JavaScriptSemanticCallSite[];
readonly argumentFlows: readonly JavaScriptSemanticArgumentFlow[];
readonly callReturnFlows: readonly JavaScriptSemanticCallReturnFlow[];
readonly callResultFlows: readonly JavaScriptSemanticCallResultFlow[];
readonly closureCaptures: readonly JavaScriptSemanticClosureCapture[];
readonly frontiers: readonly JavaScriptSemanticFrontier[];
readonly coverage: JavaScriptSemanticCoverage;
@ -287,6 +296,7 @@ export const failedJavaScriptSemanticIr = (): JavaScriptSemanticIr => ({
callSites: [],
argumentFlows: [],
callReturnFlows: [],
callResultFlows: [],
closureCaptures: [],
frontiers: [],
coverage: { status: "failed", omittedCount: null, limitsReached: [] },

View File

@ -1,17 +1,11 @@
import { createHash } from "node:crypto";
import canonicalize from "canonicalize";
import { compareCodePoints } from "./javascriptApplicationGraph.js";
import type {
JavaScriptSemanticGraph,
JavaScriptSemanticGraphNode,
JavaScriptSemanticGraphRelation,
} from "./javascriptSemanticGraph.js";
import {
JAVASCRIPT_SEMANTIC_RELATION_FAMILY,
type JavaScriptSemanticGraphUnknown,
} from "./javascriptSemanticGraphSchemas.js";
import type { JavaScriptSemanticGraphUnknown } from "./javascriptSemanticGraphSchemas.js";
import { isJavaScriptSemanticOwnershipRelation as ownershipRelation } from "./javascriptSemanticQueryRelations.js";
import {
javaScriptSemanticQueryInputSchema,
javaScriptSemanticQueryResultSchema,
@ -22,6 +16,14 @@ import {
assessJavaScriptSemanticQuery,
type JavaScriptSemanticQueryFrontier,
} from "./javascriptSemanticQueryAssessment.js";
import {
canonicalJavaScriptSemanticQueryJson as canonicalJson,
createJavaScriptSemanticQueryCursor as createCursor,
JavaScriptSemanticQueryCursorError,
javaScriptSemanticQueryIdentifier as queryIdentifier,
parseJavaScriptSemanticQueryCursor as parseCursor,
resolveJavaScriptSemanticQuerySeeds as resolveSeeds,
} from "./javascriptSemanticQueryIdentity.js";
interface TraversalEntry {
readonly relation: JavaScriptSemanticGraphRelation;
@ -43,20 +45,22 @@ interface SeedAdmission {
readonly frontier: QueryFrontier[];
}
const canonicalJson = (value: unknown): string => {
const encoded = canonicalize(value);
if (encoded === undefined)
throw new TypeError(
"JavaScript semantic query could not canonicalize data",
);
return encoded;
};
interface QueryPage {
readonly retainedNodes: JavaScriptSemanticGraphNode[];
readonly retainedRelations: JavaScriptSemanticGraphRelation[];
readonly pageRelations: JavaScriptSemanticGraphRelation[];
readonly pageNodes: JavaScriptSemanticGraphNode[];
readonly nextCursor: string | null;
}
const digest = (value: unknown): string =>
createHash("sha256").update(canonicalJson(value)).digest("hex");
const uniqueSorted = (values: readonly string[]): string[] =>
[...new Set(values)].sort(compareCodePoints);
interface QueryPageInput {
readonly graph: JavaScriptSemanticGraph;
readonly traversal: Traversal;
readonly query: JavaScriptSemanticQueryInput;
readonly queryId: string;
readonly offset: number;
readonly retainedSeeds: readonly string[];
}
/** Run one deterministic bounded traversal over a verified semantic graph. */
export const queryJavaScriptSemanticGraph = (
@ -75,41 +79,31 @@ export const queryJavaScriptSemanticGraph = (
...traversal.frontier,
...admission.frontier,
]);
const retainedNodes = graph.nodes.filter(({ node_id }) =>
traversal.nodeIds.has(node_id),
);
const retainedRelations = graph.relations.filter(({ relation_id }) =>
traversal.relationIds.has(relation_id),
);
if (offset > retainedRelations.length)
throw new TypeError("Semantic query cursor is beyond the retained result");
const pageRelations = retainedRelations.slice(
const page = createQueryPage({
graph,
traversal,
query: input,
queryId,
offset,
offset + input.limits.page_size,
);
const pageNodeIds = new Set([
...retainedSeeds,
...pageRelations.flatMap(({ source_node_id, target_node_id }) => [
source_node_id,
target_node_id,
]),
]);
const pageNodes = graph.nodes.filter(({ node_id }) =>
pageNodeIds.has(node_id),
);
const relevantUnknowns = relevantUnknownFrontiers(
retainedSeeds,
});
const allRelevantUnknowns = relevantUnknownFrontiers(
graph,
traversal.nodeIds,
input,
);
const relevantUnknowns = allRelevantUnknowns.slice(
0,
input.limits.max_unknowns,
);
const candidateRelations = relevantCandidateRelationCount(
graph,
traversal.nodeIds,
input,
);
const expectedMatches = expectedMatchesFor(
retainedNodes,
retainedRelations,
page.retainedNodes,
page.retainedRelations,
input,
);
const assessment = assessJavaScriptSemanticQuery({
@ -121,12 +115,8 @@ export const queryJavaScriptSemanticGraph = (
frontier,
unknowns: relevantUnknowns,
candidateRelations,
unknownsTruncated: allRelevantUnknowns.length > relevantUnknowns.length,
});
const nextOffset = offset + pageRelations.length;
const nextCursor =
nextOffset < retainedRelations.length
? createCursor(queryId, nextOffset)
: null;
return javaScriptSemanticQueryResultSchema.parse({
schema_version: 1,
query_id: queryId,
@ -135,8 +125,8 @@ export const queryJavaScriptSemanticGraph = (
direction: input.direction,
status: assessment.status,
seed_node_ids: retainedSeeds,
nodes: pageNodes,
relations: pageRelations,
nodes: page.pageNodes,
relations: page.pageRelations,
unknowns: relevantUnknowns,
expected_match_node_ids: expectedMatches.map(({ node_id }) => node_id),
summary: {
@ -146,13 +136,14 @@ export const queryJavaScriptSemanticGraph = (
traversed_relations: traversal.relationIds.size,
traversed_functions: traversal.functionIds.size,
traversed_modules: traversal.modules.size,
relevant_unknowns: relevantUnknowns.length,
relevant_unknowns: allRelevantUnknowns.length,
retained_unknowns: relevantUnknowns.length,
},
coverage: assessment.coverage,
page: {
offset,
size: pageRelations.length,
next_cursor: nextCursor,
size: page.pageRelations.length,
next_cursor: page.nextCursor,
},
applied_limits: input.limits,
accepted_limit_ranges: assessment.acceptedLimitRanges,
@ -160,96 +151,49 @@ export const queryJavaScriptSemanticGraph = (
});
};
const queryIdentifier = (
graph: JavaScriptSemanticGraph,
input: JavaScriptSemanticQueryInput,
): string => {
const { cursor: _cursor, ...semantic } = input;
return `jsrq_${digest({
source_graph_id: graph.graph_id,
...semantic,
allowed_relations:
semantic.allowed_relations === undefined
? undefined
: uniqueSorted(semantic.allowed_relations),
expected:
semantic.expected === null
? null
: {
...semantic.expected,
classes: uniqueSorted(semantic.expected.classes),
},
})}`;
};
const createCursor = (queryId: string, offset: number): string =>
`jsrqc_${String(offset)}_${digest({ query_id: queryId, offset })}`;
const parseCursor = (cursor: string | null, queryId: string): number => {
if (cursor === null) return 0;
const match = /^jsrqc_([0-9]+)_([a-f0-9]{64})$/u.exec(cursor);
const offsetText = match?.[1];
const commitment = match?.[2];
if (offsetText === undefined || commitment === undefined)
throw new TypeError("Semantic query cursor is malformed");
const offset = Number(offsetText);
if (
!Number.isSafeInteger(offset) ||
offset < 0 ||
commitment !== digest({ query_id: queryId, offset })
)
throw new TypeError("Semantic query cursor does not match this query");
return offset;
};
const resolveSeeds = (
graph: JavaScriptSemanticGraph,
input: JavaScriptSemanticQueryInput,
): string[] => {
const fingerprintFunctions = new Set(
input.seed.kind === "function"
? graph.fingerprints
.filter(({ fingerprint_sha256 }) => {
const seed = input.seed;
return (
seed.kind === "function" &&
fingerprint_sha256 === seed.fingerprint_sha256
);
})
.map(({ function_node_id }) => function_node_id)
: [],
const createQueryPage = ({
graph,
traversal,
query,
queryId,
offset,
retainedSeeds,
}: QueryPageInput): QueryPage => {
const retainedNodes = graph.nodes.filter(({ node_id }) =>
traversal.nodeIds.has(node_id),
);
return graph.nodes
.filter((node) => seedMatches(node, input, fingerprintFunctions))
.map(({ node_id }) => node_id)
.sort(compareCodePoints);
};
const seedMatches = (
node: JavaScriptSemanticGraphNode,
input: JavaScriptSemanticQueryInput,
fingerprintFunctions: ReadonlySet<string>,
): boolean => {
const seed = input.seed;
if (seed.kind === "semantic-node") return node.node_id === seed.node_id;
if (seed.kind === "application-node")
return node.application_node_ids.includes(seed.node_id);
if (seed.kind === "function") return fingerprintFunctions.has(node.node_id);
if (seed.kind === "literal")
return (
node.kind === "literal" &&
canonicalJson(node.properties.value) === canonicalJson(seed.value)
const retainedRelations = graph.relations.filter(({ relation_id }) =>
traversal.relationIds.has(relation_id),
);
if (offset > retainedRelations.length)
throw new JavaScriptSemanticQueryCursorError(
"Semantic query cursor is beyond the retained result",
);
if (seed.kind === "property")
return node.kind === "property-slot" && node.properties.name === seed.name;
if (seed.kind === "endpoint")
return node.kind === "request" && node.properties.endpoint === seed.value;
if (seed.kind === "event")
return (
["event", "listener"].includes(node.kind) &&
node.properties.event_name === seed.name
);
return node.kind === "boundary" && node.properties.field === seed.field;
const pageRelations = retainedRelations.slice(
offset,
offset + query.limits.page_size,
);
const pageNodeIds = new Set([
...retainedSeeds,
...pageRelations.flatMap(({ source_node_id, target_node_id }) => [
source_node_id,
target_node_id,
]),
]);
const pageNodes = graph.nodes.filter(({ node_id }) =>
pageNodeIds.has(node_id),
);
const nextOffset = offset + pageRelations.length;
return {
retainedNodes,
retainedRelations,
pageRelations,
pageNodes,
nextCursor:
nextOffset < retainedRelations.length
? createCursor(queryId, nextOffset)
: null,
};
};
const admitSeeds = (
@ -395,23 +339,6 @@ const addDirectedEntries = (
);
};
const ownershipRelation = (
relation: JavaScriptSemanticGraphRelation["relation"],
): boolean =>
[
"acquires",
"aggregates",
"awaits",
"chains",
"connects-stdio",
"creates-promise",
"detaches-task",
"owns",
"releases",
"returns-task",
"spawns",
].includes(relation);
const addEntry = (
adjacency: Map<string, TraversalEntry[]>,
from: string,
@ -585,8 +512,3 @@ const expectedMatchesFor = (
({ kind, node_id }) => classes.has(kind) && !connected.has(node_id),
);
};
/** Return a relation's declared semantic coverage family. */
export const javaScriptSemanticRelationFamily = (
relation: JavaScriptSemanticGraphRelation["relation"],
) => JAVASCRIPT_SEMANTIC_RELATION_FAMILY[relation];

View File

@ -26,6 +26,7 @@ export interface JavaScriptSemanticQueryAssessmentInput {
readonly frontier: readonly JavaScriptSemanticQueryFrontier[];
readonly unknowns: readonly JavaScriptSemanticGraphUnknown[];
readonly candidateRelations: number;
readonly unknownsTruncated: boolean;
}
/** Status, coverage, limits, and limitations derived from one traversal. */
@ -50,6 +51,7 @@ export const assessJavaScriptSemanticQuery = (
max_depth: { minimum: 0, maximum: 64 },
max_functions: { minimum: 1, maximum: 10_000 },
max_modules: { minimum: 1, maximum: 10_000 },
max_unknowns: { minimum: 0, maximum: 10_000 },
page_size: { minimum: 1, maximum: 1_000 },
},
});
@ -60,7 +62,7 @@ const queryCoverage = (
status:
input.graph.coverage.status === "unavailable"
? "unavailable"
: input.frontier.length > 0
: input.frontier.length > 0 || input.unknownsTruncated
? "truncated"
: input.unknowns.length > 0 ||
input.candidateRelations > 0 ||
@ -74,15 +76,16 @@ const queryStatus = (
input: JavaScriptSemanticQueryAssessmentInput,
): JavaScriptSemanticQueryResult["status"] => {
if (input.graph.coverage.status === "unavailable") return "unsupported";
if (input.frontier.length > 0) return "truncated";
if (input.unknowns.length > 0 || input.graph.coverage.status !== "complete")
return "partial";
if (input.frontier.length > 0 || input.unknownsTruncated) return "truncated";
if (input.candidateRelations > 0) return "ambiguous";
if (
input.totalSeeds === 0 ||
(input.hasExpectation && input.expectedMatches === 0)
)
return "no-match";
return input.unknowns.length > 0 ||
input.graph.coverage.status !== "complete"
? "partial"
: "no-match";
return input.totalSeeds > 1 ? "ambiguous" : "found";
};
@ -107,6 +110,11 @@ const queryLimitations = (
: [
"Relevant dynamic, unsupported, incomplete, or ambiguous semantics remain unknown.",
]),
...(input.unknownsTruncated
? [
"Relevant unknown frontiers exceeded the caller limit; omitted unknowns remain unresolved.",
]
: []),
...(input.candidateRelations === 0
? []
: [

View File

@ -0,0 +1,137 @@
import { createHash } from "node:crypto";
import canonicalize from "canonicalize";
import { compareCodePoints } from "./javascriptApplicationGraph.js";
import type {
JavaScriptSemanticGraph,
JavaScriptSemanticGraphNode,
} from "./javascriptSemanticGraph.js";
import type { JavaScriptSemanticQueryInput } from "./javascriptSemanticQuerySchemas.js";
/** Caller cursor that cannot identify a page of the current semantic query. */
export class JavaScriptSemanticQueryCursorError extends TypeError {}
/** Canonical JSON used by semantic query commitments and ordering. */
export const canonicalJavaScriptSemanticQueryJson = (
value: unknown,
): string => {
const encoded = canonicalize(value);
if (encoded === undefined)
throw new TypeError(
"JavaScript semantic query could not canonicalize data",
);
return encoded;
};
const digest = (value: unknown): string =>
createHash("sha256")
.update(canonicalJavaScriptSemanticQueryJson(value))
.digest("hex");
const uniqueSorted = (values: readonly string[]): string[] =>
[...new Set(values)].sort(compareCodePoints);
/** Derive the stable identity for one graph-bound query without its cursor. */
export const javaScriptSemanticQueryIdentifier = (
graph: JavaScriptSemanticGraph,
input: JavaScriptSemanticQueryInput,
): string => {
const { cursor: _cursor, ...semantic } = input;
return `jsrq_${digest({
source_graph_id: graph.graph_id,
...semantic,
allowed_relations:
semantic.allowed_relations === undefined
? undefined
: uniqueSorted(semantic.allowed_relations),
expected:
semantic.expected === null
? null
: {
...semantic.expected,
classes: uniqueSorted(semantic.expected.classes),
},
})}`;
};
/** Commit one deterministic relation-page offset to a query identity. */
export const createJavaScriptSemanticQueryCursor = (
queryId: string,
offset: number,
): string => `jsrqc_${String(offset)}_${digest({ query_id: queryId, offset })}`;
/** Parse a query-bound cursor and reject stale, malformed, or foreign values. */
export const parseJavaScriptSemanticQueryCursor = (
cursor: string | null,
queryId: string,
): number => {
if (cursor === null) return 0;
const match = /^jsrqc_([0-9]+)_([a-f0-9]{64})$/u.exec(cursor);
const offsetText = match?.[1];
const commitment = match?.[2];
if (offsetText === undefined || commitment === undefined)
throw new JavaScriptSemanticQueryCursorError(
"Semantic query cursor is malformed",
);
const offset = Number(offsetText);
if (
!Number.isSafeInteger(offset) ||
offset < 0 ||
commitment !== digest({ query_id: queryId, offset })
)
throw new JavaScriptSemanticQueryCursorError(
"Semantic query cursor does not match this query",
);
return offset;
};
/** Resolve deterministic seed candidates without applying traversal limits. */
export const resolveJavaScriptSemanticQuerySeeds = (
graph: JavaScriptSemanticGraph,
input: JavaScriptSemanticQueryInput,
): string[] => {
const fingerprintFunctions = new Set(
input.seed.kind === "function"
? graph.fingerprints
.filter(
({ fingerprint_sha256 }) =>
input.seed.kind === "function" &&
fingerprint_sha256 === input.seed.fingerprint_sha256,
)
.map(({ function_node_id }) => function_node_id)
: [],
);
return graph.nodes
.filter((node) => seedMatches(node, input, fingerprintFunctions))
.map(({ node_id }) => node_id)
.sort(compareCodePoints);
};
const seedMatches = (
node: JavaScriptSemanticGraphNode,
input: JavaScriptSemanticQueryInput,
fingerprintFunctions: ReadonlySet<string>,
): boolean => {
const seed = input.seed;
if (seed.kind === "semantic-node") return node.node_id === seed.node_id;
if (seed.kind === "application-node")
return node.application_node_ids.includes(seed.node_id);
if (seed.kind === "function") return fingerprintFunctions.has(node.node_id);
if (seed.kind === "literal")
return (
node.kind === "literal" &&
canonicalJavaScriptSemanticQueryJson(node.properties.value) ===
canonicalJavaScriptSemanticQueryJson(seed.value)
);
if (seed.kind === "property")
return node.kind === "property-slot" && node.properties.name === seed.name;
if (seed.kind === "endpoint")
return node.kind === "request" && node.properties.endpoint === seed.value;
if (seed.kind === "event")
return (
["event", "listener"].includes(node.kind) &&
node.properties.event_name === seed.name
);
return node.kind === "boundary" && node.properties.field === seed.field;
};

View File

@ -0,0 +1,22 @@
import type { JavaScriptSemanticGraphRelation } from "./javascriptSemanticGraph.js";
const OWNERSHIP_RELATIONS = new Set<
JavaScriptSemanticGraphRelation["relation"]
>([
"acquires",
"aggregates",
"awaits",
"chains",
"connects-stdio",
"creates-promise",
"detaches-task",
"owns",
"releases",
"returns-task",
"spawns",
]);
/** Whether one relation participates in bidirectional ownership traversal. */
export const isJavaScriptSemanticOwnershipRelation = (
relation: JavaScriptSemanticGraphRelation["relation"],
): boolean => OWNERSHIP_RELATIONS.has(relation);

View File

@ -11,18 +11,6 @@ import { jsonValueSchema } from "./jsonValue.js";
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
const semanticNodeIdSchema = z.string().regex(/^jsrg_node_[a-f0-9]{64}$/u);
const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
/** Exact accepted ranges and defaults for one semantic traversal. */
export const JAVASCRIPT_SEMANTIC_QUERY_LIMIT_RANGES = {
max_seed_matches: { minimum: 1, maximum: 1_000, default: 25 },
max_nodes: { minimum: 1, maximum: 50_000, default: 2_000 },
max_edges: { minimum: 1, maximum: 100_000, default: 4_000 },
max_depth: { minimum: 0, maximum: 64, default: 12 },
max_functions: { minimum: 1, maximum: 10_000, default: 500 },
max_modules: { minimum: 1, maximum: 10_000, default: 500 },
page_size: { minimum: 1, maximum: 1_000, default: 200 },
} as const;
const literalSeedSchema = z.strictObject({
kind: z.literal("literal"),
@ -77,17 +65,13 @@ export const javaScriptSemanticQueryLimitsSchema = z.strictObject({
max_depth: z.number().int().min(0).max(64).default(12),
max_functions: z.number().int().min(1).max(10_000).default(500),
max_modules: z.number().int().min(1).max(10_000).default(500),
max_unknowns: z.number().int().min(0).max(10_000).default(1_000),
page_size: z.number().int().min(1).max(1_000).default(200),
});
const sourceMapAuthoritySchema = z.discriminatedUnion("authority", [
z.strictObject({ authority: z.literal("none") }),
z.strictObject({
authority: z.literal("authenticated"),
evidence_id: evidenceIdSchema,
source_map_sha256: digestSchema,
}),
]);
const sourceMapAuthoritySchema = z.strictObject({
authority: z.literal("none"),
});
/** Parsed pure-domain query over one authenticated companion graph. */
export const javaScriptSemanticQueryInputSchema = z.strictObject({
@ -96,7 +80,6 @@ export const javaScriptSemanticQueryInputSchema = z.strictObject({
"backward-provenance",
"forward-influence",
"callers",
"consumers",
"ownership",
]),
allowed_relations: z
@ -120,6 +103,7 @@ export const javaScriptSemanticQueryInputSchema = z.strictObject({
max_depth: 12,
max_functions: 500,
max_modules: 500,
max_unknowns: 1_000,
page_size: 200,
}),
cursor: z
@ -170,6 +154,7 @@ export const javaScriptSemanticQueryResultSchema = z.strictObject({
traversed_functions: z.number().int().min(0),
traversed_modules: z.number().int().min(0),
relevant_unknowns: z.number().int().min(0),
retained_unknowns: z.number().int().min(0).max(10_000),
}),
coverage: z.strictObject({
status: z.enum(["complete", "partial", "truncated", "unavailable"]),
@ -209,6 +194,10 @@ export const javaScriptSemanticQueryResultSchema = z.strictObject({
minimum: z.literal(1),
maximum: z.literal(10_000),
}),
max_unknowns: z.strictObject({
minimum: z.literal(0),
maximum: z.literal(10_000),
}),
page_size: z.strictObject({
minimum: z.literal(1),
maximum: z.literal(1_000),

View File

@ -60,6 +60,10 @@ export const collectSemanticReturns = (
expression.location,
),
location: expression.location,
identityReferenceLocation:
expression.node !== null && t.isIdentifier(expression.node)
? range(expression.node)
: null,
value:
expression.node === null
? { status: "unknown", reason: "Return has no value." }

View File

@ -0,0 +1,27 @@
import { z } from "zod";
import { evidenceSchema } from "./evidence.js";
import {
javaScriptSemanticQueryInputSchema,
javaScriptSemanticQueryResultSchema,
} from "./javascriptSemanticQuerySchemas.js";
const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
/** Authenticated application Evidence plus one bounded semantic query. */
export const traceJavaScriptSemanticsInputSchema = z.strictObject({
application: evidenceSchema,
query: javaScriptSemanticQueryInputSchema,
});
/** Evidence-linked semantic query result shared by CLI and MCP. */
export const javaScriptSemanticTraceResultSchema =
javaScriptSemanticQueryResultSchema.extend({
source_evidence_id: evidenceIdSchema,
evidence_links: z.array(evidenceIdSchema).length(1),
});
/** Validated Evidence-linked semantic trace result. */
export type JavaScriptSemanticTraceResult = z.infer<
typeof javaScriptSemanticTraceResultSchema
>;

View File

@ -58,7 +58,8 @@ export const summarizeJavaScriptApplicationEvidence = (
location: observation.evidence.location,
};
});
const { graph: _graph, ...summary } = result;
const summary =
result.schema_version === 1 ? omitGraph(result) : omitGraphs(result);
const unknowns = new Set([
...result.limitations,
...result.graph.limitations,
@ -77,6 +78,19 @@ export const summarizeJavaScriptApplicationEvidence = (
);
return javascriptApplicationAnalysisSummarySchema.parse({
...summary,
...(result.schema_version === 1
? {}
: {
semantic_graph: {
graph_id: result.semantic_graph.graph_id,
nodes: result.semantic_graph.nodes.length,
relations: result.semantic_graph.relations.length,
unknown_frontiers: result.semantic_graph.unknowns.length,
fingerprints: result.semantic_graph.fingerprints.length,
coverage: result.semantic_graph.coverage.status,
query_tool: "trace_javascript_semantics",
},
}),
unknowns: [...unknowns],
graph: {
graph_id: result.graph.graph_id,
@ -101,3 +115,23 @@ export const summarizeJavaScriptApplicationEvidence = (
},
});
};
const omitGraph = (
result: Extract<
ReturnType<typeof javascriptApplicationAnalysisResultSchema.parse>,
{ readonly schema_version: 1 }
>,
) => {
const { graph: _graph, ...summary } = result;
return summary;
};
const omitGraphs = (
result: Extract<
ReturnType<typeof javascriptApplicationAnalysisResultSchema.parse>,
{ readonly schema_version: 2 }
>,
) => {
const { graph: _graph, semantic_graph: _semanticGraph, ...summary } = result;
return summary;
};

View File

@ -1,6 +1,7 @@
import type { McpServer } from "@modelcontextprotocol/server";
import { registerTraceFeatureTool } from "./registerApplicationTools/traceFeature.js";
import { registerTraceJavaScriptSemanticsTool } from "./registerApplicationTools/traceSemantics.js";
import { registerCompareApplicationVersionsTool } from "./registerApplicationTools/compareVersions.js";
import { registerCompareJavaScriptExportShapesTool } from "./registerApplicationTools/compareExportShapes.js";
import { registerControlledReplayTool } from "./registerApplicationTools/controlledReplay.js";
@ -16,6 +17,7 @@ export const registerApplicationTools = (
options: ApplicationToolRegistration,
): void => {
registerTraceFeatureTool(server, options);
registerTraceJavaScriptSemanticsTool(server, options);
registerCompareApplicationVersionsTool(server, options);
registerCompareJavaScriptExportShapesTool(server, options);
registerControlledReplayTool(server, options);

View File

@ -0,0 +1,49 @@
import type { McpServer } from "@modelcontextprotocol/server";
import { resolveTraceJavaScriptSemanticsRequestValidated } from "../../application/ApplicationWorkflowEvidenceResolver.js";
import { traceJavaScriptSemanticsEvidenceValidated } from "../../application/JavaScriptSemanticTraceService.js";
import { applicationToolContract } from "../../contracts/applicationToolContracts.js";
import { traceJavaScriptSemanticsRequestSchema } from "../../contracts/applicationWorkflowInputContracts.js";
import { safeParseToolInput } from "../toolInputValidation.js";
import { logToolExecution } from "../toolLogging.js";
import { toolRegistrationOptions } from "../toolRegistrationOptions.js";
import { toCallToolResult } from "../toolResult.js";
import { recordResult, recordSources } from "./helpers.js";
import type { ApplicationToolRegistration } from "./types.js";
const contract = applicationToolContract("trace_javascript_semantics");
/** Register the bounded JavaScript semantic relation trace tool. */
export const registerTraceJavaScriptSemanticsTool = (
server: McpServer,
options: ApplicationToolRegistration,
): void => {
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async (input) => {
const parsedInput = safeParseToolInput(
traceJavaScriptSemanticsRequestSchema,
input,
contract.name,
);
if (!parsedInput.ok) return toCallToolResult(parsedInput, contract);
const resolved = resolveTraceJavaScriptSemanticsRequestValidated(
parsedInput.value,
options.evidenceLookup,
);
if (!resolved.ok) return toCallToolResult(resolved, contract);
const result = await logToolExecution(options.logger, contract.name, () =>
Promise.resolve(
traceJavaScriptSemanticsEvidenceValidated(resolved.value),
),
);
if (!result.ok) return toCallToolResult(result, contract);
const recorded = recordSources(options.recordEvidence, [
resolved.value.application,
]);
if (!recorded.ok) return toCallToolResult(recorded, contract);
return recordResult(options, contract, result.value);
},
);
};

View File

@ -20,6 +20,7 @@ import {
import { analyzeJavaScriptApplication } from "../src/application/JavaScriptApplicationService.js";
import { permissionAuthorityForRoot } from "./fixtures/permissionAuthority.js";
import { REPLAY_MACHINE_RUN_EXAMPLE } from "../src/contracts/replayMachineExample.js";
import { javascriptApplicationAnalysisResultSchema } from "../src/domain/javascriptApplicationAnalysis.js";
const execute = promisify(execFile);
@ -66,6 +67,54 @@ describe("application workflow CLI parity", () => {
});
}, 20_000);
it("traces the same authenticated semantic graph through the CLI", async () => {
const root = await createTestTempDirectory("rea-semantic-cli-");
temporary.push(root);
await writeFile(
join(root, "app.js"),
"function add(value) { return value + 1; } add(2);",
);
const authority = await permissionAuthorityForRoot(
root,
["investigation_input"],
["investigation_input"],
);
const analyzed = await analyzeJavaScriptApplication(authority, {
input_path: root,
approved: true,
});
if (!analyzed.ok) throw analyzed.error;
const result = javascriptApplicationAnalysisResultSchema.parse(
analyzed.value.normalized_result,
);
if (result.schema_version !== 2)
throw new TypeError("Expected semantic application Evidence");
const seed = result.semantic_graph.relations[0]?.source_node_id;
if (seed === undefined)
throw new TypeError("Expected at least one semantic relation");
const traced = await runCli([
"trace-javascript-semantics",
JSON.stringify({
application: analyzed.value,
query: {
seed: { kind: "semantic-node", node_id: seed },
direction: "forward-influence",
include_ambiguous_dynamic_edges: true,
},
}),
"--json",
]);
expect(traced).toMatchObject({
operation: "trace_javascript_semantics",
predicate_type: "rea.javascript-semantic-trace/v1",
normalized_result: {
source_evidence_id: analyzed.value.evidence_id,
source_graph_id: result.semantic_graph.graph_id,
},
});
}, 20_000);
it("resolves Evidence IDs from an explicitly authorized bundle", async () => {
const root = await createTestTempDirectory("rea-application-bundle-cli-");
temporary.push(root);

View File

@ -21,6 +21,7 @@ import { createPermissionPolicy } from "../src/domain/permissionPolicy.js";
import { controlledReplayOutputSchema } from "../src/domain/javascriptReplay.js";
import { nodeCharacterizationPreparationOutputSchema } from "../src/domain/nodeRuntimeCharacterization.js";
import { analyzeJavaScriptApplication } from "../src/application/JavaScriptApplicationService.js";
import { javascriptApplicationAnalysisResultSchema } from "../src/domain/javascriptApplicationAnalysis.js";
import { permissionAuthorityForRoot } from "./fixtures/permissionAuthority.js";
describe("application workflow MCP parity", () => {
@ -114,6 +115,35 @@ describe("application workflow MCP parity", () => {
changes: [expect.objectContaining({ path: "/depth" })],
},
});
const analyzed = javascriptApplicationAnalysisResultSchema.parse(
left.value.normalized_result,
);
if (analyzed.schema_version !== 2)
throw new TypeError("Expected semantic application Evidence");
const seed = analyzed.semantic_graph.relations[0]?.source_node_id;
if (seed === undefined)
throw new TypeError("Expected at least one semantic relation");
const semantic = await client.callTool({
name: "trace_javascript_semantics",
arguments: {
application: left.value,
query: {
seed: { kind: "semantic-node", node_id: seed },
direction: "forward-influence",
include_ambiguous_dynamic_edges: true,
},
},
});
expect(semantic.isError).not.toBe(true);
expect(semantic.structuredContent).toMatchObject({
result: {
schema_version: 1,
source_evidence_id: left.value.evidence_id,
source_graph_id: analyzed.semantic_graph.graph_id,
summary: { retained_seed_matches: 1 },
},
});
} finally {
await client.close();
await server.close();

View File

@ -97,7 +97,7 @@ describe("tool contract surface", () => {
...ELECTRON_TOOL_CONTRACTS,
...APPLICATION_TOOL_CONTRACTS,
];
expect(contracts).toHaveLength(78);
expect(contracts).toHaveLength(79);
for (const contract of contracts) {
const inputSchema = contractJsonSchema(contract.inputSchema);
const outputSchema = contractJsonSchema(contract.outputSchema);

View File

@ -247,13 +247,13 @@ describe("static Electron application analysis", () => {
const evidence = parseEvidence(result.value);
expect(evidence).toMatchObject({
operation: "analyze_javascript_application",
predicate_type: "rea.javascript-application-analysis/v1",
predicate_type: "rea.javascript-application-analysis/v2",
provider: { id: "rea-javascript-application" },
authority: "shipped-artifact",
confidence: "derived",
subject: { local_path: root, format: "directory" },
normalized_result: {
schema_version: 1,
schema_version: 2,
input_path: root,
summary: { browser_windows: 3 },
},

View File

@ -229,7 +229,7 @@ describe("Electron MCP tools", () => {
.object({
evidence_id: z.string(),
result: z.object({
schema_version: z.literal(1),
schema_version: z.literal(2),
input_path: z.string(),
unknowns: z.array(z.string()),
summary: z.object({
@ -244,6 +244,13 @@ describe("Electron MCP tools", () => {
top_findings: z.array(z.unknown()),
pages: z.object({ nodes: z.string(), edges: z.string() }),
}),
semantic_graph: z.object({
graph_id: z.string(),
nodes: z.number(),
relations: z.number(),
unknown_frontiers: z.number(),
query_tool: z.literal("trace_javascript_semantics"),
}),
}),
})
.parse(analyzed.structuredContent);
@ -256,6 +263,7 @@ describe("Electron MCP tools", () => {
},
});
expect(projected.result.graph.node_count).toBeGreaterThan(0);
expect(projected.result.semantic_graph.nodes).toBeGreaterThan(0);
expect(JSON.stringify(analyzed.structuredContent)).not.toContain(
'"nodes":[',
);

View File

@ -19,6 +19,9 @@ import {
type ArtifactReader,
} from "../src/artifacts/ArtifactReader.js";
import { parseJavaScriptApplicationGraph } from "../src/domain/javascriptApplicationGraph.js";
import { javaScriptApplicationAnalysisResultV2Schema } from "../src/domain/javascriptApplicationAnalysis.js";
import { createJavaScriptSemanticGraph } from "../src/domain/javascriptSemanticGraph.js";
import { parseJavaScriptSemanticGraph } from "../src/domain/javascriptSemanticGraphSerialization.js";
import { writeJavaScriptArtifactFixture } from "./fixtures/javascriptArtifactApplication.js";
describe("JavaScript artifact reconstruction", () => {
@ -31,6 +34,7 @@ describe("JavaScript artifact reconstruction", () => {
source_map_read_approved: true,
});
const graph = parseJavaScriptApplicationGraph(result.graph);
const semanticGraph = parseJavaScriptSemanticGraph(result.semantic_graph);
expect(Reflect.get(globalThis, "__rea_bundle_executed")).toBeUndefined();
expect(result.input_path).toBe(root);
@ -106,6 +110,27 @@ describe("JavaScript artifact reconstruction", () => {
"persists_to",
]),
);
expect(semanticGraph.application_graph_id).toBe(graph.graph_id);
const { graph_id: _semanticGraphId, ...semanticGraphInput } = semanticGraph;
const mismatchedSemanticGraph = createJavaScriptSemanticGraph({
...semanticGraphInput,
application_graph_id: `jag_${"f".repeat(64)}`,
});
const { electron_summary: summary, ...analysisResult } = result;
expect(() =>
javaScriptApplicationAnalysisResultV2Schema.parse({
...analysisResult,
schema_version: 2,
summary,
semantic_graph: mismatchedSemanticGraph,
}),
).toThrow(/must commit the containing application graph/u);
expect(
semanticGraph.nodes.some(
({ application_node_ids: identifiers }) => identifiers.length > 0,
),
).toBe(true);
expect(semanticGraph.relations.length).toBeGreaterThan(0);
const mainRole = graph.nodes.find(
(node) =>
node.kind === "electron-main" &&
@ -134,6 +159,29 @@ describe("JavaScript artifact reconstruction", () => {
resolution_status: "resolved",
limitations: [],
});
const mainAsset = graph.nodes.find(
(node) =>
node.kind === "javascript-asset" &&
node.observations.some(
({ evidence }) =>
evidence.location.available &&
evidence.location.value.kind === "artifact-path" &&
evidence.location.value.path === "main.js",
),
);
if (mainAsset === undefined)
throw new Error("Expected main JavaScript asset");
const mainSemanticNodes = semanticGraph.nodes.filter(
({ identity }) => identity.module_path === "main.js",
);
const linkedMainSemanticNodes = mainSemanticNodes.filter(
({ application_node_ids: identifiers }) =>
identifiers.includes(mainAsset.node_id),
);
expect(linkedMainSemanticNodes.length).toBeGreaterThan(0);
expect(linkedMainSemanticNodes.length).toBeLessThan(
mainSemanticNodes.length,
);
for (const role of [mainRole, preloadRole]) {
expect(role).toBeDefined();
expect(

View File

@ -0,0 +1,314 @@
import { describe, expect, it } from "vitest";
import { buildJavaScriptSemanticGraph } from "../src/application/JavaScriptSemanticGraphBuilder.js";
import type { JavaScriptArtifactAnalysis } from "../src/application/JavaScriptArtifactAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "../src/application/JavaScriptArtifactFiles.js";
import { queryJavaScriptSemanticGraph } from "../src/domain/javascriptSemanticQuery.js";
import { analyzeJavaScriptSemantics } from "../src/domain/javascriptSemanticAnalysis.js";
import {
DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS,
type JavaScriptSemanticLimits,
} from "../src/domain/javascriptSemanticIr.js";
const SHA256 = "a".repeat(64);
const GRAPH_ID = `jag_${"b".repeat(64)}`;
describe("JavaScript semantic companion graph", () => {
it("projects closure and direct interprocedural flow without execution", () => {
const graph = graphFor(`
const outer = 2;
function add(value) { return value + outer; }
const input = 40;
const answer = add(input);
`);
const relations = new Set(graph.relations.map(({ relation }) => relation));
for (const expected of [
"argument-to-parameter",
"calls",
"captures",
"defines",
"reads",
"returns-to-call",
] as const)
expect(relations.has(expected)).toBe(true);
expect(graph.coverage.families).toHaveLength(12);
expect(graph.limitations).toContain(
"The semantic graph contains static syntax observations and conservative relationship candidates; it does not claim runtime execution.",
);
const parameter = graph.nodes.find(
({ kind, label }) => kind === "parameter" && label === "value",
);
if (parameter === undefined) throw new Error("Expected parameter node");
const provenance = queryJavaScriptSemanticGraph(graph, {
seed: { kind: "semantic-node", node_id: parameter.node_id },
direction: "backward-provenance",
include_ambiguous_dynamic_edges: true,
});
expect(provenance.relations.map(({ relation }) => relation)).toEqual(
expect.arrayContaining([
"aliases",
"argument-to-parameter",
"defines",
"reads",
]),
);
});
it("keeps dynamic calls explicit and returns bounded deterministic pages", () => {
const graph = graphFor(`
const handlers = { ready() { return 1; } };
const key = process.argv[2];
handlers[key]();
`);
expect(graph.unknowns.some(({ reason }) => reason === "dynamic-call")).toBe(
true,
);
const seedNodeId = graph.relations.find(
({ relation }) => relation === "defines",
)?.source_node_id;
expect(seedNodeId).toBeDefined();
if (seedNodeId === undefined) throw new Error("Expected a definition seed");
const first = queryJavaScriptSemanticGraph(graph, {
seed: { kind: "semantic-node", node_id: seedNodeId },
direction: "forward-influence",
include_ambiguous_dynamic_edges: true,
limits: {
max_seed_matches: 25,
max_nodes: 2_000,
max_edges: 4_000,
max_depth: 12,
max_functions: 500,
max_modules: 500,
page_size: 1,
},
});
const repeated = queryJavaScriptSemanticGraph(graph, {
seed: { kind: "semantic-node", node_id: seedNodeId },
direction: "forward-influence",
include_ambiguous_dynamic_edges: true,
limits: first.applied_limits,
});
expect(repeated).toEqual(first);
});
it("keeps ambiguous interprocedural flow out of the default traversal", () => {
const graph = graphFor(`
function left(value) { return value; }
function right(value) { return value; }
const input = 1;
const selected = Math.random() ? left : right;
selected(input);
`);
const input = graph.nodes.find(
({ kind, label }) => kind === "binding" && label === "input",
);
if (input === undefined) throw new Error("Expected input binding");
const query = {
seed: { kind: "semantic-node" as const, node_id: input.node_id },
direction: "forward-influence" as const,
allowed_relations: [
"reads" as const,
"aliases" as const,
"argument-to-parameter" as const,
],
};
const strict = queryJavaScriptSemanticGraph(graph, query);
expect(
strict.relations.some(
({ relation }) => relation === "argument-to-parameter",
),
).toBe(false);
expect(strict.status).toBe("ambiguous");
const admitted = queryJavaScriptSemanticGraph(graph, {
...query,
include_ambiguous_dynamic_edges: true,
});
expect(
admitted.relations.filter(
({ relation }) => relation === "argument-to-parameter",
),
).toHaveLength(2);
expect(admitted.status).toBe("ambiguous");
});
it("connects caller results, direct returns, parameters, and captures", () => {
const graph = graphFor(`
const outer = 2;
function identity(value) { return value; }
function readOuter() { return outer; }
const input = 40;
const output = identity(input);
`);
const binding = (label: string) =>
graph.nodes.find(
({ kind, label: nodeLabel }) =>
["binding", "parameter"].includes(kind) && nodeLabel === label,
);
const output = binding("output");
const input = binding("input");
const outer = binding("outer");
const readOuter = graph.nodes.find(
({ kind, label }) => kind === "function" && label === "readOuter",
);
if (
output === undefined ||
input === undefined ||
outer === undefined ||
readOuter === undefined
)
throw new Error("Expected semantic flow nodes");
const provenance = queryJavaScriptSemanticGraph(graph, {
seed: { kind: "semantic-node", node_id: output.node_id },
direction: "backward-provenance",
});
expect(provenance.nodes.map(({ node_id }) => node_id)).toContain(
input.node_id,
);
const influence = queryJavaScriptSemanticGraph(graph, {
seed: { kind: "semantic-node", node_id: outer.node_id },
direction: "forward-influence",
allowed_relations: ["captures"],
});
expect(influence.nodes.map(({ node_id }) => node_id)).toContain(
readOuter.node_id,
);
const capture = graph.relations.find(
({ relation }) => relation === "captures",
);
expect(capture?.evidence.location).toMatchObject({
available: true,
value: { kind: "source-range", source: "app.js" },
});
expect(capture?.evidence.location).not.toEqual(readOuter.evidence.location);
});
it("preserves semantic analyzer truncation in graph coverage", () => {
const graph = graphFor(
"const first = 1; const second = first; const third = second;",
{ maxReferences: 1 },
);
expect(graph.coverage).toMatchObject({
status: "partial",
truncated: true,
omitted_nodes: null,
omitted_relations: null,
limits: expect.arrayContaining([
{ name: "semantic.maxReferences", value: 1, unit: "items" },
]),
});
expect(
graph.coverage.families.find(
({ family }) => family === "promise-ownership",
),
).toMatchObject({ status: "unsupported", retained_relations: 0 });
});
it("does not promote transformed return expressions to resolved aliases", () => {
const graph = graphFor(`
function increment(value) { return value + 1; }
const input = 1;
const output = increment(input);
`);
const returnNode = graph.nodes.find(({ kind }) => kind === "return-site");
if (returnNode === undefined) throw new Error("Expected return site");
expect(
graph.relations.filter(
({ relation, target_node_id }) =>
relation === "aliases" && target_node_id === returnNode.node_id,
),
).toEqual([expect.objectContaining({ resolution: "candidate" })]);
const output = graph.nodes.find(
({ kind, label }) => kind === "binding" && label === "output",
);
const input = graph.nodes.find(
({ kind, label }) => kind === "binding" && label === "input",
);
if (output === undefined || input === undefined)
throw new Error("Expected caller bindings");
const strict = queryJavaScriptSemanticGraph(graph, {
seed: { kind: "semantic-node", node_id: output.node_id },
direction: "backward-provenance",
});
expect(strict.nodes.map(({ node_id }) => node_id)).not.toContain(
input.node_id,
);
expect(strict.status).toBe("ambiguous");
});
it("uses unavailable evidence when no source produced semantic IR", () => {
const graph = buildJavaScriptSemanticGraph({
rootArtifactSha256: SHA256,
applicationGraph: { graph_id: GRAPH_ID, nodes: [] },
analysis: emptyAnalysis(),
});
expect(graph.nodes).toHaveLength(1);
expect(graph.nodes[0]?.evidence).toMatchObject({
authority: "unknown",
state: "unavailable",
location: { available: false, reason: "not-observed" },
});
});
});
const emptyAnalysis = (): JavaScriptArtifactAnalysis => ({
files: [],
packages: [],
json_modules: [],
html_scripts: [],
source_maps: [],
visited_ast_nodes: 0,
findings: 0,
modules: 0,
parse_failures: 0,
truncated_scopes: 0,
limitations: [],
});
const graphFor = (
source: string,
inputLimits: Partial<JavaScriptSemanticLimits> = {},
) => {
const file: JavaScriptArtifactFile = {
path: "app.js",
container_sha256: SHA256,
sha256: SHA256,
bytes: Buffer.byteLength(source),
inventory_artifact_id: `art_${SHA256}`,
kind: "javascript",
unpacked: false,
text: { included: true, value: source },
};
const semanticLimits = {
...DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS,
...inputLimits,
};
const analysis: JavaScriptArtifactAnalysis = {
files: [
{
file,
javascript: null,
semantic: {
ir: analyzeJavaScriptSemantics(source, inputLimits),
limits: semanticLimits,
},
},
],
packages: [],
json_modules: [],
html_scripts: [],
source_maps: [],
visited_ast_nodes: 0,
findings: 0,
modules: 0,
parse_failures: 0,
truncated_scopes: 0,
limitations: [],
};
return buildJavaScriptSemanticGraph({
rootArtifactSha256: SHA256,
applicationGraph: { graph_id: GRAPH_ID, nodes: [] },
analysis,
});
};

View File

@ -12,11 +12,13 @@ import {
createJavaScriptSemanticGraphNode,
createJavaScriptSemanticGraphRelation,
createJavaScriptSemanticGraphUnknown,
parseJavaScriptSemanticGraph,
serializeJavaScriptSemanticGraph,
type JavaScriptSemanticGraph,
type JavaScriptSemanticGraphNode,
} from "../src/domain/javascriptSemanticGraph.js";
import {
parseJavaScriptSemanticGraph,
serializeJavaScriptSemanticGraph,
} from "../src/domain/javascriptSemanticGraphSerialization.js";
import { queryJavaScriptSemanticGraph } from "../src/domain/javascriptSemanticQuery.js";
import { javaScriptSemanticQueryInputSchema } from "../src/domain/javascriptSemanticQuerySchemas.js";
@ -301,10 +303,45 @@ describe("JavaScript semantic relation graph", () => {
coverage: { ...graph.coverage, omitted_nodes: 1 },
}),
).toThrow(/Complete graph coverage/u);
const graphWithUnknown = fixtureGraph(true);
const { graph_id: _unknownGraphId, ...unknownGraphInput } =
graphWithUnknown;
const originalUnknown = graphWithUnknown.unknowns[0];
if (originalUnknown === undefined)
throw new Error("Expected unknown frontier");
const { unknown_id: _unknownId, ...unknownInput } = originalUnknown;
expect(() =>
createJavaScriptSemanticGraph({
...unknownGraphInput,
unknowns: [
createJavaScriptSemanticGraphUnknown({
...unknownInput,
candidate_node_ids: [`jsrg_node_${"f".repeat(64)}`],
}),
],
}),
).toThrow(/candidate node is absent/u);
});
});
describe("JavaScript semantic query", () => {
it("bounds relevant unknown frontiers independently from relation pages", () => {
const graph = fixtureGraph(true);
const callable = graph.nodes.find(({ kind }) => kind === "function");
if (callable === undefined) throw new Error("Expected callable node");
const result = queryJavaScriptSemanticGraph(graph, {
seed: { kind: "semantic-node", node_id: callable.node_id },
direction: "forward-influence",
limits: { max_unknowns: 0 },
});
expect(result).toMatchObject({
status: "truncated",
unknowns: [],
summary: { relevant_unknowns: 1, retained_unknowns: 0 },
coverage: { status: "truncated" },
});
});
it("traces deterministic forward influence and pages without changing identity", () => {
const graph = fixtureGraph();
const input = {
@ -349,7 +386,7 @@ describe("JavaScript semantic query", () => {
seed: { kind: "literal", value: "TOKEN" },
direction: "forward-influence",
});
expect(result.status).toBe("partial");
expect(result.status).toBe("found");
expect(result.coverage.status).toBe("partial");
expect(result.unknowns).toMatchObject([
{ reason: "dynamic-call", relation_kinds: ["calls"] },

View File

@ -120,6 +120,7 @@ describe("canonical product catalog", () => {
"query_reconstruction_coverage",
"run_controlled_replay",
"trace_application_feature",
"trace_javascript_semantics",
]);
expect(
catalog.providers.find(({ id }) => id === MANAGED_WORKFLOW_PROVIDER.id)

View File

@ -26,10 +26,10 @@ describe("server and catalog identity", () => {
});
expect(PRODUCT_IDENTITY.packageVersion).toBe(packageJson.version);
expect(SDK_IDENTITY.server).toBe("2.0.0-beta.4");
expect(CLI_COMMAND_NAMES).toHaveLength(57);
expect(new Set(CLI_COMMAND_NAMES).size).toBe(57);
expect(CLI_COMMAND_NAMES).toHaveLength(58);
expect(new Set(CLI_COMMAND_NAMES).size).toBe(58);
expect(CATALOG_IDENTITY.counts).toEqual({
cli_commands: 57,
cli_commands: 58,
mcp_tools: TOOL_CONTRACTS.length,
mcp_prompts: 6,
mcp_resources: 2,

View File

@ -50,6 +50,7 @@ describe("tool contract inventory", () => {
]);
expect(APPLICATION_TOOL_CONTRACTS.map(({ name }) => name)).toEqual([
"trace_application_feature",
"trace_javascript_semantics",
"compare_application_versions",
"compare_javascript_export_shapes",
"run_controlled_replay",