Merge pull request #420 from morluto/codex/issue-404-integration
feat(javascript): add bounded semantic tracing
This commit is contained in:
commit
56c1a669d7
|
|
@ -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 ||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import {
|
|||
collectSemanticReturns,
|
||||
resolveSemanticModuleCallables,
|
||||
} from "./javascriptSemanticReturns.js";
|
||||
import { collectJavaScriptSemanticCalls } from "./javascriptSemanticCalls.js";
|
||||
import {
|
||||
compareCodePoints,
|
||||
propertyName,
|
||||
|
|
@ -86,14 +87,16 @@ export const analyzeJavaScriptSemantics = (
|
|||
const bindings = immutableSemanticBindings(state);
|
||||
const callables = collectSemanticReturns(file.program, state, parserPartial);
|
||||
const moduleLinks = resolveSemanticModuleCallables(state, callables);
|
||||
const calls = collectJavaScriptSemanticCalls(file.program, state, callables);
|
||||
return {
|
||||
schema: "JavaScriptSemanticIR",
|
||||
schemaVersion: 2,
|
||||
schemaVersion: 3,
|
||||
scopes: immutableSemanticScopes(state),
|
||||
bindings,
|
||||
callables,
|
||||
references,
|
||||
moduleLinks,
|
||||
...calls,
|
||||
coverage: {
|
||||
status:
|
||||
state.limitsReached.size > 0
|
||||
|
|
@ -115,6 +118,7 @@ export const analyzeJavaScriptSemantics = (
|
|||
: ["Semantic recovery stopped retaining facts at explicit limits."]),
|
||||
"Values and aliases were recovered from inert syntax only; no JavaScript was executed.",
|
||||
"Return sites include only direct callable returns; nested callable returns remain separate.",
|
||||
"Local call, argument, return, and closure relations are static candidates and do not prove runtime invocation.",
|
||||
"Cross-function mutation and dynamic property resolution remain unknown.",
|
||||
],
|
||||
};
|
||||
|
|
@ -139,6 +143,7 @@ const createState = (
|
|||
scopeByNode: new WeakMap([[program, root]]),
|
||||
bindingsById: new Map(),
|
||||
callables: [],
|
||||
callableNodesById: new Map(),
|
||||
moduleLinks: [],
|
||||
limitsReached: new Set(),
|
||||
omittedCount: 0,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,229 @@
|
|||
import * as t from "@babel/types";
|
||||
|
||||
import type {
|
||||
JavaScriptSemanticArgumentFlow,
|
||||
JavaScriptSemanticCallable,
|
||||
} from "./javascriptSemanticIr.js";
|
||||
import {
|
||||
reachSemanticLimit,
|
||||
semanticCallableIdForNode,
|
||||
} from "./javascriptSemanticProjection.js";
|
||||
import {
|
||||
resolveSemanticBindingState,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
type JavaScriptSemanticBindingState,
|
||||
} from "./javascriptSemanticState.js";
|
||||
import { compareCodePoints, range } from "./javascriptStaticAnalysisHelpers.js";
|
||||
|
||||
/** Resolution of one call target within the currently analyzed source. */
|
||||
export interface LocalCallableResolution {
|
||||
readonly callableIds: readonly string[];
|
||||
readonly complete: boolean;
|
||||
readonly reason: string;
|
||||
}
|
||||
|
||||
interface CallResolutionContext {
|
||||
readonly state: JavaScriptSemanticAnalysisState;
|
||||
readonly callableById: ReadonlyMap<string, JavaScriptSemanticCallable>;
|
||||
readonly seenBindings: ReadonlySet<string>;
|
||||
readonly depth: number;
|
||||
readonly bindingCache: Map<string, LocalCallableResolution>;
|
||||
}
|
||||
|
||||
/** Inputs needed to resolve one local call target. */
|
||||
export interface ResolveLocalCallablesInput {
|
||||
readonly node: t.Node;
|
||||
readonly state: JavaScriptSemanticAnalysisState;
|
||||
readonly callableById: ReadonlyMap<string, JavaScriptSemanticCallable>;
|
||||
readonly seenBindings: ReadonlySet<string>;
|
||||
readonly depth: number;
|
||||
readonly bindingCache: Map<string, LocalCallableResolution>;
|
||||
}
|
||||
|
||||
/** Resolve bounded local callable candidates without following dynamic properties. */
|
||||
export const resolveLocalCallables = (
|
||||
input: ResolveLocalCallablesInput,
|
||||
): LocalCallableResolution =>
|
||||
resolveCallables(input.node, {
|
||||
state: input.state,
|
||||
callableById: input.callableById,
|
||||
seenBindings: input.seenBindings,
|
||||
depth: input.depth,
|
||||
bindingCache: input.bindingCache,
|
||||
});
|
||||
|
||||
const resolveCallables = (
|
||||
node: t.Node,
|
||||
context: CallResolutionContext,
|
||||
): LocalCallableResolution => {
|
||||
if (context.depth >= context.state.limits.maxValueDepth) {
|
||||
reachSemanticLimit(context.state, "maxValueDepth");
|
||||
return {
|
||||
callableIds: [],
|
||||
complete: false,
|
||||
reason: "Call target depth limit reached.",
|
||||
};
|
||||
}
|
||||
const direct = semanticCallableIdForNode(node);
|
||||
if (direct !== null && context.callableById.has(direct))
|
||||
return {
|
||||
callableIds: [direct],
|
||||
complete: true,
|
||||
reason: "Direct local callable.",
|
||||
};
|
||||
if (t.isIdentifier(node)) return resolveIdentifier(node, context);
|
||||
if (
|
||||
t.isTSAsExpression(node) ||
|
||||
t.isTSTypeAssertion(node) ||
|
||||
t.isTSNonNullExpression(node)
|
||||
)
|
||||
return resolveCallables(node.expression, nestedContext(context));
|
||||
if (t.isConditionalExpression(node) || t.isLogicalExpression(node))
|
||||
return resolveAlternatives(
|
||||
t.isConditionalExpression(node) ? node.consequent : node.left,
|
||||
t.isConditionalExpression(node) ? node.alternate : node.right,
|
||||
context,
|
||||
);
|
||||
return {
|
||||
callableIds: [],
|
||||
complete: false,
|
||||
reason: `Unsupported ${node.type} call target.`,
|
||||
};
|
||||
};
|
||||
|
||||
const resolveIdentifier = (
|
||||
node: t.Identifier,
|
||||
context: CallResolutionContext,
|
||||
): LocalCallableResolution => {
|
||||
const binding = resolveSemanticBindingState(context.state, node, node.name);
|
||||
return binding === undefined
|
||||
? {
|
||||
callableIds: [],
|
||||
complete: false,
|
||||
reason: `Unresolved call target ${node.name}.`,
|
||||
}
|
||||
: resolveBindingCallables(binding, nestedContext(context));
|
||||
};
|
||||
|
||||
const resolveAlternatives = (
|
||||
left: t.Node,
|
||||
right: t.Node,
|
||||
context: CallResolutionContext,
|
||||
): LocalCallableResolution => {
|
||||
const candidates = [left, right].map((candidate) =>
|
||||
resolveCallables(candidate, nestedContext(context)),
|
||||
);
|
||||
return {
|
||||
callableIds: uniqueCallableIds(
|
||||
candidates.flatMap(({ callableIds }) => callableIds),
|
||||
),
|
||||
complete: candidates.every(({ complete }) => complete),
|
||||
reason: "Conditional or logical call target has multiple candidates.",
|
||||
};
|
||||
};
|
||||
|
||||
const resolveBindingCallables = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
context: CallResolutionContext,
|
||||
): LocalCallableResolution => {
|
||||
const cached = context.bindingCache.get(binding.bindingId);
|
||||
if (cached !== undefined) return cached;
|
||||
if (context.seenBindings.has(binding.bindingId))
|
||||
return {
|
||||
callableIds: [],
|
||||
complete: false,
|
||||
reason: `Call target alias cycle at ${binding.name}.`,
|
||||
};
|
||||
if (binding.initializers.length === 0)
|
||||
return missingBindingInitializer(binding);
|
||||
const candidateContext = {
|
||||
...nestedContext(context),
|
||||
seenBindings: new Set([...context.seenBindings, binding.bindingId]),
|
||||
};
|
||||
const candidates = binding.initializers.map(({ node, projection }) =>
|
||||
projection.length === 0
|
||||
? resolveCallables(node, candidateContext)
|
||||
: {
|
||||
callableIds: [],
|
||||
complete: false,
|
||||
reason: `Projected call target ${binding.name} is unresolved.`,
|
||||
},
|
||||
);
|
||||
const callableIds = uniqueCallableIds(
|
||||
candidates.flatMap((candidate) => candidate.callableIds),
|
||||
);
|
||||
const resolution = {
|
||||
callableIds,
|
||||
complete: candidates.every(({ complete }) => complete),
|
||||
reason:
|
||||
callableIds.length === 1 && candidates.every(({ complete }) => complete)
|
||||
? "Unique local callable target."
|
||||
: `Call target ${binding.name} has ambiguous or unsupported assignments.`,
|
||||
};
|
||||
if (resolution.complete)
|
||||
context.bindingCache.set(binding.bindingId, resolution);
|
||||
return resolution;
|
||||
};
|
||||
|
||||
const missingBindingInitializer = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
): LocalCallableResolution => ({
|
||||
callableIds: [],
|
||||
complete: false,
|
||||
reason:
|
||||
binding.kind === "import"
|
||||
? `Call target ${binding.name} is imported from outside this source.`
|
||||
: `Call target ${binding.name} has no local initializer.`,
|
||||
});
|
||||
|
||||
/** Find all bindings introduced by one positional parameter pattern. */
|
||||
export const parameterBindings = (
|
||||
callable: JavaScriptSemanticCallable,
|
||||
index: number,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): JavaScriptSemanticBindingState[] => {
|
||||
if (callable.bodyScopeId === null) return [];
|
||||
const scope = state.scopesById.get(callable.bodyScopeId);
|
||||
if (scope === undefined) return [];
|
||||
return [...scope.bindings.values()]
|
||||
.filter(
|
||||
(binding) =>
|
||||
binding.kind === "parameter" &&
|
||||
binding.definitions.some(
|
||||
({ kind, location }) =>
|
||||
kind === "parameter" &&
|
||||
parameterIndex(callable, location, state) === index,
|
||||
),
|
||||
)
|
||||
.sort((left, right) => compareCodePoints(left.bindingId, right.bindingId));
|
||||
};
|
||||
|
||||
const parameterIndex = (
|
||||
callable: JavaScriptSemanticCallable,
|
||||
location: JavaScriptSemanticArgumentFlow["parameterLocation"],
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): number => {
|
||||
const node = state.callableNodesById.get(callable.callableId);
|
||||
if (!t.isFunction(node)) return -1;
|
||||
return node.params.findIndex((parameter) =>
|
||||
rangeContains(range(parameter), location),
|
||||
);
|
||||
};
|
||||
|
||||
const rangeContains = (
|
||||
outer: JavaScriptSemanticArgumentFlow["parameterLocation"],
|
||||
inner: JavaScriptSemanticArgumentFlow["parameterLocation"],
|
||||
): boolean =>
|
||||
(outer.start.line < inner.start.line ||
|
||||
(outer.start.line === inner.start.line &&
|
||||
outer.start.column <= inner.start.column)) &&
|
||||
(outer.end.line > inner.end.line ||
|
||||
(outer.end.line === inner.end.line &&
|
||||
outer.end.column >= inner.end.column));
|
||||
|
||||
const nestedContext = (
|
||||
context: CallResolutionContext,
|
||||
): CallResolutionContext => ({ ...context, depth: context.depth + 1 });
|
||||
|
||||
const uniqueCallableIds = (values: readonly string[]): string[] =>
|
||||
[...new Set(values)].sort(compareCodePoints);
|
||||
|
|
@ -0,0 +1,431 @@
|
|||
import * as t from "@babel/types";
|
||||
|
||||
import type {
|
||||
JavaScriptSemanticArgumentFlow,
|
||||
JavaScriptSemanticCallable,
|
||||
JavaScriptSemanticCallResultFlow,
|
||||
JavaScriptSemanticCallReturnFlow,
|
||||
JavaScriptSemanticCallSite,
|
||||
JavaScriptSemanticClosureCapture,
|
||||
JavaScriptSemanticFrontier,
|
||||
} from "./javascriptSemanticIr.js";
|
||||
import {
|
||||
reachSemanticLimit,
|
||||
semanticCallableIdForNode,
|
||||
semanticReferenceRole,
|
||||
semanticStaticPropertyName,
|
||||
} from "./javascriptSemanticProjection.js";
|
||||
import {
|
||||
resolveSemanticBindingState,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
type JavaScriptSemanticBindingState,
|
||||
} from "./javascriptSemanticState.js";
|
||||
import {
|
||||
type LocalCallableResolution,
|
||||
parameterBindings,
|
||||
resolveLocalCallables,
|
||||
} from "./javascriptSemanticCallResolution.js";
|
||||
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
|
||||
import { range } from "./javascriptStaticAnalysisHelpers.js";
|
||||
|
||||
/** Local call, flow, capture, and unsupported-frontier facts. */
|
||||
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[];
|
||||
}
|
||||
|
||||
interface MutableCallAnalysis {
|
||||
readonly callSites: JavaScriptSemanticCallSite[];
|
||||
readonly argumentFlows: JavaScriptSemanticArgumentFlow[];
|
||||
readonly callReturnFlows: JavaScriptSemanticCallReturnFlow[];
|
||||
readonly callResultFlows: JavaScriptSemanticCallResultFlow[];
|
||||
readonly closureCaptures: JavaScriptSemanticClosureCapture[];
|
||||
readonly frontiers: JavaScriptSemanticFrontier[];
|
||||
retainedArguments: number;
|
||||
}
|
||||
|
||||
interface CallCollectionContext {
|
||||
readonly state: JavaScriptSemanticAnalysisState;
|
||||
readonly callableById: ReadonlyMap<string, JavaScriptSemanticCallable>;
|
||||
readonly output: MutableCallAnalysis;
|
||||
readonly bindingResolutionCache: Map<string, LocalCallableResolution>;
|
||||
readonly parameterBindingCache: Map<
|
||||
string,
|
||||
readonly JavaScriptSemanticBindingState[]
|
||||
>;
|
||||
}
|
||||
|
||||
/** Recover bounded direct-call and lexical-capture candidates from inert syntax. */
|
||||
export const collectJavaScriptSemanticCalls = (
|
||||
program: t.Program,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
callables: readonly JavaScriptSemanticCallable[],
|
||||
): JavaScriptSemanticCallAnalysis => {
|
||||
const output: MutableCallAnalysis = {
|
||||
callSites: [],
|
||||
argumentFlows: [],
|
||||
callReturnFlows: [],
|
||||
callResultFlows: [],
|
||||
closureCaptures: [],
|
||||
frontiers: [],
|
||||
retainedArguments: 0,
|
||||
};
|
||||
const callableById = new Map(
|
||||
callables.map((callable) => [callable.callableId, callable]),
|
||||
);
|
||||
const context: CallCollectionContext = {
|
||||
state,
|
||||
callableById,
|
||||
output,
|
||||
bindingResolutionCache: new Map(),
|
||||
parameterBindingCache: new Map(),
|
||||
};
|
||||
const callableStack: JavaScriptSemanticCallable[] = [];
|
||||
traverseJavaScriptAst(program, {
|
||||
enter: (node, parent) => {
|
||||
const callableId = semanticCallableIdForNode(node);
|
||||
const callable =
|
||||
callableId === null ? undefined : callableById.get(callableId);
|
||||
if (callable !== undefined) callableStack.push(callable);
|
||||
const owner = enclosingFunction(callableStack);
|
||||
collectCapture(node, parent, owner, context);
|
||||
collectDynamicProperty(node, owner, state, output);
|
||||
if (
|
||||
t.isCallExpression(node) ||
|
||||
t.isOptionalCallExpression(node) ||
|
||||
t.isNewExpression(node)
|
||||
)
|
||||
collectCallSite(node, parent, owner, context);
|
||||
},
|
||||
exit: (node) => {
|
||||
const callableId = semanticCallableIdForNode(node);
|
||||
if (
|
||||
callableId !== null &&
|
||||
callableStack.at(-1)?.callableId === callableId
|
||||
)
|
||||
callableStack.pop();
|
||||
},
|
||||
});
|
||||
return {
|
||||
callSites: output.callSites,
|
||||
argumentFlows: output.argumentFlows,
|
||||
callReturnFlows: output.callReturnFlows,
|
||||
callResultFlows: output.callResultFlows,
|
||||
closureCaptures: output.closureCaptures,
|
||||
frontiers: output.frontiers,
|
||||
};
|
||||
};
|
||||
|
||||
const collectCallSite = (
|
||||
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
|
||||
parent: t.Node | null,
|
||||
owner: JavaScriptSemanticCallable | undefined,
|
||||
context: CallCollectionContext,
|
||||
): void => {
|
||||
const { state, callableById, output } = context;
|
||||
if (output.callSites.length >= state.limits.maxCallSites) {
|
||||
reachSemanticLimit(state, "maxCallSites");
|
||||
return;
|
||||
}
|
||||
const resolution = resolveLocalCallables({
|
||||
node: node.callee,
|
||||
state,
|
||||
callableById,
|
||||
seenBindings: new Set(),
|
||||
depth: 0,
|
||||
bindingCache: context.bindingResolutionCache,
|
||||
});
|
||||
const callSiteId = semanticCallSiteId(node);
|
||||
const argumentsValue = retainedArguments(node.arguments, state, output);
|
||||
const site: JavaScriptSemanticCallSite = {
|
||||
callSiteId,
|
||||
kind: t.isNewExpression(node) ? "construct" : "call",
|
||||
callerCallableId: owner?.callableId ?? null,
|
||||
location: range(node),
|
||||
calleeLocation: range(node.callee),
|
||||
resolution:
|
||||
resolution.callableIds.length === 1 && resolution.complete
|
||||
? "exact"
|
||||
: resolution.callableIds.length > 0
|
||||
? "ambiguous"
|
||||
: "unresolved",
|
||||
calleeCallableIds: resolution.callableIds,
|
||||
arguments: argumentsValue,
|
||||
};
|
||||
output.callSites.push(site);
|
||||
collectCallResultFlow(site, node, parent, context);
|
||||
if (site.resolution !== "exact")
|
||||
addFrontier(
|
||||
{
|
||||
kind: "dynamic-call",
|
||||
callableId: owner?.callableId ?? null,
|
||||
location: range(node.callee),
|
||||
reason: resolution.reason,
|
||||
},
|
||||
state,
|
||||
output,
|
||||
);
|
||||
collectArgumentFlows(site, node, context);
|
||||
if (site.kind === "call")
|
||||
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
|
||||
| t.SpreadElement
|
||||
| t.JSXNamespacedName
|
||||
| t.ArgumentPlaceholder
|
||||
)[],
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
output: MutableCallAnalysis,
|
||||
): JavaScriptSemanticCallSite["arguments"] => {
|
||||
const retained: JavaScriptSemanticCallSite["arguments"][number][] = [];
|
||||
for (const [index, node] of nodes.entries()) {
|
||||
if (output.retainedArguments >= state.limits.maxCallArguments) {
|
||||
reachSemanticLimit(state, "maxCallArguments");
|
||||
break;
|
||||
}
|
||||
output.retainedArguments += 1;
|
||||
retained.push({
|
||||
index,
|
||||
location: range(node),
|
||||
spread: t.isSpreadElement(node),
|
||||
});
|
||||
}
|
||||
return retained;
|
||||
};
|
||||
|
||||
const collectArgumentFlows = (
|
||||
site: JavaScriptSemanticCallSite,
|
||||
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
|
||||
context: CallCollectionContext,
|
||||
): void => {
|
||||
const { state, callableById, output } = context;
|
||||
const retainedIndexes = new Set(site.arguments.map(({ index }) => index));
|
||||
let positionIsExact = true;
|
||||
for (const [index, argument] of node.arguments.entries()) {
|
||||
if (t.isSpreadElement(argument)) {
|
||||
positionIsExact = false;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
!positionIsExact ||
|
||||
!retainedIndexes.has(index) ||
|
||||
!t.isExpression(argument)
|
||||
)
|
||||
continue;
|
||||
for (const callableId of site.calleeCallableIds) {
|
||||
const callable = callableById.get(callableId);
|
||||
if (callable === undefined) continue;
|
||||
for (const parameter of cachedParameterBindings(
|
||||
callable,
|
||||
index,
|
||||
context,
|
||||
)) {
|
||||
if (output.argumentFlows.length >= state.limits.maxArgumentFlows) {
|
||||
reachSemanticLimit(state, "maxArgumentFlows");
|
||||
return;
|
||||
}
|
||||
const definition = parameter.definitions.find(
|
||||
({ kind }) => kind === "parameter",
|
||||
);
|
||||
if (definition === undefined) continue;
|
||||
output.argumentFlows.push({
|
||||
callSiteId: site.callSiteId,
|
||||
argumentIndex: index,
|
||||
argumentLocation: range(argument),
|
||||
callableId,
|
||||
parameterBindingId: parameter.bindingId,
|
||||
parameterLocation: definition.location,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cachedParameterBindings = (
|
||||
callable: JavaScriptSemanticCallable,
|
||||
index: number,
|
||||
context: CallCollectionContext,
|
||||
): readonly JavaScriptSemanticBindingState[] => {
|
||||
const key = `${callable.callableId}:${String(index)}`;
|
||||
const cached = context.parameterBindingCache.get(key);
|
||||
if (cached !== undefined) return cached;
|
||||
const bindings = parameterBindings(callable, index, context.state);
|
||||
context.parameterBindingCache.set(key, bindings);
|
||||
return bindings;
|
||||
};
|
||||
|
||||
const collectReturnFlows = (
|
||||
site: JavaScriptSemanticCallSite,
|
||||
callableById: ReadonlyMap<string, JavaScriptSemanticCallable>,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
output: MutableCallAnalysis,
|
||||
): void => {
|
||||
for (const callableId of site.calleeCallableIds) {
|
||||
const callable = callableById.get(callableId);
|
||||
if (callable === undefined) continue;
|
||||
for (const returnSite of callable.returnSites) {
|
||||
if (output.callReturnFlows.length >= state.limits.maxCallReturnFlows) {
|
||||
reachSemanticLimit(state, "maxCallReturnFlows");
|
||||
return;
|
||||
}
|
||||
output.callReturnFlows.push({
|
||||
callSiteId: site.callSiteId,
|
||||
callableId,
|
||||
returnSiteId: returnSite.returnSiteId,
|
||||
returnLocation: returnSite.location,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const collectCapture = (
|
||||
node: t.Node,
|
||||
parent: t.Node | null,
|
||||
owner: JavaScriptSemanticCallable | undefined,
|
||||
context: CallCollectionContext,
|
||||
): void => {
|
||||
const { state, output } = context;
|
||||
if (
|
||||
!t.isIdentifier(node) ||
|
||||
parent === null ||
|
||||
owner === undefined ||
|
||||
owner.bodyScopeId === null
|
||||
)
|
||||
return;
|
||||
if (semanticReferenceRole(node, parent) === null) return;
|
||||
const binding = resolveSemanticBindingState(state, node, node.name);
|
||||
if (
|
||||
binding === undefined ||
|
||||
bindingIsWithinCallable(binding, owner.bodyScopeId, state)
|
||||
)
|
||||
return;
|
||||
if (output.closureCaptures.length >= state.limits.maxClosureCaptures) {
|
||||
reachSemanticLimit(state, "maxClosureCaptures");
|
||||
return;
|
||||
}
|
||||
output.closureCaptures.push({
|
||||
callableId: owner.callableId,
|
||||
bindingId: binding.bindingId,
|
||||
referenceLocation: range(node),
|
||||
});
|
||||
};
|
||||
|
||||
const collectDynamicProperty = (
|
||||
node: t.Node,
|
||||
owner: JavaScriptSemanticCallable | undefined,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
output: MutableCallAnalysis,
|
||||
): void => {
|
||||
if (
|
||||
(t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) &&
|
||||
semanticStaticPropertyName(node.property, node.computed) === ""
|
||||
)
|
||||
addFrontier(
|
||||
{
|
||||
kind: "dynamic-property",
|
||||
callableId: owner?.callableId ?? null,
|
||||
location: range(node.property),
|
||||
reason: "Computed member property is not a static string or number.",
|
||||
},
|
||||
state,
|
||||
output,
|
||||
);
|
||||
else if (
|
||||
(t.isObjectProperty(node) || t.isObjectMethod(node)) &&
|
||||
node.computed &&
|
||||
semanticStaticPropertyName(node.key, true) === ""
|
||||
)
|
||||
addFrontier(
|
||||
{
|
||||
kind: "dynamic-property",
|
||||
callableId: owner?.callableId ?? null,
|
||||
location: range(node.key),
|
||||
reason: "Computed object property is not a static string or number.",
|
||||
},
|
||||
state,
|
||||
output,
|
||||
);
|
||||
};
|
||||
|
||||
const addFrontier = (
|
||||
frontier: JavaScriptSemanticFrontier,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
output: MutableCallAnalysis,
|
||||
): void => {
|
||||
if (output.frontiers.length >= state.limits.maxFrontiers) {
|
||||
reachSemanticLimit(state, "maxFrontiers");
|
||||
return;
|
||||
}
|
||||
output.frontiers.push(frontier);
|
||||
};
|
||||
|
||||
const bindingIsWithinCallable = (
|
||||
binding: JavaScriptSemanticBindingState,
|
||||
bodyScopeId: string,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): boolean => {
|
||||
let scope = state.scopesById.get(binding.scopeId);
|
||||
while (scope !== undefined) {
|
||||
if (scope.scopeId === bodyScopeId) return true;
|
||||
scope =
|
||||
scope.parentScopeId === null
|
||||
? undefined
|
||||
: state.scopesById.get(scope.parentScopeId);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const enclosingFunction = (
|
||||
stack: readonly JavaScriptSemanticCallable[],
|
||||
): JavaScriptSemanticCallable | undefined =>
|
||||
stack.findLast(({ kind }) => kind !== "class");
|
||||
|
||||
const semanticCallSiteId = (
|
||||
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
|
||||
): string =>
|
||||
`call:${t.isNewExpression(node) ? "construct" : "call"}:${String(node.start ?? -1)}:${String(node.end ?? -1)}`;
|
||||
|
|
@ -0,0 +1,522 @@
|
|||
import { createHash } from "node:crypto";
|
||||
|
||||
import canonicalize from "canonicalize";
|
||||
import { z } from "zod";
|
||||
|
||||
import { compareCodePoints } from "./javascriptApplicationGraph.js";
|
||||
import type { ApplicationGraphEvidence } from "./javascriptApplicationEvidenceSchemas.js";
|
||||
import {
|
||||
JAVASCRIPT_SEMANTIC_RELATION_FAMILIES,
|
||||
JAVASCRIPT_SEMANTIC_RELATION_FAMILY,
|
||||
javaScriptSemanticFingerprintInputSchema,
|
||||
javaScriptSemanticFingerprintSchema,
|
||||
javaScriptSemanticGraphInputSchema,
|
||||
javaScriptSemanticGraphRecordSchema,
|
||||
javaScriptSemanticNodeInputSchema,
|
||||
javaScriptSemanticNodeSchema,
|
||||
javaScriptSemanticRelationInputSchema,
|
||||
javaScriptSemanticRelationSchema,
|
||||
javaScriptSemanticUnknownInputSchema,
|
||||
javaScriptSemanticUnknownSchema,
|
||||
type JavaScriptSemanticGraphInput,
|
||||
type JavaScriptSemanticGraphNode,
|
||||
type JavaScriptSemanticGraphRelation,
|
||||
} from "./javascriptSemanticGraphSchemas.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;
|
||||
};
|
||||
|
||||
const digestCanonical = (value: unknown): string =>
|
||||
createHash("sha256").update(canonicalJson(value)).digest("hex");
|
||||
|
||||
const uniqueSorted = <Value extends string>(
|
||||
values: readonly Value[],
|
||||
): Value[] => [...new Set(values)].sort(compareCodePoints);
|
||||
|
||||
const normalizeEvidence = (
|
||||
evidence: ApplicationGraphEvidence,
|
||||
): ApplicationGraphEvidence => ({
|
||||
...evidence,
|
||||
coverage: {
|
||||
...evidence.coverage,
|
||||
limits: [...evidence.coverage.limits].sort((left, right) =>
|
||||
compareCodePoints(canonicalJson(left), canonicalJson(right)),
|
||||
),
|
||||
},
|
||||
limitations: uniqueSorted(evidence.limitations),
|
||||
evidence_ids: uniqueSorted(evidence.evidence_ids),
|
||||
});
|
||||
|
||||
const nodeId = (
|
||||
node: Pick<JavaScriptSemanticGraphNode, "kind" | "identity">,
|
||||
): string =>
|
||||
`jsrg_node_${digestCanonical({ kind: node.kind, identity: node.identity })}`;
|
||||
|
||||
/** Normalize one semantic entity and derive its artifact-version identifier. */
|
||||
export const createJavaScriptSemanticGraphNode = (
|
||||
input: unknown,
|
||||
): JavaScriptSemanticGraphNode => {
|
||||
const parsed = javaScriptSemanticNodeInputSchema.parse(input);
|
||||
const semantic = {
|
||||
...parsed,
|
||||
application_node_ids: uniqueSorted(parsed.application_node_ids),
|
||||
evidence: normalizeEvidence(parsed.evidence),
|
||||
identifier_strategy: {
|
||||
strategy: "semantic-content-sha256" as const,
|
||||
stability: "artifact-version" as const,
|
||||
},
|
||||
};
|
||||
return javaScriptSemanticNodeSchema.parse({
|
||||
...semantic,
|
||||
node_id: nodeId(semantic),
|
||||
});
|
||||
};
|
||||
|
||||
/** Normalize one semantic relationship and derive its exact identifier. */
|
||||
export const createJavaScriptSemanticGraphRelation = (
|
||||
input: unknown,
|
||||
): JavaScriptSemanticGraphRelation => {
|
||||
const parsed = javaScriptSemanticRelationInputSchema.parse(input);
|
||||
const semantic = {
|
||||
...parsed,
|
||||
evidence: normalizeEvidence(parsed.evidence),
|
||||
identifier_strategy: {
|
||||
strategy: "semantic-content-sha256" as const,
|
||||
stability: "relationship-exact" as const,
|
||||
},
|
||||
};
|
||||
return javaScriptSemanticRelationSchema.parse({
|
||||
...semantic,
|
||||
relation_id: `jsrg_relation_${digestCanonical(semantic)}`,
|
||||
});
|
||||
};
|
||||
|
||||
/** Normalize one unresolved semantic frontier and derive its identifier. */
|
||||
export const createJavaScriptSemanticGraphUnknown = (input: unknown) => {
|
||||
const parsed = javaScriptSemanticUnknownInputSchema.parse(input);
|
||||
const semantic = {
|
||||
...parsed,
|
||||
relation_kinds: uniqueSorted(parsed.relation_kinds),
|
||||
candidate_node_ids: uniqueSorted(parsed.candidate_node_ids),
|
||||
evidence: normalizeEvidence(parsed.evidence),
|
||||
};
|
||||
return javaScriptSemanticUnknownSchema.parse({
|
||||
...semantic,
|
||||
unknown_id: `jsrg_unknown_${digestCanonical(semantic)}`,
|
||||
});
|
||||
};
|
||||
|
||||
/** Normalize one function fingerprint and derive its component commitment. */
|
||||
export const createJavaScriptSemanticFingerprint = (input: unknown) => {
|
||||
const parsed = javaScriptSemanticFingerprintInputSchema.parse(input);
|
||||
const semantic = {
|
||||
...parsed,
|
||||
components: {
|
||||
...parsed.components,
|
||||
effects: uniqueSorted(parsed.components.effects),
|
||||
},
|
||||
limitations: uniqueSorted(parsed.limitations),
|
||||
evidence: normalizeEvidence(parsed.evidence),
|
||||
};
|
||||
const fingerprintSha256 = digestCanonical(semantic.components);
|
||||
return javaScriptSemanticFingerprintSchema.parse({
|
||||
...semantic,
|
||||
fingerprint_sha256: fingerprintSha256,
|
||||
fingerprint_id: `jsrg_fingerprint_${digestCanonical({
|
||||
function_node_id: semantic.function_node_id,
|
||||
algorithm: semantic.algorithm,
|
||||
fingerprint_sha256: fingerprintSha256,
|
||||
})}`,
|
||||
});
|
||||
};
|
||||
|
||||
type GraphRecord = z.infer<typeof javaScriptSemanticGraphRecordSchema>;
|
||||
|
||||
const sortedUniqueIssue = (
|
||||
values: readonly string[],
|
||||
path: PropertyKey[],
|
||||
label: string,
|
||||
context: z.RefinementCtx,
|
||||
): void => {
|
||||
for (let index = 1; index < values.length; index += 1) {
|
||||
if (compareCodePoints(values[index - 1] ?? "", values[index] ?? "") < 0)
|
||||
continue;
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: [...path, index],
|
||||
message: `${label} must be sorted and unique`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const checkCoverage = (graph: GraphRecord, context: z.RefinementCtx): void => {
|
||||
const families = graph.coverage.families.map(({ family }) => family);
|
||||
if (
|
||||
canonicalJson(families) !==
|
||||
canonicalJson(JAVASCRIPT_SEMANTIC_RELATION_FAMILIES)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["coverage", "families"],
|
||||
message:
|
||||
"Coverage must name every semantic relation family once in canonical order",
|
||||
});
|
||||
if (
|
||||
graph.coverage.status === "complete" &&
|
||||
(graph.coverage.truncated ||
|
||||
graph.coverage.omitted_nodes !== 0 ||
|
||||
graph.coverage.omitted_relations !== 0 ||
|
||||
graph.coverage.families.some(({ status }) => status !== "complete"))
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["coverage"],
|
||||
message: "Complete graph coverage cannot omit or truncate semantic facts",
|
||||
});
|
||||
if (graph.coverage.truncated && graph.coverage.limits.length === 0)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["coverage", "limits"],
|
||||
message: "Truncated graph coverage must identify an effective limit",
|
||||
});
|
||||
const relationsByFamily = new Map(
|
||||
JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.map((family) => [family, 0]),
|
||||
);
|
||||
for (const relation of graph.relations) {
|
||||
const family = JAVASCRIPT_SEMANTIC_RELATION_FAMILY[relation.relation];
|
||||
relationsByFamily.set(family, (relationsByFamily.get(family) ?? 0) + 1);
|
||||
}
|
||||
const unknownsById = new Map(
|
||||
graph.unknowns.map((unknown) => [unknown.unknown_id, unknown]),
|
||||
);
|
||||
for (const [index, family] of graph.coverage.families.entries()) {
|
||||
if (family.retained_relations !== relationsByFamily.get(family.family))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["coverage", "families", index, "retained_relations"],
|
||||
message: "Family retained relation count does not match graph content",
|
||||
});
|
||||
if (
|
||||
family.unknown_ids.some(
|
||||
(identifier) => unknownsById.get(identifier)?.family !== family.family,
|
||||
)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["coverage", "families", index, "unknown_ids"],
|
||||
message: "Family coverage references an unknown from another family",
|
||||
});
|
||||
if (
|
||||
family.status === "complete" &&
|
||||
(family.omitted_relations !== 0 || family.unknown_ids.length > 0)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["coverage", "families", index],
|
||||
message: "Complete family coverage cannot omit or retain unknown facts",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const checkCanonicalOrder = (
|
||||
graph: GraphRecord,
|
||||
context: z.RefinementCtx,
|
||||
): void => {
|
||||
sortedUniqueIssue(
|
||||
graph.root_node_ids,
|
||||
["root_node_ids"],
|
||||
"Root nodes",
|
||||
context,
|
||||
);
|
||||
sortedUniqueIssue(
|
||||
graph.nodes.map(({ node_id }) => node_id),
|
||||
["nodes"],
|
||||
"Nodes",
|
||||
context,
|
||||
);
|
||||
sortedUniqueIssue(
|
||||
graph.relations.map(({ relation_id }) => relation_id),
|
||||
["relations"],
|
||||
"Relations",
|
||||
context,
|
||||
);
|
||||
sortedUniqueIssue(
|
||||
graph.fingerprints.map(({ fingerprint_id }) => fingerprint_id),
|
||||
["fingerprints"],
|
||||
"Fingerprints",
|
||||
context,
|
||||
);
|
||||
sortedUniqueIssue(
|
||||
graph.unknowns.map(({ unknown_id }) => unknown_id),
|
||||
["unknowns"],
|
||||
"Unknown frontiers",
|
||||
context,
|
||||
);
|
||||
sortedUniqueIssue(graph.limitations, ["limitations"], "Limitations", context);
|
||||
};
|
||||
|
||||
const checkNodes = (
|
||||
graph: GraphRecord,
|
||||
nodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
|
||||
context: z.RefinementCtx,
|
||||
): void => {
|
||||
for (const [index, node] of graph.nodes.entries()) {
|
||||
if (node.node_id !== nodeId(node))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["nodes", index, "node_id"],
|
||||
message: "Node identifier is stale",
|
||||
});
|
||||
if (
|
||||
node.function_node_id !== null &&
|
||||
nodes.get(node.function_node_id)?.kind !== "function"
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["nodes", index, "function_node_id"],
|
||||
message: "Function owner must name a function node",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const checkRoots = (
|
||||
graph: GraphRecord,
|
||||
nodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
|
||||
context: z.RefinementCtx,
|
||||
): void => {
|
||||
for (const root of graph.root_node_ids)
|
||||
if (!nodes.has(root))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["root_node_ids"],
|
||||
message: "Root identifier must name a semantic node",
|
||||
});
|
||||
};
|
||||
|
||||
const checkRelations = (
|
||||
graph: GraphRecord,
|
||||
nodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
|
||||
context: z.RefinementCtx,
|
||||
): void => {
|
||||
for (const [index, relation] of graph.relations.entries()) {
|
||||
const { relation_id: identifier, ...semantic } = relation;
|
||||
if (identifier !== "jsrg_relation_" + digestCanonical(semantic))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["relations", index, "relation_id"],
|
||||
message: "Relation identifier is stale",
|
||||
});
|
||||
if (
|
||||
!nodes.has(relation.source_node_id) ||
|
||||
!nodes.has(relation.target_node_id)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["relations", index],
|
||||
message: "Relation endpoints must name semantic nodes",
|
||||
});
|
||||
if (relation.source_node_id === relation.target_node_id)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["relations", index],
|
||||
message: "Semantic relations cannot be self-referential",
|
||||
});
|
||||
if (
|
||||
relation.resolution === "candidate" &&
|
||||
relation.evidence.state === "observed"
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["relations", index, "evidence", "state"],
|
||||
message: "Candidate relations cannot claim observed resolution",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const checkUnknowns = (
|
||||
graph: GraphRecord,
|
||||
nodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
|
||||
context: z.RefinementCtx,
|
||||
): void => {
|
||||
for (const [index, unknown] of graph.unknowns.entries()) {
|
||||
const { unknown_id: identifier, ...semantic } = unknown;
|
||||
if (identifier !== "jsrg_unknown_" + digestCanonical(semantic))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["unknowns", index, "unknown_id"],
|
||||
message: "Unknown frontier identifier is stale",
|
||||
});
|
||||
if (unknown.node_id !== null && !nodes.has(unknown.node_id))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
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,
|
||||
)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["unknowns", index, "relation_kinds"],
|
||||
message: "Unknown relation kinds must belong to their declared family",
|
||||
});
|
||||
if (!["unknown", "unavailable"].includes(unknown.evidence.state))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["unknowns", index, "evidence", "state"],
|
||||
message: "Unknown frontiers require unknown or unavailable evidence",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const checkUnknownReferences = (
|
||||
graph: GraphRecord,
|
||||
context: z.RefinementCtx,
|
||||
): void => {
|
||||
const unknownIds = new Set(
|
||||
graph.unknowns.map(({ unknown_id }) => unknown_id),
|
||||
);
|
||||
for (const [index, family] of graph.coverage.families.entries())
|
||||
if (family.unknown_ids.some((identifier) => !unknownIds.has(identifier)))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["coverage", "families", index, "unknown_ids"],
|
||||
message: "Family coverage references an absent unknown frontier",
|
||||
});
|
||||
};
|
||||
|
||||
const checkFingerprints = (
|
||||
graph: GraphRecord,
|
||||
nodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
|
||||
context: z.RefinementCtx,
|
||||
): void => {
|
||||
for (const [index, fingerprint] of graph.fingerprints.entries()) {
|
||||
if (nodes.get(fingerprint.function_node_id)?.kind !== "function")
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["fingerprints", index, "function_node_id"],
|
||||
message: "Fingerprint must name a function node",
|
||||
});
|
||||
if (
|
||||
fingerprint.fingerprint_sha256 !== digestCanonical(fingerprint.components)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["fingerprints", index, "fingerprint_sha256"],
|
||||
message: "Fingerprint component commitment is stale",
|
||||
});
|
||||
const expectedIdentifier =
|
||||
"jsrg_fingerprint_" +
|
||||
digestCanonical({
|
||||
function_node_id: fingerprint.function_node_id,
|
||||
algorithm: fingerprint.algorithm,
|
||||
fingerprint_sha256: fingerprint.fingerprint_sha256,
|
||||
});
|
||||
if (fingerprint.fingerprint_id !== expectedIdentifier)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["fingerprints", index, "fingerprint_id"],
|
||||
message: "Fingerprint identifier is stale",
|
||||
});
|
||||
if (
|
||||
fingerprint.status !== "complete" &&
|
||||
fingerprint.limitations.length === 0
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["fingerprints", index, "limitations"],
|
||||
message: "Incomplete fingerprints require a limitation",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const checkGraph = (graph: GraphRecord, context: z.RefinementCtx): void => {
|
||||
checkCanonicalOrder(graph, context);
|
||||
const nodes = new Map(graph.nodes.map((node) => [node.node_id, node]));
|
||||
checkNodes(graph, nodes, context);
|
||||
checkRoots(graph, nodes, context);
|
||||
checkRelations(graph, nodes, context);
|
||||
checkUnknowns(graph, nodes, context);
|
||||
checkUnknownReferences(graph, context);
|
||||
checkFingerprints(graph, nodes, context);
|
||||
checkCoverage(graph, context);
|
||||
if (graph.coverage.status !== "complete" && graph.limitations.length === 0)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["limitations"],
|
||||
message: "Non-complete graph coverage requires a limitation",
|
||||
});
|
||||
const { graph_id: identifier, ...semantic } = graph;
|
||||
if (identifier !== "jsrg_" + digestCanonical(semantic))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["graph_id"],
|
||||
message: "Graph identifier is stale",
|
||||
});
|
||||
};
|
||||
|
||||
/** Strict semantic graph schema with verified canonical commitments. */
|
||||
export const javaScriptSemanticGraphSchema =
|
||||
javaScriptSemanticGraphRecordSchema.superRefine(checkGraph);
|
||||
|
||||
/** Fully validated JavaScript Semantic Relation Graph v1. */
|
||||
export type JavaScriptSemanticGraph = z.infer<
|
||||
typeof javaScriptSemanticGraphSchema
|
||||
>;
|
||||
|
||||
/** Normalize a complete companion graph and derive its graph ID. */
|
||||
export const createJavaScriptSemanticGraph = (
|
||||
input: unknown,
|
||||
): JavaScriptSemanticGraph => {
|
||||
const parsed = javaScriptSemanticGraphInputSchema.parse(input);
|
||||
const semantic: JavaScriptSemanticGraphInput = {
|
||||
...parsed,
|
||||
root_node_ids: uniqueSorted(parsed.root_node_ids),
|
||||
nodes: [...parsed.nodes].sort((left, right) =>
|
||||
compareCodePoints(left.node_id, right.node_id),
|
||||
),
|
||||
relations: [...parsed.relations].sort((left, right) =>
|
||||
compareCodePoints(left.relation_id, right.relation_id),
|
||||
),
|
||||
fingerprints: [...parsed.fingerprints].sort((left, right) =>
|
||||
compareCodePoints(left.fingerprint_id, right.fingerprint_id),
|
||||
),
|
||||
unknowns: [...parsed.unknowns].sort((left, right) =>
|
||||
compareCodePoints(left.unknown_id, right.unknown_id),
|
||||
),
|
||||
coverage: {
|
||||
...parsed.coverage,
|
||||
limits: [...parsed.coverage.limits].sort((left, right) =>
|
||||
compareCodePoints(canonicalJson(left), canonicalJson(right)),
|
||||
),
|
||||
families: [...parsed.coverage.families]
|
||||
.map((family) => ({
|
||||
...family,
|
||||
unknown_ids: uniqueSorted(family.unknown_ids),
|
||||
}))
|
||||
.sort((left, right) => compareCodePoints(left.family, right.family)),
|
||||
},
|
||||
limitations: uniqueSorted(parsed.limitations),
|
||||
};
|
||||
return javaScriptSemanticGraphSchema.parse({
|
||||
...semantic,
|
||||
graph_id: `jsrg_${digestCanonical(semantic)}`,
|
||||
});
|
||||
};
|
||||
|
||||
export type { JavaScriptSemanticGraphNode, JavaScriptSemanticGraphRelation };
|
||||
|
|
@ -0,0 +1,393 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { applicationGraphEvidenceSchema } from "./javascriptApplicationEvidenceSchemas.js";
|
||||
import { isJsonWithinLimits } from "./jsonLimits.js";
|
||||
import { jsonValueSchema } from "./jsonValue.js";
|
||||
|
||||
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
|
||||
const boundedTextSchema = z.string().min(1).max(4_096);
|
||||
const semanticNodeIdSchema = z.string().regex(/^jsrg_node_[a-f0-9]{64}$/u);
|
||||
const semanticRelationIdSchema = z
|
||||
.string()
|
||||
.regex(/^jsrg_relation_[a-f0-9]{64}$/u);
|
||||
const semanticUnknownIdSchema = z
|
||||
.string()
|
||||
.regex(/^jsrg_unknown_[a-f0-9]{64}$/u);
|
||||
|
||||
/** Semantic entity kinds admitted by JavaScript Semantic Relation Graph v1. */
|
||||
export const JAVASCRIPT_SEMANTIC_NODE_KINDS = [
|
||||
"module",
|
||||
"function",
|
||||
"parameter",
|
||||
"binding",
|
||||
"expression",
|
||||
"literal",
|
||||
"property-slot",
|
||||
"call-site",
|
||||
"return-site",
|
||||
"closure-capture",
|
||||
"promise",
|
||||
"task",
|
||||
"event",
|
||||
"listener",
|
||||
"timer",
|
||||
"child-process",
|
||||
"stdio",
|
||||
"signal",
|
||||
"config-source",
|
||||
"request",
|
||||
"response",
|
||||
"boundary",
|
||||
"resource",
|
||||
"unknown-frontier",
|
||||
] as const;
|
||||
|
||||
/** Required semantic-analysis coverage families. */
|
||||
export const JAVASCRIPT_SEMANTIC_RELATION_FAMILIES = [
|
||||
"boundary",
|
||||
"call-flow",
|
||||
"child-process",
|
||||
"closure",
|
||||
"configuration",
|
||||
"data-flow",
|
||||
"event",
|
||||
"object-flow",
|
||||
"promise-ownership",
|
||||
"request",
|
||||
"resource-lifecycle",
|
||||
"timer",
|
||||
] as const;
|
||||
|
||||
/** Directed semantic relations admitted by graph v1. */
|
||||
export const JAVASCRIPT_SEMANTIC_RELATIONS = [
|
||||
"acquires",
|
||||
"aggregates",
|
||||
"aliases",
|
||||
"argument-to-parameter",
|
||||
"awaits",
|
||||
"calls",
|
||||
"cancels-timer",
|
||||
"captures",
|
||||
"chains",
|
||||
"coerces",
|
||||
"connects-stdio",
|
||||
"consumed-by",
|
||||
"constructs-request",
|
||||
"creates-promise",
|
||||
"defaults",
|
||||
"defines",
|
||||
"destructures",
|
||||
"detaches-task",
|
||||
"dispatches-candidate",
|
||||
"forwards-signal",
|
||||
"listens-error",
|
||||
"listens-exit",
|
||||
"owns",
|
||||
"overrides",
|
||||
"parses",
|
||||
"reads",
|
||||
"reads-argv",
|
||||
"reads-config",
|
||||
"reads-environment",
|
||||
"reads-property",
|
||||
"registers-listener",
|
||||
"releases",
|
||||
"removes-listener",
|
||||
"returns-task",
|
||||
"returns-to-call",
|
||||
"schedules-timer",
|
||||
"spawns",
|
||||
"spreads",
|
||||
"supplies-argv",
|
||||
"supplies-env",
|
||||
"supplies-request-field",
|
||||
"validates",
|
||||
"writes",
|
||||
"writes-property",
|
||||
] as const;
|
||||
|
||||
/** Map each semantic relation to its coverage family. */
|
||||
export const JAVASCRIPT_SEMANTIC_RELATION_FAMILY = {
|
||||
acquires: "resource-lifecycle",
|
||||
aggregates: "promise-ownership",
|
||||
aliases: "data-flow",
|
||||
"argument-to-parameter": "call-flow",
|
||||
awaits: "promise-ownership",
|
||||
calls: "call-flow",
|
||||
"cancels-timer": "timer",
|
||||
captures: "closure",
|
||||
chains: "promise-ownership",
|
||||
coerces: "boundary",
|
||||
"connects-stdio": "child-process",
|
||||
"consumed-by": "request",
|
||||
"constructs-request": "request",
|
||||
"creates-promise": "promise-ownership",
|
||||
defaults: "configuration",
|
||||
defines: "data-flow",
|
||||
destructures: "object-flow",
|
||||
"detaches-task": "promise-ownership",
|
||||
"dispatches-candidate": "event",
|
||||
"forwards-signal": "child-process",
|
||||
"listens-error": "child-process",
|
||||
"listens-exit": "child-process",
|
||||
owns: "promise-ownership",
|
||||
overrides: "configuration",
|
||||
parses: "boundary",
|
||||
reads: "data-flow",
|
||||
"reads-argv": "configuration",
|
||||
"reads-config": "configuration",
|
||||
"reads-environment": "configuration",
|
||||
"reads-property": "object-flow",
|
||||
"registers-listener": "event",
|
||||
releases: "resource-lifecycle",
|
||||
"removes-listener": "event",
|
||||
"returns-task": "promise-ownership",
|
||||
"returns-to-call": "call-flow",
|
||||
"schedules-timer": "timer",
|
||||
spawns: "child-process",
|
||||
spreads: "object-flow",
|
||||
"supplies-argv": "child-process",
|
||||
"supplies-env": "child-process",
|
||||
"supplies-request-field": "request",
|
||||
validates: "boundary",
|
||||
writes: "data-flow",
|
||||
"writes-property": "object-flow",
|
||||
} as const satisfies Readonly<
|
||||
Record<
|
||||
(typeof JAVASCRIPT_SEMANTIC_RELATIONS)[number],
|
||||
(typeof JAVASCRIPT_SEMANTIC_RELATION_FAMILIES)[number]
|
||||
>
|
||||
>;
|
||||
|
||||
const relativePathSchema = z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(4_096)
|
||||
.refine(
|
||||
(path) =>
|
||||
!path.startsWith("/") &&
|
||||
!path.includes("\\") &&
|
||||
path.split("/").every((part) => !["", ".", ".."].includes(part)),
|
||||
"Expected a normalized relative POSIX path without traversal",
|
||||
);
|
||||
|
||||
const sourcePointSchema = z.strictObject({
|
||||
line: z.number().int().min(1),
|
||||
column: z.number().int().min(0),
|
||||
});
|
||||
|
||||
const sourceRangeSchema = z
|
||||
.strictObject({ start: sourcePointSchema, end: sourcePointSchema })
|
||||
.superRefine((range, context) => {
|
||||
if (
|
||||
range.end.line < range.start.line ||
|
||||
(range.end.line === range.start.line &&
|
||||
range.end.column < range.start.column)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["end"],
|
||||
message: "Source range end must not precede its start",
|
||||
});
|
||||
});
|
||||
|
||||
const semanticPropertiesSchema = z
|
||||
.record(z.string().min(1).max(128), jsonValueSchema)
|
||||
.superRefine((properties, context) => {
|
||||
if (Object.keys(properties).length > 64)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Semantic properties exceed 64 keys",
|
||||
});
|
||||
if (
|
||||
!isJsonWithinLimits(properties, {
|
||||
maxDepth: 6,
|
||||
maxNodes: 512,
|
||||
maxStringLength: 4_096,
|
||||
})
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Semantic properties exceed structural limits",
|
||||
});
|
||||
});
|
||||
|
||||
const semanticNodeIdentitySchema = z.strictObject({
|
||||
artifact_sha256: digestSchema,
|
||||
module_path: relativePathSchema,
|
||||
source_range: sourceRangeSchema.nullable(),
|
||||
role_key: z.string().min(1).max(1_024),
|
||||
});
|
||||
|
||||
/** One semantic entity before its canonical identifier is derived. */
|
||||
export const javaScriptSemanticNodeInputSchema = z.strictObject({
|
||||
kind: z.enum(JAVASCRIPT_SEMANTIC_NODE_KINDS),
|
||||
identity: semanticNodeIdentitySchema,
|
||||
function_node_id: semanticNodeIdSchema.nullable(),
|
||||
application_node_ids: z
|
||||
.array(z.string().regex(/^jag_node_[a-f0-9]{64}$/u))
|
||||
.max(64),
|
||||
label: z.string().min(1).max(1_024).nullable(),
|
||||
properties: semanticPropertiesSchema,
|
||||
evidence: applicationGraphEvidenceSchema,
|
||||
});
|
||||
|
||||
/** One exact, artifact-version semantic entity. */
|
||||
export const javaScriptSemanticNodeSchema =
|
||||
javaScriptSemanticNodeInputSchema.extend({
|
||||
node_id: semanticNodeIdSchema,
|
||||
identifier_strategy: z.strictObject({
|
||||
strategy: z.literal("semantic-content-sha256"),
|
||||
stability: z.literal("artifact-version"),
|
||||
}),
|
||||
});
|
||||
|
||||
/** One directed semantic relationship before its ID is derived. */
|
||||
export const javaScriptSemanticRelationInputSchema = z.strictObject({
|
||||
source_node_id: semanticNodeIdSchema,
|
||||
target_node_id: semanticNodeIdSchema,
|
||||
relation: z.enum(JAVASCRIPT_SEMANTIC_RELATIONS),
|
||||
resolution: z.enum(["resolved", "candidate"]),
|
||||
properties: semanticPropertiesSchema,
|
||||
evidence: applicationGraphEvidenceSchema,
|
||||
});
|
||||
|
||||
/** One canonical directed semantic relationship. */
|
||||
export const javaScriptSemanticRelationSchema =
|
||||
javaScriptSemanticRelationInputSchema.extend({
|
||||
relation_id: semanticRelationIdSchema,
|
||||
identifier_strategy: z.strictObject({
|
||||
strategy: z.literal("semantic-content-sha256"),
|
||||
stability: z.literal("relationship-exact"),
|
||||
}),
|
||||
});
|
||||
|
||||
/** Reasons semantic analysis can leave an explicit frontier. */
|
||||
export const JAVASCRIPT_SEMANTIC_UNKNOWN_REASONS = [
|
||||
"ambiguous-target",
|
||||
"bound-reached",
|
||||
"dynamic-call",
|
||||
"dynamic-property",
|
||||
"eval-or-generated-code",
|
||||
"incomplete-module",
|
||||
"missing-source",
|
||||
"unsupported-syntax",
|
||||
] as const;
|
||||
|
||||
/** One unresolved semantic frontier before its ID is derived. */
|
||||
export const javaScriptSemanticUnknownInputSchema = z.strictObject({
|
||||
node_id: semanticNodeIdSchema.nullable(),
|
||||
family: z.enum(JAVASCRIPT_SEMANTIC_RELATION_FAMILIES),
|
||||
relation_kinds: z.array(z.enum(JAVASCRIPT_SEMANTIC_RELATIONS)).min(1).max(45),
|
||||
reason: z.enum(JAVASCRIPT_SEMANTIC_UNKNOWN_REASONS),
|
||||
detail: boundedTextSchema,
|
||||
candidate_node_ids: z.array(semanticNodeIdSchema).max(1_000),
|
||||
evidence: applicationGraphEvidenceSchema,
|
||||
});
|
||||
|
||||
/** One canonical unresolved semantic frontier. */
|
||||
export const javaScriptSemanticUnknownSchema =
|
||||
javaScriptSemanticUnknownInputSchema.extend({
|
||||
unknown_id: semanticUnknownIdSchema,
|
||||
});
|
||||
|
||||
const fingerprintComponentsSchema = z.strictObject({
|
||||
parameter_arity: z.number().int().min(0).max(10_000),
|
||||
normalized_ast_sha256: digestSchema,
|
||||
control_flow_sha256: digestSchema,
|
||||
relation_shape_sha256: digestSchema,
|
||||
literal_set_sha256: digestSchema,
|
||||
effects: z
|
||||
.array(
|
||||
z.enum([
|
||||
"async",
|
||||
"child-process",
|
||||
"event",
|
||||
"network",
|
||||
"promise",
|
||||
"resource",
|
||||
"timer",
|
||||
]),
|
||||
)
|
||||
.max(7),
|
||||
});
|
||||
|
||||
/** One bounded rename-resistant function fingerprint. */
|
||||
export const javaScriptSemanticFingerprintInputSchema = z.strictObject({
|
||||
function_node_id: semanticNodeIdSchema,
|
||||
algorithm: z.literal("rea.javascript-semantic-function/v1"),
|
||||
status: z.enum(["complete", "partial", "unavailable"]),
|
||||
components: fingerprintComponentsSchema,
|
||||
limitations: z.array(boundedTextSchema).max(100),
|
||||
evidence: applicationGraphEvidenceSchema,
|
||||
});
|
||||
|
||||
/** One canonical function fingerprint record. */
|
||||
export const javaScriptSemanticFingerprintSchema =
|
||||
javaScriptSemanticFingerprintInputSchema.extend({
|
||||
fingerprint_id: z.string().regex(/^jsrg_fingerprint_[a-f0-9]{64}$/u),
|
||||
fingerprint_sha256: digestSchema,
|
||||
});
|
||||
|
||||
const familyCoverageSchema = z.strictObject({
|
||||
family: z.enum(JAVASCRIPT_SEMANTIC_RELATION_FAMILIES),
|
||||
status: z.enum(["complete", "partial", "unknown", "unsupported"]),
|
||||
retained_relations: z.number().int().min(0),
|
||||
omitted_relations: z.number().int().min(0).nullable(),
|
||||
unknown_ids: z.array(semanticUnknownIdSchema).max(100_000),
|
||||
});
|
||||
|
||||
const graphCoverageSchema = z.strictObject({
|
||||
status: z.enum(["complete", "partial", "unknown", "unavailable"]),
|
||||
truncated: z.boolean(),
|
||||
omitted_nodes: z.number().int().min(0).nullable(),
|
||||
omitted_relations: z.number().int().min(0).nullable(),
|
||||
limits: z
|
||||
.array(
|
||||
z.strictObject({
|
||||
name: z.string().min(1).max(128),
|
||||
value: z.number().int().min(0),
|
||||
unit: z.enum(["items", "bytes", "milliseconds", "depth", "other"]),
|
||||
}),
|
||||
)
|
||||
.max(32),
|
||||
families: z.array(familyCoverageSchema).length(12),
|
||||
});
|
||||
|
||||
/** Graph content before its top-level commitment is derived. */
|
||||
export const javaScriptSemanticGraphInputSchema = z.strictObject({
|
||||
schema: z.literal("JavaScriptSemanticRelationGraph"),
|
||||
schema_version: z.literal(1),
|
||||
root_artifact_sha256: digestSchema,
|
||||
application_graph_id: z.string().regex(/^jag_[a-f0-9]{64}$/u),
|
||||
root_node_ids: z.array(semanticNodeIdSchema).min(1).max(10_000),
|
||||
nodes: z.array(javaScriptSemanticNodeSchema).min(1).max(100_000),
|
||||
relations: z.array(javaScriptSemanticRelationSchema).max(200_000),
|
||||
fingerprints: z.array(javaScriptSemanticFingerprintSchema).max(20_000),
|
||||
unknowns: z.array(javaScriptSemanticUnknownSchema).max(100_000),
|
||||
coverage: graphCoverageSchema,
|
||||
limitations: z.array(boundedTextSchema).max(1_000),
|
||||
});
|
||||
|
||||
/** Stored JavaScript Semantic Relation Graph v1. */
|
||||
export const javaScriptSemanticGraphRecordSchema =
|
||||
javaScriptSemanticGraphInputSchema.extend({
|
||||
graph_id: z.string().regex(/^jsrg_[a-f0-9]{64}$/u),
|
||||
});
|
||||
|
||||
/** Canonical semantic node. */
|
||||
export type JavaScriptSemanticGraphNode = z.infer<
|
||||
typeof javaScriptSemanticNodeSchema
|
||||
>;
|
||||
/** Canonical semantic relation. */
|
||||
export type JavaScriptSemanticGraphRelation = z.infer<
|
||||
typeof javaScriptSemanticRelationSchema
|
||||
>;
|
||||
/** Canonical unresolved semantic frontier. */
|
||||
export type JavaScriptSemanticGraphUnknown = z.infer<
|
||||
typeof javaScriptSemanticUnknownSchema
|
||||
>;
|
||||
/** Canonical graph content before graph ID derivation. */
|
||||
export type JavaScriptSemanticGraphInput = z.infer<
|
||||
typeof javaScriptSemanticGraphInputSchema
|
||||
>;
|
||||
|
|
@ -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));
|
||||
|
|
@ -8,6 +8,12 @@ export interface JavaScriptSemanticLimits {
|
|||
readonly maxReferences: number;
|
||||
readonly maxModuleLinks: number;
|
||||
readonly maxReturnSites: number;
|
||||
readonly maxCallSites: number;
|
||||
readonly maxCallArguments: number;
|
||||
readonly maxArgumentFlows: number;
|
||||
readonly maxCallReturnFlows: number;
|
||||
readonly maxClosureCaptures: number;
|
||||
readonly maxFrontiers: number;
|
||||
readonly maxValueDepth: number;
|
||||
readonly maxUnionValues: number;
|
||||
readonly maxObjectProperties: number;
|
||||
|
|
@ -21,6 +27,12 @@ export const DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS: JavaScriptSemanticLimits = {
|
|||
maxReferences: 100_000,
|
||||
maxModuleLinks: 20_000,
|
||||
maxReturnSites: 20_000,
|
||||
maxCallSites: 20_000,
|
||||
maxCallArguments: 100_000,
|
||||
maxArgumentFlows: 100_000,
|
||||
maxCallReturnFlows: 100_000,
|
||||
maxClosureCaptures: 100_000,
|
||||
maxFrontiers: 20_000,
|
||||
maxValueDepth: 16,
|
||||
maxUnionValues: 32,
|
||||
maxObjectProperties: 256,
|
||||
|
|
@ -139,10 +151,71 @@ export interface JavaScriptSemanticCallable {
|
|||
|
||||
/** One direct return expression belonging to exactly one callable. */
|
||||
export interface JavaScriptSemanticReturnSite {
|
||||
readonly returnSiteId: string;
|
||||
readonly location: JavaScriptSourceRange;
|
||||
readonly identityReferenceLocation: JavaScriptSourceRange | null;
|
||||
readonly value: JavaScriptSemanticValue;
|
||||
}
|
||||
|
||||
/** One statically visible call or construction expression. */
|
||||
export interface JavaScriptSemanticCallSite {
|
||||
readonly callSiteId: string;
|
||||
readonly kind: "call" | "construct";
|
||||
readonly callerCallableId: string | null;
|
||||
readonly location: JavaScriptSourceRange;
|
||||
readonly calleeLocation: JavaScriptSourceRange;
|
||||
readonly resolution: "exact" | "ambiguous" | "unresolved";
|
||||
readonly calleeCallableIds: readonly string[];
|
||||
readonly arguments: readonly JavaScriptSemanticCallArgument[];
|
||||
}
|
||||
|
||||
/** One argument occurrence retained without evaluating application code. */
|
||||
export interface JavaScriptSemanticCallArgument {
|
||||
readonly index: number;
|
||||
readonly location: JavaScriptSourceRange;
|
||||
readonly spread: boolean;
|
||||
}
|
||||
|
||||
/** One positional argument that may initialize one local parameter binding. */
|
||||
export interface JavaScriptSemanticArgumentFlow {
|
||||
readonly callSiteId: string;
|
||||
readonly argumentIndex: number;
|
||||
readonly argumentLocation: JavaScriptSourceRange;
|
||||
readonly callableId: string;
|
||||
readonly parameterBindingId: string;
|
||||
readonly parameterLocation: JavaScriptSourceRange;
|
||||
}
|
||||
|
||||
/** One direct local return that may supply a call expression's value. */
|
||||
export interface JavaScriptSemanticCallReturnFlow {
|
||||
readonly callSiteId: string;
|
||||
readonly callableId: string;
|
||||
readonly returnSiteId: string;
|
||||
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;
|
||||
readonly bindingId: string;
|
||||
readonly referenceLocation: JavaScriptSourceRange;
|
||||
}
|
||||
|
||||
/** One syntax location where exact local semantic continuation is unavailable. */
|
||||
export interface JavaScriptSemanticFrontier {
|
||||
readonly kind: "dynamic-call" | "dynamic-property";
|
||||
readonly callableId: string | null;
|
||||
readonly location: JavaScriptSourceRange;
|
||||
readonly reason: string;
|
||||
}
|
||||
|
||||
/** Exact retention state for one callable's direct returns. */
|
||||
interface JavaScriptSemanticReturnCoverage {
|
||||
readonly status: "complete" | "partial" | "truncated";
|
||||
|
|
@ -174,15 +247,21 @@ interface JavaScriptSemanticCoverage {
|
|||
readonly limitsReached: readonly (keyof JavaScriptSemanticLimits)[];
|
||||
}
|
||||
|
||||
/** Provider-neutral JavaScript semantic IR v2. */
|
||||
/** Provider-neutral JavaScript semantic IR v3. */
|
||||
export interface JavaScriptSemanticIr {
|
||||
readonly schema: "JavaScriptSemanticIR";
|
||||
readonly schemaVersion: 2;
|
||||
readonly schemaVersion: 3;
|
||||
readonly scopes: readonly JavaScriptSemanticScope[];
|
||||
readonly bindings: readonly JavaScriptSemanticBinding[];
|
||||
readonly callables: readonly JavaScriptSemanticCallable[];
|
||||
readonly references: readonly JavaScriptSemanticReference[];
|
||||
readonly moduleLinks: readonly JavaScriptSemanticModuleLink[];
|
||||
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;
|
||||
readonly limitations: readonly string[];
|
||||
}
|
||||
|
|
@ -208,12 +287,18 @@ export const semanticBinding = (
|
|||
/** Fail-closed result when Babel cannot produce an inert syntax tree. */
|
||||
export const failedJavaScriptSemanticIr = (): JavaScriptSemanticIr => ({
|
||||
schema: "JavaScriptSemanticIR",
|
||||
schemaVersion: 2,
|
||||
schemaVersion: 3,
|
||||
scopes: [],
|
||||
bindings: [],
|
||||
callables: [],
|
||||
references: [],
|
||||
moduleLinks: [],
|
||||
callSites: [],
|
||||
argumentFlows: [],
|
||||
callReturnFlows: [],
|
||||
callResultFlows: [],
|
||||
closureCaptures: [],
|
||||
frontiers: [],
|
||||
coverage: { status: "failed", omittedCount: null, limitsReached: [] },
|
||||
limitations: [
|
||||
"JavaScript parsing failed; no semantic absence claim is available.",
|
||||
|
|
|
|||
|
|
@ -42,8 +42,10 @@ export const collectSemanticCallable = (input: CollectCallableInput): void => {
|
|||
reachSemanticLimit(state, "maxCallables");
|
||||
return;
|
||||
}
|
||||
const callableId =
|
||||
semanticCallableIdForNode(node) ?? "callable:unknown:-1:-1";
|
||||
state.callables.push({
|
||||
callableId: semanticCallableIdForNode(node) ?? "callable:unknown:-1:-1",
|
||||
callableId,
|
||||
kind,
|
||||
name: callableName(node, parent),
|
||||
containerScopeId: containerScope.scopeId,
|
||||
|
|
@ -60,6 +62,7 @@ export const collectSemanticCallable = (input: CollectCallableInput): void => {
|
|||
limitsReached: [],
|
||||
},
|
||||
});
|
||||
state.callableNodesById.set(callableId, node);
|
||||
};
|
||||
|
||||
/** Collect one static import/export relationship for later composition. */
|
||||
|
|
@ -102,7 +105,7 @@ export const collectSemanticReferences = (
|
|||
traverseJavaScriptAst(program, {
|
||||
enter: (node, parent) => {
|
||||
if (!t.isIdentifier(node) || parent === null) return;
|
||||
const role = referenceRole(node, parent);
|
||||
const role = semanticReferenceRole(node, parent);
|
||||
if (role === null) return;
|
||||
const key = `${String(node.start)}:${role}:${node.name}`;
|
||||
if (seen.has(key)) return;
|
||||
|
|
@ -403,7 +406,8 @@ const declarationCallableId = (
|
|||
: semanticCallableIdForNode(declarator.init);
|
||||
};
|
||||
|
||||
const referenceRole = (
|
||||
/** Classify one identifier occurrence consistently with semantic references. */
|
||||
export const semanticReferenceRole = (
|
||||
node: t.Identifier,
|
||||
parent: t.Node,
|
||||
): JavaScriptSemanticReference["role"] | null => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,514 @@
|
|||
import { compareCodePoints } from "./javascriptApplicationGraph.js";
|
||||
import type {
|
||||
JavaScriptSemanticGraph,
|
||||
JavaScriptSemanticGraphNode,
|
||||
JavaScriptSemanticGraphRelation,
|
||||
} from "./javascriptSemanticGraph.js";
|
||||
import type { JavaScriptSemanticGraphUnknown } from "./javascriptSemanticGraphSchemas.js";
|
||||
import { isJavaScriptSemanticOwnershipRelation as ownershipRelation } from "./javascriptSemanticQueryRelations.js";
|
||||
import {
|
||||
javaScriptSemanticQueryInputSchema,
|
||||
javaScriptSemanticQueryResultSchema,
|
||||
type JavaScriptSemanticQueryInput,
|
||||
type JavaScriptSemanticQueryResult,
|
||||
} from "./javascriptSemanticQuerySchemas.js";
|
||||
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;
|
||||
readonly nextNodeId: string;
|
||||
}
|
||||
|
||||
type QueryFrontier = JavaScriptSemanticQueryFrontier;
|
||||
|
||||
interface Traversal {
|
||||
readonly nodeIds: Set<string>;
|
||||
readonly relationIds: Set<string>;
|
||||
readonly functionIds: Set<string>;
|
||||
readonly modules: Set<string>;
|
||||
readonly frontier: QueryFrontier[];
|
||||
}
|
||||
|
||||
interface SeedAdmission {
|
||||
readonly nodeIds: string[];
|
||||
readonly frontier: QueryFrontier[];
|
||||
}
|
||||
|
||||
interface QueryPage {
|
||||
readonly retainedNodes: JavaScriptSemanticGraphNode[];
|
||||
readonly retainedRelations: JavaScriptSemanticGraphRelation[];
|
||||
readonly pageRelations: JavaScriptSemanticGraphRelation[];
|
||||
readonly pageNodes: JavaScriptSemanticGraphNode[];
|
||||
readonly nextCursor: string | null;
|
||||
}
|
||||
|
||||
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 = (
|
||||
graph: JavaScriptSemanticGraph,
|
||||
rawInput: unknown,
|
||||
): JavaScriptSemanticQueryResult => {
|
||||
const input = javaScriptSemanticQueryInputSchema.parse(rawInput);
|
||||
const queryId = queryIdentifier(graph, input);
|
||||
const offset = parseCursor(input.cursor, queryId);
|
||||
const seeds = resolveSeeds(graph, input);
|
||||
const admission = admitSeeds(graph, seeds, input);
|
||||
const retainedSeeds = admission.nodeIds;
|
||||
const adjacency = buildAdjacency(graph.relations, input);
|
||||
const traversal = traverse(graph, retainedSeeds, adjacency, input);
|
||||
const frontier = uniqueFrontier([
|
||||
...traversal.frontier,
|
||||
...admission.frontier,
|
||||
]);
|
||||
const page = createQueryPage({
|
||||
graph,
|
||||
traversal,
|
||||
query: input,
|
||||
queryId,
|
||||
offset,
|
||||
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(
|
||||
page.retainedNodes,
|
||||
page.retainedRelations,
|
||||
input,
|
||||
);
|
||||
const assessment = assessJavaScriptSemanticQuery({
|
||||
graph,
|
||||
totalSeeds: seeds.length,
|
||||
retainedSeeds: retainedSeeds.length,
|
||||
expectedMatches: expectedMatches.length,
|
||||
hasExpectation: input.expected !== null,
|
||||
frontier,
|
||||
unknowns: relevantUnknowns,
|
||||
candidateRelations,
|
||||
unknownsTruncated: allRelevantUnknowns.length > relevantUnknowns.length,
|
||||
});
|
||||
return javaScriptSemanticQueryResultSchema.parse({
|
||||
schema_version: 1,
|
||||
query_id: queryId,
|
||||
source_graph_id: graph.graph_id,
|
||||
seed: input.seed,
|
||||
direction: input.direction,
|
||||
status: assessment.status,
|
||||
seed_node_ids: retainedSeeds,
|
||||
nodes: page.pageNodes,
|
||||
relations: page.pageRelations,
|
||||
unknowns: relevantUnknowns,
|
||||
expected_match_node_ids: expectedMatches.map(({ node_id }) => node_id),
|
||||
summary: {
|
||||
total_seed_matches: seeds.length,
|
||||
retained_seed_matches: retainedSeeds.length,
|
||||
traversed_nodes: traversal.nodeIds.size,
|
||||
traversed_relations: traversal.relationIds.size,
|
||||
traversed_functions: traversal.functionIds.size,
|
||||
traversed_modules: traversal.modules.size,
|
||||
relevant_unknowns: allRelevantUnknowns.length,
|
||||
retained_unknowns: relevantUnknowns.length,
|
||||
},
|
||||
coverage: assessment.coverage,
|
||||
page: {
|
||||
offset,
|
||||
size: page.pageRelations.length,
|
||||
next_cursor: page.nextCursor,
|
||||
},
|
||||
applied_limits: input.limits,
|
||||
accepted_limit_ranges: assessment.acceptedLimitRanges,
|
||||
limitations: assessment.limitations,
|
||||
});
|
||||
};
|
||||
|
||||
const createQueryPage = ({
|
||||
graph,
|
||||
traversal,
|
||||
query,
|
||||
queryId,
|
||||
offset,
|
||||
retainedSeeds,
|
||||
}: QueryPageInput): QueryPage => {
|
||||
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 JavaScriptSemanticQueryCursorError(
|
||||
"Semantic query cursor is beyond the retained result",
|
||||
);
|
||||
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 = (
|
||||
graph: JavaScriptSemanticGraph,
|
||||
seeds: readonly string[],
|
||||
input: JavaScriptSemanticQueryInput,
|
||||
): SeedAdmission => {
|
||||
const nodes = new Map(graph.nodes.map((node) => [node.node_id, node]));
|
||||
const retained: string[] = [];
|
||||
const functions = new Set<string>();
|
||||
const modules = new Set<string>();
|
||||
const frontier: QueryFrontier[] = [];
|
||||
for (const nodeId of seeds) {
|
||||
const node = nodes.get(nodeId);
|
||||
if (node === undefined) continue;
|
||||
const reason = seedBlockedReason(node, {
|
||||
retained,
|
||||
functions,
|
||||
modules,
|
||||
input,
|
||||
});
|
||||
if (reason !== null) {
|
||||
frontier.push({ node_id: nodeId, depth: 0, reason });
|
||||
continue;
|
||||
}
|
||||
retained.push(nodeId);
|
||||
retainOwners(node, functions, modules);
|
||||
}
|
||||
return { nodeIds: retained, frontier: uniqueFrontier(frontier) };
|
||||
};
|
||||
|
||||
interface SeedBounds {
|
||||
readonly retained: readonly string[];
|
||||
readonly functions: ReadonlySet<string>;
|
||||
readonly modules: ReadonlySet<string>;
|
||||
readonly input: JavaScriptSemanticQueryInput;
|
||||
}
|
||||
|
||||
interface TraversalBounds {
|
||||
readonly nodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>;
|
||||
readonly nodeIds: ReadonlySet<string>;
|
||||
readonly relationIds: ReadonlySet<string>;
|
||||
readonly functionIds: ReadonlySet<string>;
|
||||
readonly modules: ReadonlySet<string>;
|
||||
readonly input: JavaScriptSemanticQueryInput;
|
||||
}
|
||||
|
||||
const seedBlockedReason = (
|
||||
node: JavaScriptSemanticGraphNode,
|
||||
bounds: SeedBounds,
|
||||
): QueryFrontier["reason"] | null => {
|
||||
if (bounds.retained.length >= bounds.input.limits.max_seed_matches)
|
||||
return "max-seed-matches";
|
||||
if (bounds.retained.length >= bounds.input.limits.max_nodes)
|
||||
return "max-nodes";
|
||||
const functionId = functionOwner(node);
|
||||
if (
|
||||
functionId !== null &&
|
||||
!bounds.functions.has(functionId) &&
|
||||
bounds.functions.size >= bounds.input.limits.max_functions
|
||||
)
|
||||
return "max-functions";
|
||||
if (
|
||||
!bounds.modules.has(node.identity.module_path) &&
|
||||
bounds.modules.size >= bounds.input.limits.max_modules
|
||||
)
|
||||
return "max-modules";
|
||||
return null;
|
||||
};
|
||||
|
||||
const buildAdjacency = (
|
||||
relations: readonly JavaScriptSemanticGraphRelation[],
|
||||
input: JavaScriptSemanticQueryInput,
|
||||
): ReadonlyMap<string, TraversalEntry[]> => {
|
||||
const adjacency = new Map<string, TraversalEntry[]>();
|
||||
const allowed =
|
||||
input.allowed_relations === undefined
|
||||
? null
|
||||
: new Set(input.allowed_relations);
|
||||
for (const relation of relations) {
|
||||
if (allowed !== null && !allowed.has(relation.relation)) continue;
|
||||
if (
|
||||
relation.resolution === "candidate" &&
|
||||
!input.include_ambiguous_dynamic_edges
|
||||
)
|
||||
continue;
|
||||
addDirectedEntries(adjacency, relation, input.direction);
|
||||
}
|
||||
for (const entries of adjacency.values())
|
||||
entries.sort((left, right) =>
|
||||
compareCodePoints(
|
||||
`${left.relation.relation_id}\0${left.nextNodeId}`,
|
||||
`${right.relation.relation_id}\0${right.nextNodeId}`,
|
||||
),
|
||||
);
|
||||
return adjacency;
|
||||
};
|
||||
|
||||
const addDirectedEntries = (
|
||||
adjacency: Map<string, TraversalEntry[]>,
|
||||
relation: JavaScriptSemanticGraphRelation,
|
||||
direction: JavaScriptSemanticQueryInput["direction"],
|
||||
): void => {
|
||||
if (direction === "callers") {
|
||||
if (relation.relation === "calls")
|
||||
addEntry(
|
||||
adjacency,
|
||||
relation.target_node_id,
|
||||
relation.source_node_id,
|
||||
relation,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (direction === "ownership") {
|
||||
if (!ownershipRelation(relation.relation)) return;
|
||||
addEntry(
|
||||
adjacency,
|
||||
relation.source_node_id,
|
||||
relation.target_node_id,
|
||||
relation,
|
||||
);
|
||||
addEntry(
|
||||
adjacency,
|
||||
relation.target_node_id,
|
||||
relation.source_node_id,
|
||||
relation,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (direction === "backward-provenance")
|
||||
addEntry(
|
||||
adjacency,
|
||||
relation.target_node_id,
|
||||
relation.source_node_id,
|
||||
relation,
|
||||
);
|
||||
else
|
||||
addEntry(
|
||||
adjacency,
|
||||
relation.source_node_id,
|
||||
relation.target_node_id,
|
||||
relation,
|
||||
);
|
||||
};
|
||||
|
||||
const addEntry = (
|
||||
adjacency: Map<string, TraversalEntry[]>,
|
||||
from: string,
|
||||
nextNodeId: string,
|
||||
relation: JavaScriptSemanticGraphRelation,
|
||||
): void => {
|
||||
const entries = adjacency.get(from) ?? [];
|
||||
entries.push({ relation, nextNodeId });
|
||||
adjacency.set(from, entries);
|
||||
};
|
||||
|
||||
const traverse = (
|
||||
graph: JavaScriptSemanticGraph,
|
||||
seeds: readonly string[],
|
||||
adjacency: ReadonlyMap<string, TraversalEntry[]>,
|
||||
input: JavaScriptSemanticQueryInput,
|
||||
): Traversal => {
|
||||
const nodes = new Map(graph.nodes.map((node) => [node.node_id, node]));
|
||||
const nodeIds = new Set(seeds);
|
||||
const relationIds = new Set<string>();
|
||||
const functionIds = new Set<string>();
|
||||
const modules = new Set<string>();
|
||||
const frontier: QueryFrontier[] = [];
|
||||
for (const nodeId of nodeIds)
|
||||
retainOwners(nodes.get(nodeId), functionIds, modules);
|
||||
const depths = new Map([...nodeIds].map((nodeId) => [nodeId, 0]));
|
||||
const queue = [...nodeIds];
|
||||
const bounds: TraversalBounds = {
|
||||
nodes,
|
||||
nodeIds,
|
||||
relationIds,
|
||||
functionIds,
|
||||
modules,
|
||||
input,
|
||||
};
|
||||
for (let offset = 0; offset < queue.length; offset += 1) {
|
||||
const current = queue[offset];
|
||||
if (current === undefined) continue;
|
||||
const depth = depths.get(current) ?? 0;
|
||||
for (const entry of adjacency.get(current) ?? []) {
|
||||
const reason = blockedReason(entry, depth, bounds);
|
||||
if (reason !== null) {
|
||||
frontier.push({ node_id: current, depth, reason });
|
||||
continue;
|
||||
}
|
||||
relationIds.add(entry.relation.relation_id);
|
||||
if (nodeIds.has(entry.nextNodeId)) continue;
|
||||
const node = nodes.get(entry.nextNodeId);
|
||||
if (node === undefined) continue;
|
||||
nodeIds.add(entry.nextNodeId);
|
||||
retainOwners(node, functionIds, modules);
|
||||
depths.set(entry.nextNodeId, depth + 1);
|
||||
queue.push(entry.nextNodeId);
|
||||
}
|
||||
}
|
||||
return {
|
||||
nodeIds,
|
||||
relationIds,
|
||||
functionIds,
|
||||
modules,
|
||||
frontier: uniqueFrontier(frontier),
|
||||
};
|
||||
};
|
||||
|
||||
const blockedReason = (
|
||||
entry: TraversalEntry,
|
||||
depth: number,
|
||||
bounds: TraversalBounds,
|
||||
): QueryFrontier["reason"] | null => {
|
||||
if (depth >= bounds.input.limits.max_depth) return "max-depth";
|
||||
if (
|
||||
!bounds.relationIds.has(entry.relation.relation_id) &&
|
||||
bounds.relationIds.size >= bounds.input.limits.max_edges
|
||||
)
|
||||
return "max-edges";
|
||||
if (bounds.nodeIds.has(entry.nextNodeId)) return null;
|
||||
if (bounds.nodeIds.size >= bounds.input.limits.max_nodes) return "max-nodes";
|
||||
const node = bounds.nodes.get(entry.nextNodeId);
|
||||
if (node === undefined) return null;
|
||||
const functionId = functionOwner(node);
|
||||
if (
|
||||
functionId !== null &&
|
||||
!bounds.functionIds.has(functionId) &&
|
||||
bounds.functionIds.size >= bounds.input.limits.max_functions
|
||||
)
|
||||
return "max-functions";
|
||||
if (
|
||||
!bounds.modules.has(node.identity.module_path) &&
|
||||
bounds.modules.size >= bounds.input.limits.max_modules
|
||||
)
|
||||
return "max-modules";
|
||||
return null;
|
||||
};
|
||||
|
||||
const retainOwners = (
|
||||
node: JavaScriptSemanticGraphNode | undefined,
|
||||
functions: Set<string>,
|
||||
modules: Set<string>,
|
||||
): void => {
|
||||
if (node === undefined) return;
|
||||
const functionId = functionOwner(node);
|
||||
if (functionId !== null) functions.add(functionId);
|
||||
modules.add(node.identity.module_path);
|
||||
};
|
||||
|
||||
const functionOwner = (node: JavaScriptSemanticGraphNode): string | null =>
|
||||
node.kind === "function" ? node.node_id : node.function_node_id;
|
||||
|
||||
const uniqueFrontier = (frontier: readonly QueryFrontier[]): QueryFrontier[] =>
|
||||
[
|
||||
...new Map(
|
||||
frontier.map((item) => [
|
||||
`${item.node_id}\0${String(item.depth)}\0${item.reason}`,
|
||||
item,
|
||||
]),
|
||||
).values(),
|
||||
].sort((left, right) =>
|
||||
compareCodePoints(canonicalJson(left), canonicalJson(right)),
|
||||
);
|
||||
|
||||
const relevantUnknownFrontiers = (
|
||||
graph: JavaScriptSemanticGraph,
|
||||
nodeIds: ReadonlySet<string>,
|
||||
input: JavaScriptSemanticQueryInput,
|
||||
): JavaScriptSemanticGraphUnknown[] => {
|
||||
const allowed =
|
||||
input.allowed_relations === undefined
|
||||
? null
|
||||
: new Set(input.allowed_relations);
|
||||
return graph.unknowns.filter(
|
||||
(unknown) =>
|
||||
(unknown.node_id === null || nodeIds.has(unknown.node_id)) &&
|
||||
(allowed === null ||
|
||||
unknown.relation_kinds.some((relation) => allowed.has(relation))),
|
||||
);
|
||||
};
|
||||
|
||||
const relevantCandidateRelationCount = (
|
||||
graph: JavaScriptSemanticGraph,
|
||||
nodeIds: ReadonlySet<string>,
|
||||
input: JavaScriptSemanticQueryInput,
|
||||
): number => {
|
||||
const allowed =
|
||||
input.allowed_relations === undefined
|
||||
? null
|
||||
: new Set(input.allowed_relations);
|
||||
return graph.relations.filter(
|
||||
(relation) =>
|
||||
relation.resolution === "candidate" &&
|
||||
(allowed === null || allowed.has(relation.relation)) &&
|
||||
(nodeIds.has(relation.source_node_id) ||
|
||||
nodeIds.has(relation.target_node_id)),
|
||||
).length;
|
||||
};
|
||||
|
||||
const expectedMatchesFor = (
|
||||
nodes: readonly JavaScriptSemanticGraphNode[],
|
||||
relations: readonly JavaScriptSemanticGraphRelation[],
|
||||
input: JavaScriptSemanticQueryInput,
|
||||
): JavaScriptSemanticGraphNode[] => {
|
||||
if (input.expected === null) return [];
|
||||
const classes = new Set(input.expected.classes);
|
||||
const connected = new Set(
|
||||
relations.map((relation) =>
|
||||
input.expected?.role === "source"
|
||||
? relation.target_node_id
|
||||
: relation.source_node_id,
|
||||
),
|
||||
);
|
||||
return nodes.filter(
|
||||
({ kind, node_id }) => classes.has(kind) && !connected.has(node_id),
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
import { compareCodePoints } from "./javascriptApplicationGraph.js";
|
||||
import type { JavaScriptSemanticGraph } from "./javascriptSemanticGraph.js";
|
||||
import type { JavaScriptSemanticGraphUnknown } from "./javascriptSemanticGraphSchemas.js";
|
||||
import type { JavaScriptSemanticQueryResult } from "./javascriptSemanticQuerySchemas.js";
|
||||
|
||||
/** One caller-limit frontier retained by deterministic query traversal. */
|
||||
export interface JavaScriptSemanticQueryFrontier {
|
||||
readonly node_id: string;
|
||||
readonly depth: number;
|
||||
readonly reason:
|
||||
| "max-depth"
|
||||
| "max-edges"
|
||||
| "max-functions"
|
||||
| "max-modules"
|
||||
| "max-nodes"
|
||||
| "max-seed-matches";
|
||||
}
|
||||
|
||||
/** Inputs needed to classify one completed semantic traversal. */
|
||||
export interface JavaScriptSemanticQueryAssessmentInput {
|
||||
readonly graph: JavaScriptSemanticGraph;
|
||||
readonly totalSeeds: number;
|
||||
readonly retainedSeeds: number;
|
||||
readonly expectedMatches: number;
|
||||
readonly hasExpectation: boolean;
|
||||
readonly frontier: readonly JavaScriptSemanticQueryFrontier[];
|
||||
readonly unknowns: readonly JavaScriptSemanticGraphUnknown[];
|
||||
readonly candidateRelations: number;
|
||||
readonly unknownsTruncated: boolean;
|
||||
}
|
||||
|
||||
/** Status, coverage, limits, and limitations derived from one traversal. */
|
||||
export interface JavaScriptSemanticQueryAssessment {
|
||||
readonly status: JavaScriptSemanticQueryResult["status"];
|
||||
readonly coverage: JavaScriptSemanticQueryResult["coverage"];
|
||||
readonly limitations: string[];
|
||||
readonly acceptedLimitRanges: JavaScriptSemanticQueryResult["accepted_limit_ranges"];
|
||||
}
|
||||
|
||||
/** Classify a bounded semantic traversal without promoting unknowns to absence. */
|
||||
export const assessJavaScriptSemanticQuery = (
|
||||
input: JavaScriptSemanticQueryAssessmentInput,
|
||||
): JavaScriptSemanticQueryAssessment => ({
|
||||
status: queryStatus(input),
|
||||
coverage: queryCoverage(input),
|
||||
limitations: queryLimitations(input),
|
||||
acceptedLimitRanges: {
|
||||
max_seed_matches: { minimum: 1, maximum: 1_000 },
|
||||
max_nodes: { minimum: 1, maximum: 50_000 },
|
||||
max_edges: { minimum: 1, maximum: 100_000 },
|
||||
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 },
|
||||
},
|
||||
});
|
||||
|
||||
const queryCoverage = (
|
||||
input: JavaScriptSemanticQueryAssessmentInput,
|
||||
): JavaScriptSemanticQueryResult["coverage"] => ({
|
||||
status:
|
||||
input.graph.coverage.status === "unavailable"
|
||||
? "unavailable"
|
||||
: input.frontier.length > 0 || input.unknownsTruncated
|
||||
? "truncated"
|
||||
: input.unknowns.length > 0 ||
|
||||
input.candidateRelations > 0 ||
|
||||
input.graph.coverage.status !== "complete"
|
||||
? "partial"
|
||||
: "complete",
|
||||
frontier: [...input.frontier],
|
||||
});
|
||||
|
||||
const queryStatus = (
|
||||
input: JavaScriptSemanticQueryAssessmentInput,
|
||||
): JavaScriptSemanticQueryResult["status"] => {
|
||||
if (input.graph.coverage.status === "unavailable") return "unsupported";
|
||||
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 input.unknowns.length > 0 ||
|
||||
input.graph.coverage.status !== "complete"
|
||||
? "partial"
|
||||
: "no-match";
|
||||
return input.totalSeeds > 1 ? "ambiguous" : "found";
|
||||
};
|
||||
|
||||
const queryLimitations = (
|
||||
input: JavaScriptSemanticQueryAssessmentInput,
|
||||
): string[] =>
|
||||
uniqueSorted([
|
||||
"Semantic graph reachability is a static inference and does not prove runtime execution.",
|
||||
...input.graph.limitations,
|
||||
...(input.totalSeeds === input.retainedSeeds
|
||||
? []
|
||||
: [
|
||||
"Seed matches exceeded caller bounds; omitted starts remain unexplored.",
|
||||
]),
|
||||
...(input.frontier.length === 0
|
||||
? []
|
||||
: [
|
||||
"Traversal stopped at explicit caller limits; frontier facts remain unknown.",
|
||||
]),
|
||||
...(input.unknowns.length === 0
|
||||
? []
|
||||
: [
|
||||
"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
|
||||
? []
|
||||
: [
|
||||
"Candidate relations remain ambiguous and do not establish a resolved semantic path.",
|
||||
]),
|
||||
]);
|
||||
|
||||
const uniqueSorted = (values: readonly string[]): string[] =>
|
||||
[...new Set(values)].sort(compareCodePoints);
|
||||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
JAVASCRIPT_SEMANTIC_NODE_KINDS,
|
||||
JAVASCRIPT_SEMANTIC_RELATIONS,
|
||||
javaScriptSemanticNodeSchema,
|
||||
javaScriptSemanticRelationSchema,
|
||||
javaScriptSemanticUnknownSchema,
|
||||
} from "./javascriptSemanticGraphSchemas.js";
|
||||
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 literalSeedSchema = z.strictObject({
|
||||
kind: z.literal("literal"),
|
||||
value: jsonValueSchema.refine(
|
||||
(value) =>
|
||||
value === null ||
|
||||
typeof value === "string" ||
|
||||
typeof value === "number" ||
|
||||
typeof value === "boolean",
|
||||
"Literal seeds accept only JSON primitive values",
|
||||
),
|
||||
});
|
||||
|
||||
/** Authenticated starting points accepted by semantic tracing. */
|
||||
export const javaScriptSemanticQuerySeedSchema = z.discriminatedUnion("kind", [
|
||||
z.strictObject({
|
||||
kind: z.literal("semantic-node"),
|
||||
node_id: semanticNodeIdSchema,
|
||||
}),
|
||||
z.strictObject({
|
||||
kind: z.literal("application-node"),
|
||||
node_id: z.string().regex(/^jag_node_[a-f0-9]{64}$/u),
|
||||
}),
|
||||
literalSeedSchema,
|
||||
z.strictObject({
|
||||
kind: z.literal("function"),
|
||||
fingerprint_sha256: digestSchema,
|
||||
}),
|
||||
z.strictObject({
|
||||
kind: z.literal("property"),
|
||||
name: z.string().min(1).max(4_096),
|
||||
}),
|
||||
z.strictObject({
|
||||
kind: z.literal("endpoint"),
|
||||
value: z.string().min(1).max(16_384),
|
||||
}),
|
||||
z.strictObject({
|
||||
kind: z.literal("event"),
|
||||
name: z.string().min(1).max(4_096),
|
||||
}),
|
||||
z.strictObject({
|
||||
kind: z.literal("boundary-field"),
|
||||
field: z.string().min(1).max(4_096),
|
||||
}),
|
||||
]);
|
||||
|
||||
/** Strict caller limits for deterministic bounded traversal. */
|
||||
export const javaScriptSemanticQueryLimitsSchema = z.strictObject({
|
||||
max_seed_matches: z.number().int().min(1).max(1_000).default(25),
|
||||
max_nodes: z.number().int().min(1).max(50_000).default(2_000),
|
||||
max_edges: z.number().int().min(1).max(100_000).default(4_000),
|
||||
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.strictObject({
|
||||
authority: z.literal("none"),
|
||||
});
|
||||
|
||||
/** Parsed pure-domain query over one authenticated companion graph. */
|
||||
export const javaScriptSemanticQueryInputSchema = z.strictObject({
|
||||
seed: javaScriptSemanticQuerySeedSchema,
|
||||
direction: z.enum([
|
||||
"backward-provenance",
|
||||
"forward-influence",
|
||||
"callers",
|
||||
"ownership",
|
||||
]),
|
||||
allowed_relations: z
|
||||
.array(z.enum(JAVASCRIPT_SEMANTIC_RELATIONS))
|
||||
.min(1)
|
||||
.max(JAVASCRIPT_SEMANTIC_RELATIONS.length)
|
||||
.optional(),
|
||||
include_ambiguous_dynamic_edges: z.boolean().default(false),
|
||||
expected: z
|
||||
.strictObject({
|
||||
role: z.enum(["source", "sink"]),
|
||||
classes: z.array(z.enum(JAVASCRIPT_SEMANTIC_NODE_KINDS)).min(1).max(24),
|
||||
})
|
||||
.nullable()
|
||||
.default(null),
|
||||
source_map_authority: sourceMapAuthoritySchema.default({ authority: "none" }),
|
||||
limits: javaScriptSemanticQueryLimitsSchema.default({
|
||||
max_seed_matches: 25,
|
||||
max_nodes: 2_000,
|
||||
max_edges: 4_000,
|
||||
max_depth: 12,
|
||||
max_functions: 500,
|
||||
max_modules: 500,
|
||||
max_unknowns: 1_000,
|
||||
page_size: 200,
|
||||
}),
|
||||
cursor: z
|
||||
.string()
|
||||
.regex(/^jsrqc_[0-9]+_[a-f0-9]{64}$/u)
|
||||
.nullable()
|
||||
.default(null),
|
||||
});
|
||||
|
||||
const queryFrontierSchema = z.strictObject({
|
||||
node_id: semanticNodeIdSchema,
|
||||
depth: z.number().int().min(0).max(65),
|
||||
reason: z.enum([
|
||||
"max-depth",
|
||||
"max-edges",
|
||||
"max-functions",
|
||||
"max-modules",
|
||||
"max-nodes",
|
||||
"max-seed-matches",
|
||||
]),
|
||||
});
|
||||
|
||||
/** Deterministic, pageable semantic trace result. */
|
||||
export const javaScriptSemanticQueryResultSchema = z.strictObject({
|
||||
schema_version: z.literal(1),
|
||||
query_id: z.string().regex(/^jsrq_[a-f0-9]{64}$/u),
|
||||
source_graph_id: z.string().regex(/^jsrg_[a-f0-9]{64}$/u),
|
||||
seed: javaScriptSemanticQuerySeedSchema,
|
||||
direction: javaScriptSemanticQueryInputSchema.shape.direction,
|
||||
status: z.enum([
|
||||
"found",
|
||||
"no-match",
|
||||
"ambiguous",
|
||||
"partial",
|
||||
"truncated",
|
||||
"unsupported",
|
||||
]),
|
||||
seed_node_ids: z.array(semanticNodeIdSchema).max(1_000),
|
||||
nodes: z.array(javaScriptSemanticNodeSchema).max(3_000),
|
||||
relations: z.array(javaScriptSemanticRelationSchema).max(1_000),
|
||||
unknowns: z.array(javaScriptSemanticUnknownSchema).max(100_000),
|
||||
expected_match_node_ids: z.array(semanticNodeIdSchema).max(50_000),
|
||||
summary: z.strictObject({
|
||||
total_seed_matches: z.number().int().min(0),
|
||||
retained_seed_matches: z.number().int().min(0),
|
||||
traversed_nodes: z.number().int().min(0),
|
||||
traversed_relations: z.number().int().min(0),
|
||||
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"]),
|
||||
frontier: z.array(queryFrontierSchema).max(100_000),
|
||||
}),
|
||||
page: z.strictObject({
|
||||
offset: z.number().int().min(0),
|
||||
size: z.number().int().min(0).max(1_000),
|
||||
next_cursor: z
|
||||
.string()
|
||||
.regex(/^jsrqc_[0-9]+_[a-f0-9]{64}$/u)
|
||||
.nullable(),
|
||||
}),
|
||||
applied_limits: javaScriptSemanticQueryLimitsSchema,
|
||||
accepted_limit_ranges: z.strictObject({
|
||||
max_seed_matches: z.strictObject({
|
||||
minimum: z.literal(1),
|
||||
maximum: z.literal(1_000),
|
||||
}),
|
||||
max_nodes: z.strictObject({
|
||||
minimum: z.literal(1),
|
||||
maximum: z.literal(50_000),
|
||||
}),
|
||||
max_edges: z.strictObject({
|
||||
minimum: z.literal(1),
|
||||
maximum: z.literal(100_000),
|
||||
}),
|
||||
max_depth: z.strictObject({
|
||||
minimum: z.literal(0),
|
||||
maximum: z.literal(64),
|
||||
}),
|
||||
max_functions: z.strictObject({
|
||||
minimum: z.literal(1),
|
||||
maximum: z.literal(10_000),
|
||||
}),
|
||||
max_modules: 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),
|
||||
}),
|
||||
}),
|
||||
limitations: z.array(z.string().min(1).max(4_096)).max(1_000),
|
||||
});
|
||||
|
||||
/** Validated semantic query input. */
|
||||
export type JavaScriptSemanticQueryInput = z.infer<
|
||||
typeof javaScriptSemanticQueryInputSchema
|
||||
>;
|
||||
/** Validated semantic query result. */
|
||||
export type JavaScriptSemanticQueryResult = z.infer<
|
||||
typeof javaScriptSemanticQueryResultSchema
|
||||
>;
|
||||
|
|
@ -55,7 +55,15 @@ export const collectSemanticReturns = (
|
|||
}
|
||||
retained += 1;
|
||||
sites.push({
|
||||
returnSiteId: semanticReturnSiteId(
|
||||
callable.callableId,
|
||||
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." }
|
||||
|
|
@ -76,6 +84,12 @@ export const collectSemanticReturns = (
|
|||
});
|
||||
};
|
||||
|
||||
const semanticReturnSiteId = (
|
||||
callableId: string,
|
||||
location: JavaScriptSemanticReturnSite["location"],
|
||||
): string =>
|
||||
`${callableId}:return:${String(location.start.line)}:${String(location.start.column)}:${String(location.end.line)}:${String(location.end.column)}`;
|
||||
|
||||
/** Link exports to callables only when lexical resolution is unique and exact. */
|
||||
export const resolveSemanticModuleCallables = (
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export interface JavaScriptSemanticAnalysisState {
|
|||
readonly scopeByNode: WeakMap<t.Node, JavaScriptSemanticScopeState>;
|
||||
readonly bindingsById: Map<string, JavaScriptSemanticBindingState>;
|
||||
readonly callables: JavaScriptSemanticCallable[];
|
||||
readonly callableNodesById: Map<string, t.Node>;
|
||||
readonly moduleLinks: JavaScriptSemanticModuleLink[];
|
||||
readonly limitsReached: Set<keyof JavaScriptSemanticLimits>;
|
||||
omittedCount: number;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -200,6 +200,153 @@ describe("JavaScript semantic analysis", () => {
|
|||
expect(bindingsNamed(ir, "method")).toEqual([]);
|
||||
});
|
||||
|
||||
it("links exact local calls to parameters, returns, and closure captures", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`
|
||||
const prefix = "rea";
|
||||
function render(value, { mode }) {
|
||||
return prefix + value + mode;
|
||||
}
|
||||
const alias = render;
|
||||
const result = alias(input, { mode: "fast" });
|
||||
`);
|
||||
|
||||
expect(ir.schemaVersion).toBe(3);
|
||||
const render = onlyCallable(ir, "render");
|
||||
const call = ir.callSites.find(
|
||||
({ calleeCallableIds }) => calleeCallableIds[0] === render.callableId,
|
||||
);
|
||||
expect(call).toMatchObject({
|
||||
kind: "call",
|
||||
callerCallableId: null,
|
||||
resolution: "exact",
|
||||
calleeCallableIds: [render.callableId],
|
||||
arguments: [
|
||||
{ index: 0, spread: false },
|
||||
{ index: 1, spread: false },
|
||||
],
|
||||
});
|
||||
if (call === undefined) throw new Error("Missing render call");
|
||||
expect(
|
||||
ir.argumentFlows
|
||||
.filter(({ callSiteId }) => callSiteId === call.callSiteId)
|
||||
.map(({ argumentIndex, parameterBindingId }) => ({
|
||||
argumentIndex,
|
||||
parameter: semanticBinding(ir, parameterBindingId)?.name,
|
||||
})),
|
||||
).toEqual([
|
||||
{ argumentIndex: 0, parameter: "value" },
|
||||
{ argumentIndex: 1, parameter: "mode" },
|
||||
]);
|
||||
expect(ir.callReturnFlows).toEqual([
|
||||
expect.objectContaining({
|
||||
callSiteId: call.callSiteId,
|
||||
callableId: render.callableId,
|
||||
returnSiteId: render.returnSites[0]?.returnSiteId,
|
||||
}),
|
||||
]);
|
||||
expect(ir.closureCaptures).toEqual([
|
||||
expect.objectContaining({
|
||||
callableId: render.callableId,
|
||||
bindingId: topLevelBinding(ir, "prefix").bindingId,
|
||||
}),
|
||||
]);
|
||||
expect(ir.frontiers).toEqual([]);
|
||||
});
|
||||
|
||||
it("retains ambiguous local callees and explicit dynamic frontiers", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`
|
||||
function left(value) { return value; }
|
||||
function right(value) { return value; }
|
||||
const selected = chooseLeft ? left : right;
|
||||
selected(input);
|
||||
receiver[key](input);
|
||||
external(input);
|
||||
`);
|
||||
|
||||
const selected = ir.callSites.find(
|
||||
({ resolution }) => resolution === "ambiguous",
|
||||
);
|
||||
expect(selected?.calleeCallableIds).toEqual(
|
||||
[
|
||||
onlyCallable(ir, "left").callableId,
|
||||
onlyCallable(ir, "right").callableId,
|
||||
].sort(),
|
||||
);
|
||||
expect(
|
||||
ir.argumentFlows.filter(
|
||||
({ callSiteId }) => callSiteId === selected?.callSiteId,
|
||||
),
|
||||
).toHaveLength(2);
|
||||
expect(ir.frontiers).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "dynamic-call",
|
||||
reason: expect.stringMatching(/ambiguous/iu),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: "dynamic-property",
|
||||
reason: expect.stringMatching(/computed member/iu),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: "dynamic-call",
|
||||
reason: "Unresolved call target external.",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not project positional argument flow after a spread", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`
|
||||
function target(first, second, third) { return third; }
|
||||
target(one, ...rest, three);
|
||||
`);
|
||||
|
||||
expect(ir.argumentFlows.map(({ argumentIndex }) => argumentIndex)).toEqual([
|
||||
0,
|
||||
]);
|
||||
});
|
||||
|
||||
it("applies independent hard bounds to every new semantic fact family", () => {
|
||||
const ir = analyzeJavaScriptSemantics(
|
||||
`
|
||||
const captured = 1;
|
||||
function target(value, other) { return value + other + captured; }
|
||||
target(1, 2);
|
||||
target(3, 4);
|
||||
object[first];
|
||||
object[second];
|
||||
`,
|
||||
{
|
||||
maxCallSites: 1,
|
||||
maxCallArguments: 1,
|
||||
maxArgumentFlows: 0,
|
||||
maxCallReturnFlows: 0,
|
||||
maxClosureCaptures: 0,
|
||||
maxFrontiers: 1,
|
||||
},
|
||||
);
|
||||
|
||||
expect(ir.callSites).toHaveLength(1);
|
||||
expect(ir.callSites[0]?.arguments).toEqual([
|
||||
expect.objectContaining({ index: 0, spread: false }),
|
||||
]);
|
||||
expect(ir.argumentFlows).toEqual([]);
|
||||
expect(ir.callReturnFlows).toEqual([]);
|
||||
expect(ir.closureCaptures).toEqual([]);
|
||||
expect(ir.frontiers).toHaveLength(1);
|
||||
expect(ir.coverage).toMatchObject({
|
||||
status: "truncated",
|
||||
limitsReached: expect.arrayContaining([
|
||||
"maxCallSites",
|
||||
"maxCallArguments",
|
||||
"maxArgumentFlows",
|
||||
"maxCallReturnFlows",
|
||||
"maxClosureCaptures",
|
||||
"maxFrontiers",
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it("recovers only direct return sites with literal and unknown object fields", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`
|
||||
export default function parseMarkdown(value) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,517 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { ApplicationGraphEvidence } from "../src/domain/javascriptApplicationEvidenceSchemas.js";
|
||||
import {
|
||||
JAVASCRIPT_SEMANTIC_NODE_KINDS,
|
||||
JAVASCRIPT_SEMANTIC_RELATION_FAMILIES,
|
||||
JAVASCRIPT_SEMANTIC_RELATIONS,
|
||||
} from "../src/domain/javascriptSemanticGraphSchemas.js";
|
||||
import {
|
||||
createJavaScriptSemanticFingerprint,
|
||||
createJavaScriptSemanticGraph,
|
||||
createJavaScriptSemanticGraphNode,
|
||||
createJavaScriptSemanticGraphRelation,
|
||||
createJavaScriptSemanticGraphUnknown,
|
||||
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";
|
||||
|
||||
const SHA = "a".repeat(64);
|
||||
const JAG_ID = `jag_${"b".repeat(64)}`;
|
||||
const completeCoverage = {
|
||||
status: "complete" as const,
|
||||
truncated: false,
|
||||
omitted_count: 0,
|
||||
limits: [],
|
||||
};
|
||||
|
||||
const evidence = (
|
||||
state: "observed" | "inferred" = "observed",
|
||||
): ApplicationGraphEvidence => ({
|
||||
authority:
|
||||
state === "observed"
|
||||
? "ast-static-analysis"
|
||||
: "static-relationship-inference",
|
||||
state,
|
||||
confidence: state === "observed" ? "exact" : "high",
|
||||
artifact: { available: true, artifact_id: `art_${SHA}`, sha256: SHA },
|
||||
location: {
|
||||
available: true,
|
||||
value: {
|
||||
kind: "source-range",
|
||||
source: "bundle.js",
|
||||
start: { line: 1, column: 0 },
|
||||
end: { line: 1, column: 10 },
|
||||
},
|
||||
},
|
||||
extractor: {
|
||||
name: "test",
|
||||
version: "1",
|
||||
operation: "recover-semantic-relation",
|
||||
executable_sha256: null,
|
||||
},
|
||||
coverage: completeCoverage,
|
||||
limitations:
|
||||
state === "observed"
|
||||
? []
|
||||
: ["Static reachability does not prove runtime execution."],
|
||||
evidence_ids: [],
|
||||
});
|
||||
|
||||
const unknownEvidence = (): ApplicationGraphEvidence => ({
|
||||
authority: "unknown",
|
||||
state: "unknown",
|
||||
confidence: "unknown",
|
||||
artifact: {
|
||||
available: false,
|
||||
reason: "unknown",
|
||||
detail: "Unknown artifact.",
|
||||
},
|
||||
location: {
|
||||
available: false,
|
||||
reason: "unresolved",
|
||||
detail: "Dynamic call target.",
|
||||
},
|
||||
extractor: {
|
||||
name: "test",
|
||||
version: "1",
|
||||
operation: "retain-dynamic-call",
|
||||
executable_sha256: null,
|
||||
},
|
||||
coverage: {
|
||||
status: "unknown",
|
||||
truncated: false,
|
||||
omitted_count: null,
|
||||
limits: [],
|
||||
},
|
||||
limitations: ["Dynamic call target remains unknown."],
|
||||
evidence_ids: [],
|
||||
});
|
||||
|
||||
const node = (
|
||||
kind: (typeof JAVASCRIPT_SEMANTIC_NODE_KINDS)[number],
|
||||
role: string,
|
||||
properties: Record<string, string | number | boolean | null> = {},
|
||||
functionNodeId: string | null = null,
|
||||
): JavaScriptSemanticGraphNode =>
|
||||
createJavaScriptSemanticGraphNode({
|
||||
kind,
|
||||
identity: {
|
||||
artifact_sha256: SHA,
|
||||
module_path: "bundle.js",
|
||||
source_range: {
|
||||
start: { line: 1, column: role.length },
|
||||
end: { line: 1, column: role.length + 1 },
|
||||
},
|
||||
role_key: role,
|
||||
},
|
||||
function_node_id: functionNodeId,
|
||||
application_node_ids: [],
|
||||
label: role,
|
||||
properties,
|
||||
evidence: evidence(),
|
||||
});
|
||||
|
||||
const fixtureGraph = (withUnknown = false): JavaScriptSemanticGraph => {
|
||||
const module = node("module", "module");
|
||||
const literal = node("literal", "literal", { value: "TOKEN" });
|
||||
const binding = node("binding", "binding");
|
||||
const callable = node("function", "function");
|
||||
const request = node("request", "request", {
|
||||
endpoint: "https://example.invalid/v1",
|
||||
});
|
||||
const relations = [
|
||||
createJavaScriptSemanticGraphRelation({
|
||||
source_node_id: literal.node_id,
|
||||
target_node_id: binding.node_id,
|
||||
relation: "defines",
|
||||
resolution: "resolved",
|
||||
properties: {},
|
||||
evidence: evidence("inferred"),
|
||||
}),
|
||||
createJavaScriptSemanticGraphRelation({
|
||||
source_node_id: binding.node_id,
|
||||
target_node_id: callable.node_id,
|
||||
relation: "captures",
|
||||
resolution: "resolved",
|
||||
properties: {},
|
||||
evidence: evidence("inferred"),
|
||||
}),
|
||||
createJavaScriptSemanticGraphRelation({
|
||||
source_node_id: callable.node_id,
|
||||
target_node_id: request.node_id,
|
||||
relation: "constructs-request",
|
||||
resolution: "resolved",
|
||||
properties: {},
|
||||
evidence: evidence("inferred"),
|
||||
}),
|
||||
];
|
||||
const dynamic = createJavaScriptSemanticGraphUnknown({
|
||||
node_id: callable.node_id,
|
||||
family: "call-flow",
|
||||
relation_kinds: ["calls"],
|
||||
reason: "dynamic-call",
|
||||
detail: "Computed callee is unresolved.",
|
||||
candidate_node_ids: [],
|
||||
evidence: unknownEvidence(),
|
||||
});
|
||||
const unknowns = withUnknown ? [dynamic] : [];
|
||||
const coverageFamilies = JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.map(
|
||||
(family) => ({
|
||||
family,
|
||||
status:
|
||||
withUnknown && family === "call-flow"
|
||||
? ("unknown" as const)
|
||||
: ("complete" as const),
|
||||
retained_relations: relations.filter(({ relation }) =>
|
||||
relation === "defines"
|
||||
? family === "data-flow"
|
||||
: relation === "captures"
|
||||
? family === "closure"
|
||||
: family === "request",
|
||||
).length,
|
||||
omitted_relations: withUnknown && family === "call-flow" ? null : 0,
|
||||
unknown_ids:
|
||||
withUnknown && family === "call-flow" ? [dynamic.unknown_id] : [],
|
||||
}),
|
||||
);
|
||||
const fingerprint = createJavaScriptSemanticFingerprint({
|
||||
function_node_id: callable.node_id,
|
||||
algorithm: "rea.javascript-semantic-function/v1",
|
||||
status: "complete",
|
||||
components: {
|
||||
parameter_arity: 0,
|
||||
normalized_ast_sha256: "1".repeat(64),
|
||||
control_flow_sha256: "2".repeat(64),
|
||||
relation_shape_sha256: "3".repeat(64),
|
||||
literal_set_sha256: "4".repeat(64),
|
||||
effects: ["network"],
|
||||
},
|
||||
limitations: [],
|
||||
evidence: evidence("inferred"),
|
||||
});
|
||||
return createJavaScriptSemanticGraph({
|
||||
schema: "JavaScriptSemanticRelationGraph",
|
||||
schema_version: 1,
|
||||
root_artifact_sha256: SHA,
|
||||
application_graph_id: JAG_ID,
|
||||
root_node_ids: [module.node_id],
|
||||
nodes: [request, callable, binding, literal, module],
|
||||
relations,
|
||||
fingerprints: [fingerprint],
|
||||
unknowns,
|
||||
coverage: {
|
||||
status: withUnknown ? "partial" : "complete",
|
||||
truncated: false,
|
||||
omitted_nodes: withUnknown ? null : 0,
|
||||
omitted_relations: withUnknown ? null : 0,
|
||||
limits: [],
|
||||
families: coverageFamilies,
|
||||
},
|
||||
limitations: withUnknown ? ["Dynamic call target remains unknown."] : [],
|
||||
});
|
||||
};
|
||||
|
||||
const graphWithCandidateEdge = (): JavaScriptSemanticGraph => {
|
||||
const graph = fixtureGraph();
|
||||
const literal = graph.nodes.find(({ kind }) => kind === "literal");
|
||||
const request = graph.nodes.find(({ kind }) => kind === "request");
|
||||
if (literal === undefined || request === undefined)
|
||||
throw new TypeError("Semantic fixture nodes are missing");
|
||||
const candidate = createJavaScriptSemanticGraphRelation({
|
||||
source_node_id: literal.node_id,
|
||||
target_node_id: request.node_id,
|
||||
relation: "supplies-request-field",
|
||||
resolution: "candidate",
|
||||
properties: { field: "authorization" },
|
||||
evidence: evidence("inferred"),
|
||||
});
|
||||
const { graph_id: _graphId, ...input } = graph;
|
||||
return createJavaScriptSemanticGraph({
|
||||
...input,
|
||||
relations: [...graph.relations, candidate],
|
||||
coverage: {
|
||||
...graph.coverage,
|
||||
families: graph.coverage.families.map((family) =>
|
||||
family.family === "request"
|
||||
? {
|
||||
...family,
|
||||
retained_relations: family.retained_relations + 1,
|
||||
}
|
||||
: family,
|
||||
),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const graphWithDuplicateLiteral = (): JavaScriptSemanticGraph => {
|
||||
const graph = fixtureGraph();
|
||||
const duplicate = node("literal", "literal-copy", { value: "TOKEN" });
|
||||
const { graph_id: _graphId, ...input } = graph;
|
||||
return createJavaScriptSemanticGraph({
|
||||
...input,
|
||||
nodes: [...graph.nodes, duplicate],
|
||||
});
|
||||
};
|
||||
|
||||
describe("JavaScript semantic relation graph", () => {
|
||||
it("defines every required v1 relation family and canonicalizes records", () => {
|
||||
expect(JAVASCRIPT_SEMANTIC_RELATIONS).toContain("argument-to-parameter");
|
||||
expect(JAVASCRIPT_SEMANTIC_RELATIONS).toContain("detaches-task");
|
||||
expect(JAVASCRIPT_SEMANTIC_RELATIONS).toContain("forwards-signal");
|
||||
expect(JAVASCRIPT_SEMANTIC_RELATIONS).toContain("validates");
|
||||
const graph = fixtureGraph();
|
||||
expect(
|
||||
parseJavaScriptSemanticGraph(
|
||||
JSON.parse(serializeJavaScriptSemanticGraph(graph)),
|
||||
),
|
||||
).toEqual(graph);
|
||||
expect(graph.nodes.map(({ node_id }) => node_id)).toEqual(
|
||||
graph.nodes.map(({ node_id }) => node_id).toSorted(),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects stale identities, dangling endpoints, and incomplete coverage claims", () => {
|
||||
const graph = fixtureGraph();
|
||||
const { graph_id: _graphId, ...graphInput } = graph;
|
||||
expect(() =>
|
||||
parseJavaScriptSemanticGraph({
|
||||
...graph,
|
||||
graph_id: `jsrg_${"0".repeat(64)}`,
|
||||
}),
|
||||
).toThrow();
|
||||
expect(() =>
|
||||
createJavaScriptSemanticGraph({
|
||||
...graphInput,
|
||||
relations: [
|
||||
{
|
||||
...graph.relations[0],
|
||||
target_node_id: `jsrg_node_${"f".repeat(64)}`,
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toThrow();
|
||||
expect(() =>
|
||||
createJavaScriptSemanticGraph({
|
||||
...graphInput,
|
||||
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 = {
|
||||
seed: { kind: "literal" as const, value: "TOKEN" },
|
||||
direction: "forward-influence" as const,
|
||||
expected: { role: "sink" as const, classes: ["request" as const] },
|
||||
limits: {
|
||||
max_seed_matches: 25,
|
||||
max_nodes: 20,
|
||||
max_edges: 20,
|
||||
max_depth: 12,
|
||||
max_functions: 10,
|
||||
max_modules: 10,
|
||||
page_size: 2,
|
||||
},
|
||||
};
|
||||
const first = queryJavaScriptSemanticGraph(graph, input);
|
||||
expect(first).toMatchObject({
|
||||
status: "found",
|
||||
summary: { traversed_nodes: 4, traversed_relations: 3 },
|
||||
page: { offset: 0, size: 2 },
|
||||
});
|
||||
expect(first.expected_match_node_ids).toHaveLength(1);
|
||||
expect(first.page.next_cursor).not.toBeNull();
|
||||
const second = queryJavaScriptSemanticGraph(graph, {
|
||||
...input,
|
||||
cursor: first.page.next_cursor,
|
||||
});
|
||||
expect(second.query_id).toBe(first.query_id);
|
||||
expect(second.page).toMatchObject({
|
||||
offset: 2,
|
||||
size: 1,
|
||||
next_cursor: null,
|
||||
});
|
||||
expect(second.relations[0]?.relation_id).not.toBe(
|
||||
first.relations[0]?.relation_id,
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps relevant dynamic frontiers unknown", () => {
|
||||
const result = queryJavaScriptSemanticGraph(fixtureGraph(true), {
|
||||
seed: { kind: "literal", value: "TOKEN" },
|
||||
direction: "forward-influence",
|
||||
});
|
||||
expect(result.status).toBe("found");
|
||||
expect(result.coverage.status).toBe("partial");
|
||||
expect(result.unknowns).toMatchObject([
|
||||
{ reason: "dynamic-call", relation_kinds: ["calls"] },
|
||||
]);
|
||||
});
|
||||
|
||||
it("excludes candidate edges unless the caller explicitly opts in", () => {
|
||||
const graph = graphWithCandidateEdge();
|
||||
const input = {
|
||||
seed: { kind: "literal" as const, value: "TOKEN" },
|
||||
direction: "forward-influence" as const,
|
||||
allowed_relations: ["supplies-request-field" as const],
|
||||
};
|
||||
const conservative = queryJavaScriptSemanticGraph(graph, input);
|
||||
const optedIn = queryJavaScriptSemanticGraph(graph, {
|
||||
...input,
|
||||
include_ambiguous_dynamic_edges: true,
|
||||
});
|
||||
expect(conservative.summary.traversed_relations).toBe(0);
|
||||
expect(optedIn.summary.traversed_relations).toBe(1);
|
||||
expect(conservative.status).toBe("ambiguous");
|
||||
expect(conservative.coverage.status).toBe("partial");
|
||||
expect(optedIn.status).toBe("ambiguous");
|
||||
expect(optedIn.relations).toMatchObject([
|
||||
{ relation: "supplies-request-field", resolution: "candidate" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("bounds seed ambiguity before traversal", () => {
|
||||
const result = queryJavaScriptSemanticGraph(graphWithDuplicateLiteral(), {
|
||||
seed: { kind: "literal", value: "TOKEN" },
|
||||
direction: "forward-influence",
|
||||
limits: {
|
||||
max_seed_matches: 1,
|
||||
max_nodes: 20,
|
||||
max_edges: 20,
|
||||
max_depth: 12,
|
||||
max_functions: 10,
|
||||
max_modules: 10,
|
||||
page_size: 10,
|
||||
},
|
||||
});
|
||||
expect(result.summary).toMatchObject({
|
||||
total_seed_matches: 2,
|
||||
retained_seed_matches: 1,
|
||||
});
|
||||
expect(result.status).toBe("truncated");
|
||||
expect(result.coverage.frontier).toContainEqual(
|
||||
expect.objectContaining({ reason: "max-seed-matches", depth: 0 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("reports exact caller limits and a deterministic truncation frontier", () => {
|
||||
const result = queryJavaScriptSemanticGraph(fixtureGraph(), {
|
||||
seed: { kind: "literal", value: "TOKEN" },
|
||||
direction: "forward-influence",
|
||||
limits: {
|
||||
max_seed_matches: 1,
|
||||
max_nodes: 2,
|
||||
max_edges: 10,
|
||||
max_depth: 12,
|
||||
max_functions: 10,
|
||||
max_modules: 10,
|
||||
page_size: 10,
|
||||
},
|
||||
});
|
||||
expect(result.status).toBe("truncated");
|
||||
expect(result.coverage.frontier).toMatchObject([{ reason: "max-nodes" }]);
|
||||
expect(result.applied_limits.max_nodes).toBe(2);
|
||||
expect(result.accepted_limit_ranges.max_nodes).toEqual({
|
||||
minimum: 1,
|
||||
maximum: 50_000,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
limit: { max_edges: 1 },
|
||||
reason: "max-edges",
|
||||
},
|
||||
{
|
||||
limit: { max_depth: 0 },
|
||||
reason: "max-depth",
|
||||
},
|
||||
])("reports the $reason frontier", ({ limit, reason }) => {
|
||||
const result = queryJavaScriptSemanticGraph(fixtureGraph(), {
|
||||
seed: { kind: "literal", value: "TOKEN" },
|
||||
direction: "forward-influence",
|
||||
limits: {
|
||||
max_seed_matches: 10,
|
||||
max_nodes: 20,
|
||||
max_edges: limit.max_edges ?? 20,
|
||||
max_depth: limit.max_depth ?? 12,
|
||||
max_functions: 10,
|
||||
max_modules: 10,
|
||||
page_size: 10,
|
||||
},
|
||||
});
|
||||
expect(result.status).toBe("truncated");
|
||||
expect(result.coverage.frontier).toContainEqual(
|
||||
expect.objectContaining({ reason }),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects out-of-range limits and cursors from another query", () => {
|
||||
expect(() =>
|
||||
javaScriptSemanticQueryInputSchema.parse({
|
||||
seed: { kind: "literal", value: "TOKEN" },
|
||||
direction: "forward-influence",
|
||||
limits: { max_nodes: 50_001 },
|
||||
}),
|
||||
).toThrow();
|
||||
const graph = fixtureGraph();
|
||||
const first = queryJavaScriptSemanticGraph(graph, {
|
||||
seed: { kind: "literal", value: "TOKEN" },
|
||||
direction: "forward-influence",
|
||||
limits: { page_size: 1 },
|
||||
});
|
||||
expect(() =>
|
||||
queryJavaScriptSemanticGraph(graph, {
|
||||
seed: { kind: "endpoint", value: "https://example.invalid/v1" },
|
||||
direction: "backward-provenance",
|
||||
limits: { page_size: 1 },
|
||||
cursor: first.page.next_cursor,
|
||||
}),
|
||||
).toThrow(/cursor does not match/u);
|
||||
});
|
||||
});
|
||||
|
|
@ -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