368 lines
11 KiB
TypeScript
368 lines
11 KiB
TypeScript
import { z } from "zod";
|
|
|
|
import { applicationGraphEvidenceSchema } from "./javascriptApplicationEvidenceSchemas.js";
|
|
import { jsonValueSchema } from "../jsonValue.js";
|
|
import { digestSchema } from "../digests.js";
|
|
import { prefixedDigestSchema } from "../digests.js";
|
|
|
|
const boundedTextSchema = z.string().min(1);
|
|
const semanticNodeIdSchema = prefixedDigestSchema("jsrg_node");
|
|
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. */
|
|
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 the graph. */
|
|
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)
|
|
.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), jsonValueSchema);
|
|
|
|
const semanticNodeIdentitySchema = z.strictObject({
|
|
artifact_sha256: digestSchema,
|
|
module_path: relativePathSchema,
|
|
source_range: sourceRangeSchema.nullable(),
|
|
role_key: z.string().min(1),
|
|
});
|
|
|
|
/** 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(prefixedDigestSchema("jag_node")),
|
|
label: z.string().min(1).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",
|
|
"dynamic-scope",
|
|
"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),
|
|
reason: z.enum(JAVASCRIPT_SEMANTIC_UNKNOWN_REASONS),
|
|
detail: boundedTextSchema,
|
|
candidate_node_ids: z.array(semanticNodeIdSchema),
|
|
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),
|
|
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",
|
|
]),
|
|
),
|
|
});
|
|
|
|
/** One bounded rename-resistant function fingerprint. */
|
|
export const javaScriptSemanticFingerprintInputSchema = z.strictObject({
|
|
function_node_id: semanticNodeIdSchema,
|
|
algorithm: z.literal("rea.javascript-semantic-function"),
|
|
status: z.enum(["complete", "partial", "unavailable"]),
|
|
components: fingerprintComponentsSchema,
|
|
limitations: z.array(boundedTextSchema),
|
|
evidence: applicationGraphEvidenceSchema,
|
|
});
|
|
|
|
/** One canonical function fingerprint record. */
|
|
export const javaScriptSemanticFingerprintSchema =
|
|
javaScriptSemanticFingerprintInputSchema.extend({
|
|
fingerprint_id: prefixedDigestSchema("jsrg_fingerprint"),
|
|
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),
|
|
});
|
|
|
|
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),
|
|
value: z.number().int().min(0),
|
|
unit: z.enum(["items", "bytes", "milliseconds", "depth", "other"]),
|
|
}),
|
|
),
|
|
families: z.array(familyCoverageSchema).length(12),
|
|
});
|
|
|
|
/** Graph content before its top-level commitment is derived. */
|
|
export const javaScriptSemanticGraphInputSchema = z.strictObject({
|
|
schema: z.literal("JavaScriptSemanticRelationGraph"),
|
|
root_artifact_sha256: digestSchema,
|
|
application_graph_id: prefixedDigestSchema("jag"),
|
|
root_node_ids: z.array(semanticNodeIdSchema).min(1),
|
|
nodes: z.array(javaScriptSemanticNodeSchema).min(1),
|
|
relations: z.array(javaScriptSemanticRelationSchema),
|
|
fingerprints: z.array(javaScriptSemanticFingerprintSchema),
|
|
unknowns: z.array(javaScriptSemanticUnknownSchema),
|
|
coverage: graphCoverageSchema,
|
|
limitations: z.array(boundedTextSchema),
|
|
});
|
|
|
|
/** Stored JavaScript Semantic Relation Graph. */
|
|
export const javaScriptSemanticGraphRecordSchema =
|
|
javaScriptSemanticGraphInputSchema.extend({
|
|
graph_id: prefixedDigestSchema("jsrg"),
|
|
});
|
|
|
|
/** 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
|
|
>;
|