feat(javascript): expose bounded semantic tracing
This commit is contained in:
parent
8678b57b5e
commit
508a9ccde9
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 مع وكلاء برمجة آخرين
|
||||
|
|
|
|||
|
|
@ -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`、証拠バンドル、有限リプレイ状態機械、プロセス/成果物/関数比較、残余未知レジストリ |
|
||||
|
||||
## 他のコーディングエージェントで使う
|
||||
|
|
|
|||
|
|
@ -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 상태 머신, 프로세스·아티팩트·함수 비교, 잔여 미지수 레지스트리 |
|
||||
|
||||
## 다른 코딩 에이전트에서 사용하기
|
||||
|
|
|
|||
|
|
@ -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`、证据包、有限重放状态机、进程、制品与函数比较、残余未知项注册表 |
|
||||
|
||||
## 与其他编程智能体一起使用
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
],
|
||||
[
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
|
|
@ -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");
|
||||
|
|
@ -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."],
|
||||
});
|
||||
|
|
@ -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" }));
|
||||
};
|
||||
|
|
@ -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")}`;
|
||||
|
|
@ -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 }),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
>;
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
@ -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: [] },
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
? []
|
||||
: [
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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);
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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." }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
>;
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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":[',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
|
@ -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"] },
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue