mirror of
https://github.com/morluto/rea
synced 2026-10-27 22:45:19 -04:00
fix(javascript): bound semantic unknown retention at projection
Admit unknowns before canonical construction and provenance interning. Offer independent per-file and application capacities through CLI, Evidence and the owned analysis worker, and reject over-allocation during streamed decoding. Report examined omitted emission occurrences by family and reason without retaining discarded identities. Preserve omission-free graph identities and distinguish node truncation from overall partial coverage. Fixes #4797
This commit is contained in:
@@ -6,7 +6,11 @@ import { AnalysisResourceConstraintError } from "../../domain/analysisErrorCore.
|
||||
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import type { JavaScriptModuleSemanticIr } from "../../domain/javascript/javascriptModuleSemanticIr.js";
|
||||
import type { JavaScriptSemanticFileProjection } from "../../domain/javascript/javascriptSemanticFileProjection.js";
|
||||
import { JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM } from "../../domain/javascript/javascriptAnalysisResourceControls.js";
|
||||
import type {
|
||||
JavaScriptSemanticFileProjection,
|
||||
JavaScriptSemanticProjectionCapacity,
|
||||
} from "../../domain/javascript/javascriptSemanticFileProjection.js";
|
||||
import { digestSchema } from "../../domain/digests.js";
|
||||
import {
|
||||
javaScriptWorkerGraphRecordSchema,
|
||||
@@ -183,7 +187,15 @@ export const writeJavaScriptAnalysisTransfer = async (
|
||||
try {
|
||||
await append({
|
||||
kind: "projection-header",
|
||||
value: { roots: projection.roots, truncated: projection.truncated },
|
||||
value: {
|
||||
roots: projection.roots,
|
||||
nodesTruncated: projection.nodesTruncated,
|
||||
...(projection.omittedUnknownOccurrences === undefined
|
||||
? {}
|
||||
: {
|
||||
omittedUnknownOccurrences: projection.omittedUnknownOccurrences,
|
||||
}),
|
||||
},
|
||||
});
|
||||
for (const record of moduleTransferRecords(module)) await append(record);
|
||||
for (const value of projection.evidenceContexts)
|
||||
@@ -226,7 +238,7 @@ export function readJavaScriptAnalysisTransfer(
|
||||
descriptor: Descriptor,
|
||||
source: Pick<JavaScriptArtifactFile, "path" | "sha256">,
|
||||
signal: AbortSignal | undefined,
|
||||
retainGraph: boolean,
|
||||
capacity: JavaScriptSemanticProjectionCapacity | null,
|
||||
): Promise<{
|
||||
readonly module: JavaScriptModuleSemanticIr;
|
||||
readonly projection: JavaScriptSemanticFileProjection | null;
|
||||
@@ -236,11 +248,26 @@ export async function readJavaScriptAnalysisTransfer(
|
||||
descriptor: Descriptor,
|
||||
source: Pick<JavaScriptArtifactFile, "path" | "sha256">,
|
||||
signal?: AbortSignal,
|
||||
retainGraph = true,
|
||||
capacity: JavaScriptSemanticProjectionCapacity | null = {
|
||||
nodes: JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM,
|
||||
unknowns: JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM,
|
||||
},
|
||||
): Promise<{
|
||||
readonly module: JavaScriptModuleSemanticIr;
|
||||
readonly projection: JavaScriptSemanticFileProjection | null;
|
||||
}> {
|
||||
if (
|
||||
capacity !== null &&
|
||||
[capacity.nodes, capacity.unknowns].some(
|
||||
(value) =>
|
||||
!Number.isSafeInteger(value) ||
|
||||
value < 0 ||
|
||||
value > JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM,
|
||||
)
|
||||
)
|
||||
throw new RangeError(
|
||||
"JavaScript transfer requires valid retained capacities",
|
||||
);
|
||||
const expected = javaScriptAnalysisTransferDescriptorSchema.parse(descriptor);
|
||||
if ((await stat(path)).size !== expected.bytes)
|
||||
throw new TypeError(
|
||||
@@ -340,7 +367,7 @@ export async function readJavaScriptAnalysisTransfer(
|
||||
"JavaScript transfer facts precede their headers",
|
||||
);
|
||||
if (
|
||||
!retainGraph &&
|
||||
capacity === null &&
|
||||
[
|
||||
"evidence-context",
|
||||
"node",
|
||||
@@ -389,6 +416,10 @@ export async function readJavaScriptAnalysisTransfer(
|
||||
throw new TypeError(
|
||||
"JavaScript transfer node belongs to a different artifact source",
|
||||
);
|
||||
if (capacity !== null && nodes.length >= capacity.nodes)
|
||||
throw new TypeError(
|
||||
"JavaScript transfer nodes exceed the offered capacity",
|
||||
);
|
||||
nodes.push(record.value);
|
||||
break;
|
||||
}
|
||||
@@ -398,6 +429,10 @@ export async function readJavaScriptAnalysisTransfer(
|
||||
break;
|
||||
case "unknown":
|
||||
retainIdentity(record.kind, record.value.unknown_id);
|
||||
if (capacity !== null && unknowns.length >= capacity.unknowns)
|
||||
throw new TypeError(
|
||||
"JavaScript transfer unknowns exceed the offered capacity",
|
||||
);
|
||||
unknowns.push(record.value);
|
||||
break;
|
||||
case "fingerprint":
|
||||
@@ -433,15 +468,16 @@ export async function readJavaScriptAnalysisTransfer(
|
||||
throw new TypeError("JavaScript analysis transfer is missing its headers");
|
||||
return {
|
||||
module: { ...moduleHeader, scopes, bindings, callables, moduleLinks },
|
||||
projection: retainGraph
|
||||
? {
|
||||
...projectionHeader,
|
||||
evidenceContexts,
|
||||
nodes,
|
||||
relations,
|
||||
unknowns,
|
||||
fingerprints,
|
||||
}
|
||||
: null,
|
||||
projection:
|
||||
capacity !== null
|
||||
? {
|
||||
...projectionHeader,
|
||||
evidenceContexts,
|
||||
nodes,
|
||||
relations,
|
||||
unknowns,
|
||||
fingerprints,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { JavaScriptSemanticProjectionCapacity } from "../../domain/javascript/javascriptSemanticFileProjection.js";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { readFile, rm, stat, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
@@ -154,7 +155,7 @@ export class JavaScriptAnalysisWorker {
|
||||
/** Analyze inert source; hard worker failure does not terminate this process. */
|
||||
async analyze(
|
||||
file: JavaScriptArtifactFile,
|
||||
nodeBudget: number,
|
||||
capacity: JavaScriptSemanticProjectionCapacity,
|
||||
signal?: AbortSignal,
|
||||
): Promise<
|
||||
Result<JavaScriptAnalyzedSource, JavaScriptSourceAnalysisFailure>
|
||||
@@ -203,7 +204,8 @@ export class JavaScriptAnalysisWorker {
|
||||
let offeredResultBudget = 0;
|
||||
const request = javaScriptWorkerRequestSchema.parse({
|
||||
id,
|
||||
node_budget: nodeBudget,
|
||||
node_budget: capacity.nodes,
|
||||
unknown_budget: capacity.unknowns,
|
||||
text_sha256: createHash("sha256").update(file.text.value).digest("hex"),
|
||||
file: metadata,
|
||||
});
|
||||
@@ -344,7 +346,7 @@ export class JavaScriptAnalysisWorker {
|
||||
descriptor,
|
||||
file,
|
||||
deadline.signal,
|
||||
retainGraph,
|
||||
retainGraph ? capacity : null,
|
||||
);
|
||||
module = decoded.module;
|
||||
projection = decoded.projection;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { jsonValueSchema } from "../../domain/jsonValue.js";
|
||||
import { digestSchema } from "../../domain/digests.js";
|
||||
import {
|
||||
JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS,
|
||||
JAVASCRIPT_SEMANTIC_NODE_MAXIMUM,
|
||||
JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM,
|
||||
} from "../../domain/javascript/javascriptAnalysisResourceControls.js";
|
||||
import { javaScriptAnalysisTransferDescriptorSchema } from "./JavaScriptAnalysisTransfer.js";
|
||||
|
||||
@@ -18,7 +18,8 @@ export const JAVASCRIPT_ANALYSIS_WORKER_DEFAULTS = {
|
||||
/** Caller-selected artifact identity, separate from the decoded source-text digest. */
|
||||
export const javaScriptWorkerRequestSchema = z.strictObject({
|
||||
id: z.uuid(),
|
||||
node_budget: z.number().int().min(0).max(JAVASCRIPT_SEMANTIC_NODE_MAXIMUM),
|
||||
unknown_budget: z.number().int().min(0).max(JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM),
|
||||
node_budget: z.number().int().min(0).max(JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM),
|
||||
text_sha256: digestSchema,
|
||||
file: z.strictObject({
|
||||
path: z.string(),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { javaScriptSemanticUnknownOmissionsSchema } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
|
||||
import { z } from "zod";
|
||||
import { digestSchema } from "../../domain/digests.js";
|
||||
import type { JavaScriptStaticAnalysis } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
|
||||
@@ -653,8 +654,10 @@ export const javaScriptWorkerGraphRecordSchema = z.discriminatedUnion("kind", [
|
||||
|
||||
/** Portable per-file header, before any graph binding or immutable proof. */
|
||||
export const javaScriptWorkerProjectionHeaderSchema = z.strictObject({
|
||||
omittedUnknownOccurrences:
|
||||
javaScriptSemanticUnknownOmissionsSchema.exactOptional(),
|
||||
roots: z.array(javaScriptSemanticNodeSchema.shape.node_id),
|
||||
truncated: z.boolean(),
|
||||
nodesTruncated: z.boolean(),
|
||||
});
|
||||
|
||||
/** Module facts are transferred individually to avoid a whole-IR JSON string. */
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { JavaScriptSemanticProjectionCapacity } from "../../domain/javascript/javascriptSemanticFileProjection.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
@@ -26,7 +27,7 @@ import {
|
||||
export interface JavaScriptSourceWorkerAnalyzer {
|
||||
(
|
||||
file: JavaScriptArtifactFile,
|
||||
nodeBudget: number,
|
||||
capacity: JavaScriptSemanticProjectionCapacity,
|
||||
callbacks: {
|
||||
readonly phase: (
|
||||
phase: "parse" | "static" | "semantic" | "projection",
|
||||
@@ -111,7 +112,7 @@ const analyzeJob = async (
|
||||
...request.file,
|
||||
text: { included: true, value: source },
|
||||
},
|
||||
request.node_budget,
|
||||
{ nodes: request.node_budget, unknowns: request.unknown_budget },
|
||||
{
|
||||
phase: async (phase) => send({ kind: "phase", id: request.id, phase }),
|
||||
checkpoint: async (facts) => {
|
||||
|
||||
Reference in New Issue
Block a user