mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04:00
refactor(javascript): group domain semantics and contracts (#907)
This commit is contained in:
@@ -0,0 +1,474 @@
|
||||
import { z } from "zod";
|
||||
import { canonicalDigest, canonicalJson } from "../comparisonSemantics.js";
|
||||
import { compareCodePoints, uniqueSorted } from "../canonicalOrdering.js";
|
||||
|
||||
import type {
|
||||
ApplicationGraphEvidence,
|
||||
ApplicationNodeIdentity,
|
||||
} from "./javascriptApplicationEvidenceSchemas.js";
|
||||
import {
|
||||
applicationEdgeInputSchema,
|
||||
applicationEdgeSchema,
|
||||
applicationNodeInputSchema,
|
||||
applicationNodeObservationSchema,
|
||||
applicationNodeSchema,
|
||||
javascriptApplicationGraphInputSchema,
|
||||
javascriptApplicationGraphRecordSchema,
|
||||
type ApplicationEdge,
|
||||
type ApplicationNode,
|
||||
type JavaScriptApplicationGraphInput,
|
||||
} from "./javascriptApplicationGraphSchemas.js";
|
||||
|
||||
const OBSERVATION_IDENTIFIER_STRATEGY = {
|
||||
strategy: "semantic-content-sha256" as const,
|
||||
stability: "observation-exact" as const,
|
||||
};
|
||||
const EDGE_IDENTIFIER_STRATEGY = {
|
||||
strategy: "semantic-content-sha256" as const,
|
||||
stability: "relationship-exact" as const,
|
||||
};
|
||||
|
||||
const limitKey = (
|
||||
limit: ApplicationGraphEvidence["coverage"]["limits"][number],
|
||||
) => canonicalJson(limit, "JavaScript Application Graph");
|
||||
|
||||
const normalizeLimits = (
|
||||
limits: ApplicationGraphEvidence["coverage"]["limits"],
|
||||
): ApplicationGraphEvidence["coverage"]["limits"] =>
|
||||
[...new Map(limits.map((limit) => [limitKey(limit), limit])).values()].sort(
|
||||
(left, right) => compareCodePoints(limitKey(left), limitKey(right)),
|
||||
);
|
||||
|
||||
const normalizeEvidence = (
|
||||
evidence: ApplicationGraphEvidence,
|
||||
): ApplicationGraphEvidence => ({
|
||||
...evidence,
|
||||
coverage: {
|
||||
...evidence.coverage,
|
||||
limits: normalizeLimits(evidence.coverage.limits),
|
||||
},
|
||||
limitations: uniqueSorted(evidence.limitations),
|
||||
evidence_ids: uniqueSorted(evidence.evidence_ids),
|
||||
});
|
||||
|
||||
const normalizeIdentity = (
|
||||
identity: ApplicationNodeIdentity,
|
||||
): ApplicationNodeIdentity =>
|
||||
identity.strategy === "structural-fingerprint"
|
||||
? { ...identity, basis: uniqueSorted(identity.basis) }
|
||||
: identity;
|
||||
|
||||
const nodeSemantic = (node: Pick<ApplicationNode, "kind" | "identity">) => ({
|
||||
kind: node.kind,
|
||||
identity: node.identity,
|
||||
});
|
||||
|
||||
const nodeId = (node: Pick<ApplicationNode, "kind" | "identity">): string =>
|
||||
`jag_node_${canonicalDigest(nodeSemantic(node), "JavaScript Application Graph")}`;
|
||||
|
||||
const observationSemantic = (
|
||||
nodeIdentifier: string,
|
||||
observation: Omit<
|
||||
z.infer<typeof applicationNodeObservationSchema>,
|
||||
"observation_id"
|
||||
>,
|
||||
) => ({ node_id: nodeIdentifier, ...observation });
|
||||
|
||||
const observationId = (
|
||||
nodeIdentifier: string,
|
||||
observation: Omit<
|
||||
z.infer<typeof applicationNodeObservationSchema>,
|
||||
"observation_id"
|
||||
>,
|
||||
): string =>
|
||||
`jag_observation_${canonicalDigest(
|
||||
observationSemantic(nodeIdentifier, observation),
|
||||
)}`;
|
||||
|
||||
type EdgeSemantic = Omit<ApplicationEdge, "edge_id">;
|
||||
|
||||
const edgeId = (edge: EdgeSemantic): string =>
|
||||
`jag_edge_${canonicalDigest(edge, "JavaScript Application Graph")}`;
|
||||
|
||||
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",
|
||||
message: `${label} must be sorted and unique by Unicode code point`,
|
||||
path: [...path, index],
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const checkEvidenceNormalization = (
|
||||
evidence: ApplicationGraphEvidence,
|
||||
path: PropertyKey[],
|
||||
context: z.RefinementCtx,
|
||||
): void => {
|
||||
sortedUniqueIssue(
|
||||
evidence.limitations,
|
||||
[...path, "limitations"],
|
||||
"Evidence limitations",
|
||||
context,
|
||||
);
|
||||
sortedUniqueIssue(
|
||||
evidence.evidence_ids,
|
||||
[...path, "evidence_ids"],
|
||||
"Evidence identifiers",
|
||||
context,
|
||||
);
|
||||
sortedUniqueIssue(
|
||||
evidence.coverage.limits.map(limitKey),
|
||||
[...path, "coverage", "limits"],
|
||||
"Evidence limits",
|
||||
context,
|
||||
);
|
||||
|
||||
const runtimeLocation =
|
||||
evidence.location.available && evidence.location.value.kind === "runtime";
|
||||
const staticAuthority = [
|
||||
"artifact-bytes",
|
||||
"ast-static-analysis",
|
||||
"static-relationship-inference",
|
||||
"native-analysis-provider",
|
||||
].includes(evidence.authority);
|
||||
if (runtimeLocation && staticAuthority)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Static and native authorities cannot claim a runtime location",
|
||||
path: [...path, "location"],
|
||||
});
|
||||
};
|
||||
|
||||
const observationMatchesIdentity = (
|
||||
node: ApplicationNode,
|
||||
observation: ApplicationNode["observations"][number],
|
||||
): boolean => {
|
||||
const { identity } = node;
|
||||
const { evidence } = observation;
|
||||
if (identity.strategy === "content-digest")
|
||||
return (
|
||||
evidence.state === "observed" &&
|
||||
evidence.artifact.available &&
|
||||
evidence.artifact.sha256 === identity.sha256
|
||||
);
|
||||
if (identity.strategy === "source-map-original")
|
||||
return (
|
||||
evidence.state === "observed" &&
|
||||
evidence.artifact.available &&
|
||||
evidence.artifact.sha256 === identity.source_map_sha256
|
||||
);
|
||||
if (identity.strategy === "canonical-path")
|
||||
return (
|
||||
evidence.artifact.available &&
|
||||
evidence.artifact.sha256 === identity.artifact_sha256 &&
|
||||
evidence.location.available &&
|
||||
evidence.location.value.kind === "artifact-path" &&
|
||||
evidence.location.value.path === identity.path
|
||||
);
|
||||
if (identity.strategy === "artifact-local-key")
|
||||
return (
|
||||
evidence.artifact.available &&
|
||||
evidence.artifact.sha256 === identity.artifact_sha256
|
||||
);
|
||||
if (identity.strategy === "runtime-instance") {
|
||||
if (
|
||||
!["passive-cdp-runtime", "controlled-replay"].includes(
|
||||
evidence.authority,
|
||||
) ||
|
||||
!evidence.location.available ||
|
||||
evidence.location.value.kind !== "runtime"
|
||||
)
|
||||
return false;
|
||||
const location = evidence.location.value;
|
||||
return (
|
||||
location.capture_sha256 === identity.capture_sha256 &&
|
||||
[location.target_key, location.frame_key, location.script_key].includes(
|
||||
identity.runtime_key,
|
||||
)
|
||||
);
|
||||
}
|
||||
if (identity.strategy === "structural-fingerprint")
|
||||
return evidence.state === "inferred";
|
||||
return evidence.state === "observed";
|
||||
};
|
||||
|
||||
const checkNode = (
|
||||
node: ApplicationNode,
|
||||
index: number,
|
||||
context: z.RefinementCtx,
|
||||
): void => {
|
||||
if (node.node_id !== nodeId(node))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Node identifier does not match its kind and identity",
|
||||
path: ["nodes", index, "node_id"],
|
||||
});
|
||||
if (
|
||||
node.identity.strategy === "structural-fingerprint" &&
|
||||
canonicalJson(node.identity.basis, "JavaScript Application Graph") !==
|
||||
canonicalJson(
|
||||
uniqueSorted(node.identity.basis),
|
||||
"JavaScript Application Graph",
|
||||
)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Structural fingerprint basis must be sorted and unique",
|
||||
path: ["nodes", index, "identity", "basis"],
|
||||
});
|
||||
|
||||
sortedUniqueIssue(
|
||||
node.observations.map(({ observation_id: id }) => id),
|
||||
["nodes", index, "observations"],
|
||||
"Node observations",
|
||||
context,
|
||||
);
|
||||
for (const [observationIndex, observation] of node.observations.entries()) {
|
||||
const { observation_id: identifier, ...semantic } = observation;
|
||||
if (identifier !== observationId(node.node_id, semantic))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Observation identifier does not match its semantic content",
|
||||
path: [
|
||||
"nodes",
|
||||
index,
|
||||
"observations",
|
||||
observationIndex,
|
||||
"observation_id",
|
||||
],
|
||||
});
|
||||
checkEvidenceNormalization(
|
||||
observation.evidence,
|
||||
["nodes", index, "observations", observationIndex, "evidence"],
|
||||
context,
|
||||
);
|
||||
}
|
||||
if (
|
||||
!node.observations.some((observation) =>
|
||||
observationMatchesIdentity(node, observation),
|
||||
)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Node identity is not supported by a compatible observation",
|
||||
path: ["nodes", index, "identity"],
|
||||
});
|
||||
};
|
||||
|
||||
type GraphRecord = z.infer<typeof javascriptApplicationGraphRecordSchema>;
|
||||
|
||||
const checkGraphInvariants = (
|
||||
graph: GraphRecord,
|
||||
context: z.RefinementCtx,
|
||||
): void => {
|
||||
sortedUniqueIssue(
|
||||
graph.root_node_ids,
|
||||
["root_node_ids"],
|
||||
"Root nodes",
|
||||
context,
|
||||
);
|
||||
sortedUniqueIssue(
|
||||
graph.nodes.map(({ node_id: id }) => id),
|
||||
["nodes"],
|
||||
"Nodes",
|
||||
context,
|
||||
);
|
||||
sortedUniqueIssue(
|
||||
graph.edges.map(({ edge_id: id }) => id),
|
||||
["edges"],
|
||||
"Edges",
|
||||
context,
|
||||
);
|
||||
sortedUniqueIssue(
|
||||
graph.limitations,
|
||||
["limitations"],
|
||||
"Graph limitations",
|
||||
context,
|
||||
);
|
||||
sortedUniqueIssue(
|
||||
graph.coverage.limits.map(limitKey),
|
||||
["coverage", "limits"],
|
||||
"Graph limits",
|
||||
context,
|
||||
);
|
||||
if (graph.coverage.status !== "complete" && graph.limitations.length === 0)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Non-complete graph coverage requires an explicit limitation",
|
||||
path: ["limitations"],
|
||||
});
|
||||
|
||||
const nodesById = new Map(graph.nodes.map((node) => [node.node_id, node]));
|
||||
for (const root of graph.root_node_ids)
|
||||
if (!nodesById.has(root))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Root identifier must name a graph node",
|
||||
path: ["root_node_ids"],
|
||||
});
|
||||
for (const [index, node] of graph.nodes.entries())
|
||||
checkNode(node, index, context);
|
||||
for (const [index, edge] of graph.edges.entries()) {
|
||||
const { edge_id: identifier, ...semantic } = edge;
|
||||
if (identifier !== edgeId(semantic))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Edge identifier does not match its semantic content",
|
||||
path: ["edges", index, "edge_id"],
|
||||
});
|
||||
if (
|
||||
!nodesById.has(edge.source_node_id) ||
|
||||
!nodesById.has(edge.target_node_id)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Edge endpoints must name graph nodes",
|
||||
path: ["edges", index],
|
||||
});
|
||||
if (edge.source_node_id === edge.target_node_id)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Application graph edges cannot be self-referential",
|
||||
path: ["edges", index],
|
||||
});
|
||||
if (
|
||||
edge.relation === "changed_from" &&
|
||||
nodesById.get(edge.source_node_id)?.kind !==
|
||||
nodesById.get(edge.target_node_id)?.kind
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "changed_from endpoints must have the same node kind",
|
||||
path: ["edges", index, "relation"],
|
||||
});
|
||||
checkEvidenceNormalization(
|
||||
edge.evidence,
|
||||
["edges", index, "evidence"],
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
const { graph_id: identifier, ...semantic } = graph;
|
||||
if (
|
||||
identifier !==
|
||||
`jag_${canonicalDigest(semantic, "JavaScript Application Graph")}`
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Graph identifier does not match its semantic content",
|
||||
path: ["graph_id"],
|
||||
});
|
||||
};
|
||||
|
||||
/** Strict JavaScript Application Graph with verified commitments. */
|
||||
export const javascriptApplicationGraphSchema =
|
||||
javascriptApplicationGraphRecordSchema.superRefine(checkGraphInvariants);
|
||||
|
||||
/** Fully validated JavaScript Application Graph. */
|
||||
export type JavaScriptApplicationGraph = z.infer<
|
||||
typeof javascriptApplicationGraphSchema
|
||||
>;
|
||||
|
||||
/** Build one normalized application entity and derive all semantic IDs. */
|
||||
export const createJavaScriptApplicationNode = (
|
||||
input: unknown,
|
||||
): ApplicationNode => {
|
||||
const parsed = applicationNodeInputSchema.parse(input);
|
||||
const identity = normalizeIdentity(parsed.identity);
|
||||
const identifier = nodeId({ kind: parsed.kind, identity });
|
||||
const observations = parsed.observations
|
||||
.map((observation) => {
|
||||
const semantic = {
|
||||
...observation,
|
||||
evidence: normalizeEvidence(observation.evidence),
|
||||
identifier_strategy: OBSERVATION_IDENTIFIER_STRATEGY,
|
||||
};
|
||||
return applicationNodeObservationSchema.parse({
|
||||
...semantic,
|
||||
observation_id: observationId(identifier, semantic),
|
||||
});
|
||||
})
|
||||
.sort((left, right) =>
|
||||
compareCodePoints(left.observation_id, right.observation_id),
|
||||
);
|
||||
return applicationNodeSchema.parse({
|
||||
node_id: identifier,
|
||||
kind: parsed.kind,
|
||||
identity,
|
||||
observations,
|
||||
});
|
||||
};
|
||||
|
||||
/** Build one normalized directed relationship and derive its semantic ID. */
|
||||
export const createJavaScriptApplicationEdge = (
|
||||
input: unknown,
|
||||
): ApplicationEdge => {
|
||||
const parsed = applicationEdgeInputSchema.parse(input);
|
||||
const semantic = {
|
||||
...parsed,
|
||||
evidence: normalizeEvidence(parsed.evidence),
|
||||
identifier_strategy: EDGE_IDENTIFIER_STRATEGY,
|
||||
};
|
||||
return applicationEdgeSchema.parse({
|
||||
...semantic,
|
||||
edge_id: edgeId(semantic),
|
||||
});
|
||||
};
|
||||
|
||||
/** Normalize a complete graph and derive its top-level semantic ID. */
|
||||
export const createJavaScriptApplicationGraph = (
|
||||
input: unknown,
|
||||
): JavaScriptApplicationGraph => {
|
||||
const parsed = javascriptApplicationGraphInputSchema.parse(input);
|
||||
const semantic: JavaScriptApplicationGraphInput = {
|
||||
...parsed,
|
||||
root_node_ids: uniqueSorted(parsed.root_node_ids),
|
||||
nodes: [...parsed.nodes].sort((left, right) =>
|
||||
compareCodePoints(left.node_id, right.node_id),
|
||||
),
|
||||
edges: [...parsed.edges].sort((left, right) =>
|
||||
compareCodePoints(left.edge_id, right.edge_id),
|
||||
),
|
||||
coverage: {
|
||||
...parsed.coverage,
|
||||
limits: normalizeLimits(parsed.coverage.limits),
|
||||
},
|
||||
limitations: uniqueSorted(parsed.limitations),
|
||||
};
|
||||
return javascriptApplicationGraphSchema.parse({
|
||||
...semantic,
|
||||
graph_id: `jag_${canonicalDigest(semantic, "JavaScript Application Graph")}`,
|
||||
});
|
||||
};
|
||||
|
||||
/** Parse a stored graph and reject stale IDs. */
|
||||
export const parseJavaScriptApplicationGraph = (
|
||||
input: unknown,
|
||||
): JavaScriptApplicationGraph => javascriptApplicationGraphSchema.parse(input);
|
||||
|
||||
/** Compute the byte-stable SHA-256 commitment of a verified graph. */
|
||||
export const computeJavaScriptApplicationGraphSha256 = (
|
||||
input: unknown,
|
||||
): string =>
|
||||
canonicalDigest(
|
||||
parseJavaScriptApplicationGraph(input),
|
||||
"JavaScript Application Graph",
|
||||
);
|
||||
|
||||
/** Serialize a verified graph as RFC 8785 canonical JSON. */
|
||||
export const serializeJavaScriptApplicationGraph = (input: unknown): string =>
|
||||
canonicalJson(
|
||||
parseJavaScriptApplicationGraph(input),
|
||||
"JavaScript Application Graph",
|
||||
);
|
||||
|
||||
export type { ApplicationEdge, ApplicationGraphEvidence, ApplicationNode };
|
||||
Reference in New Issue
Block a user