mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
* feat(native): page retained feature traces * fix(snapshot): retain linked source evidence for replay * test(snapshot): verify linked foreign evidence stays scoped * feat(native): page retained procedure references * docs(mcp): clarify imported residual questions * docs(mcp): guide trace reference classification * fix(cli): report result sources when Evidence publication fails A trace keeps linked sources on the result. Use those records for the undelivered source closure as well as the bundle, so a refused output does not report them as not captured. * ci: rerun pull request checks The merge of origin/main did not start the CI workflow for this head. * fix(native): document shared procedure endpoints and the required selector Describe kind shared and items.endpoints instead of inline procedure copies. State that procedure-references requires a positional selector or --procedure. * fix(native): count procedure-reference endpoints before keeping a row Include the shared endpoint table in the page budget, with JSON punctuation, and store a new procedure only when its row is accepted. * fix(native): measure a procedure endpoint once before its comparison key Size the candidate before building one canonical identity, and reuse that key for the selected-procedure and shared-endpoint checks. * fix(native): reject an oversized procedure endpoint before canonicalizing it When the JSON size already exceeds every stored identity, skip the canonical string and keep the row out of the page. * fix(native): describe procedure-reference direction and coverage Avoid execution wording in the tool description, and give the CLI direction option the help text the catalog requires. * fix(native): compare endpoint size with UTF-8 identity length Keep one running maximum of stored identity bytes. A multibyte selected procedure is recognized when its JSON no longer fits the page. * test(cli): count procedure-references as a dedicated command The MCP tool has a CLI command, so it is no longer an MCP-only exception. * test(native): catch the string-length endpoint comparison Use a 200,000-character multibyte selected procedure and a shorter ASCII endpoint so the old JavaScript length check drops the selected row. * ci: rerun after the binary-layout toolchain timeout The required check failed because the toolchain install exceeded 15 minutes. The same job completed on the previous head, and this commit does not change the procedure-reference behavior. * test: stabilize capture decoder exhaustion coverage --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
392 lines
13 KiB
TypeScript
392 lines
13 KiB
TypeScript
import { z } from "zod";
|
|
import { canonicalJson } from "../comparisonSemantics.js";
|
|
import { jsonValueSchema } from "../jsonValue.js";
|
|
import { procedureIdentitySchema } from "../native/procedureInfo.js";
|
|
import {
|
|
referenceEdgeSchema,
|
|
unresolvedCallSchema,
|
|
} from "../native/nativeReference.js";
|
|
import { procedureReferencesSchema } from "../native/procedureReferences.js";
|
|
import { err, ok } from "../result.js";
|
|
import {
|
|
createAnalysisViewResultReader,
|
|
type AnalysisViewParent,
|
|
} from "./analysisViewParent.js";
|
|
import {
|
|
assertOwnedView,
|
|
type AdmittedView,
|
|
type SummaryAnalysisView,
|
|
} from "./analysisViewShape.js";
|
|
import { analysisViewInputError } from "./analysisViewErrors.js";
|
|
import { AnalysisResourceConstraintError } from "../analysisErrorCore.js";
|
|
import type { JsonValue } from "../jsonValue.js";
|
|
import { jsonValueByteLength } from "../jsonValueByteLength.js";
|
|
import {
|
|
boundedViewMessages,
|
|
completeWithinViewCoverage,
|
|
pageViewCoverage,
|
|
} from "./analysisViewCoverage.js";
|
|
|
|
const page = {
|
|
kind: z.literal("native-procedure-references"),
|
|
offset: z.int().nonnegative().default(0),
|
|
limit: z.int().positive().default(64),
|
|
};
|
|
export const procedureReferenceViewRequests = {
|
|
references: z.strictObject({
|
|
...page,
|
|
collection: z.literal("references"),
|
|
source_address: z
|
|
.string()
|
|
.min(1)
|
|
.optional()
|
|
.describe("Match an exact retained source address."),
|
|
target_address: z
|
|
.string()
|
|
.min(1)
|
|
.optional()
|
|
.describe("Match an exact retained target address."),
|
|
}),
|
|
unresolved_calls: z.strictObject({
|
|
...page,
|
|
collection: z.literal("unresolved_calls"),
|
|
}),
|
|
} as const;
|
|
export const nativeProcedureReferencesViewRequestSchema = z.discriminatedUnion(
|
|
"collection",
|
|
[
|
|
procedureReferenceViewRequests.references,
|
|
procedureReferenceViewRequests.unresolved_calls,
|
|
],
|
|
);
|
|
const endpointSchema = z.discriminatedUnion("kind", [
|
|
z.strictObject({ kind: z.literal("null") }),
|
|
z.strictObject({ kind: z.literal("selected-procedure") }),
|
|
z.strictObject({
|
|
kind: z.literal("shared"),
|
|
index: z.int().nonnegative(),
|
|
}),
|
|
]);
|
|
const rowSchema = referenceEdgeSchema.extend({
|
|
source_procedure: endpointSchema,
|
|
target_procedure: endpointSchema,
|
|
source_index: z.int().nonnegative(),
|
|
});
|
|
export const procedureReferenceViewItems = {
|
|
references: z.strictObject({
|
|
collection: z.literal("references"),
|
|
endpoints: z.array(procedureIdentitySchema),
|
|
rows: z.array(rowSchema),
|
|
}),
|
|
unresolved_calls: z.strictObject({
|
|
collection: z.literal("unresolved_calls"),
|
|
rows: z.array(
|
|
z.strictObject({
|
|
source_index: z.int().nonnegative(),
|
|
value: unresolvedCallSchema,
|
|
}),
|
|
),
|
|
}),
|
|
} as const;
|
|
export const procedureReferenceSummarySchema = z.strictObject({
|
|
procedure: procedureIdentitySchema,
|
|
direction: z.enum(["incoming", "outgoing"]),
|
|
reference_kinds_available: z.boolean(),
|
|
reference_count: z.int().nonnegative(),
|
|
unresolved_call_count: z.int().nonnegative(),
|
|
});
|
|
const readSource = createAnalysisViewResultReader(procedureReferencesSchema);
|
|
// The analysis-view projection allowance leaves room for Evidence and MCP
|
|
// encoding expansion. Count JSON without serializing the full page.
|
|
const PAGE_BYTES = 1024 * 1024;
|
|
const DIGEST_BYTES = jsonValueByteLength({ view_digest: "0".repeat(64) }) - 1;
|
|
|
|
/** Page the retained result without re-querying the provider or changing source coverage. */
|
|
export const projectNativeProcedureReferencesView = (
|
|
parent: AnalysisViewParent,
|
|
view: AdmittedView,
|
|
) => {
|
|
if (view.kind !== "summary" && view.kind !== "native-procedure-references")
|
|
return err(
|
|
analysisViewInputError([
|
|
{
|
|
path: ["view"],
|
|
reason: "invalid_value",
|
|
message:
|
|
"Procedure-reference Evidence supports summary and native-procedure-references views.",
|
|
},
|
|
]),
|
|
);
|
|
assertOwnedView<
|
|
| SummaryAnalysisView
|
|
| z.output<typeof nativeProcedureReferencesViewRequestSchema>
|
|
>(view, "summary", "native-procedure-references");
|
|
const parsed = readSource(parent);
|
|
if (!parsed.ok) return parsed;
|
|
const source = parsed.value;
|
|
const summary = {
|
|
procedure: source.procedure,
|
|
direction: source.direction,
|
|
reference_kinds_available: source.reference_kinds_available,
|
|
reference_count: source.references.length,
|
|
unresolved_call_count: source.unresolved_calls.length,
|
|
};
|
|
const shared = {
|
|
parent_evidence_id: parent.evidenceId,
|
|
parent_operation: "procedure_references" as const,
|
|
parent_digest: parent.evidenceId.slice(3),
|
|
artifact: parent.artifact ?? null,
|
|
source_provider: parent.provider ?? null,
|
|
source_analysis_profile_digest: parent.analysisProfile?.digest ?? null,
|
|
source_parameters: jsonValueSchema.parse(parent.parameters ?? null),
|
|
limitations: boundedViewMessages(
|
|
parent.limitations,
|
|
parent.evidenceId,
|
|
"limitations",
|
|
),
|
|
unknowns: [
|
|
"Page coverage describes retained references, not provider discovery completeness or runtime execution.",
|
|
],
|
|
};
|
|
const tooLarge = () =>
|
|
err(
|
|
new AnalysisResourceConstraintError(
|
|
"inspect_analysis_view",
|
|
"transport",
|
|
"The selected procedure-reference view cannot fit the analysis-view byte budget.",
|
|
{ parent_evidence_id: parent.evidenceId, limit_bytes: PAGE_BYTES },
|
|
{
|
|
retryable: false,
|
|
remediationAction:
|
|
"An individual row or the page context exceeds the fixed 1,048,576-byte projection allowance. Use the complete input Evidence or its local bundle, or retrieve or export the retained parent by the reported ID. If a CLI failure includes a partial observation, that observation is the completed parent.",
|
|
},
|
|
),
|
|
);
|
|
if (view.kind === "summary") {
|
|
const result = {
|
|
...shared,
|
|
kind: "summary" as const,
|
|
view,
|
|
summary,
|
|
coverage: completeWithinViewCoverage(1, 1),
|
|
};
|
|
return jsonValueByteLength(result as JsonValue, PAGE_BYTES - DIGEST_BYTES) >
|
|
PAGE_BYTES - DIGEST_BYTES
|
|
? tooLarge()
|
|
: ok(result);
|
|
}
|
|
const common = {
|
|
...shared,
|
|
kind: "native-procedure-references" as const,
|
|
view,
|
|
summary,
|
|
};
|
|
const header = {
|
|
...common,
|
|
items:
|
|
view.collection === "references"
|
|
? { collection: view.collection, endpoints: [], rows: [] }
|
|
: { collection: view.collection, rows: [] },
|
|
byte_limited: false,
|
|
coverage: {
|
|
status: "complete-within-view",
|
|
examined: Number.MAX_SAFE_INTEGER,
|
|
total: Number.MAX_SAFE_INTEGER,
|
|
next_offset: Number.MAX_SAFE_INTEGER,
|
|
exhausted: false,
|
|
},
|
|
};
|
|
let bytes =
|
|
DIGEST_BYTES +
|
|
jsonValueByteLength(header as JsonValue, PAGE_BYTES - DIGEST_BYTES);
|
|
if (bytes > PAGE_BYTES) return tooLarge();
|
|
const referenceRows: z.infer<typeof rowSchema>[] = [];
|
|
const unresolvedRows: {
|
|
source_index: number;
|
|
value: z.infer<typeof unresolvedCallSchema>;
|
|
}[] = [];
|
|
let byteLimited = false;
|
|
const offer = (row: JsonValue, accepted: number, extra = 0) => {
|
|
const size =
|
|
jsonValueByteLength(row, PAGE_BYTES - bytes) +
|
|
(accepted > 0 ? 1 : 0) +
|
|
extra;
|
|
if (bytes + size > PAGE_BYTES) {
|
|
byteLimited = true;
|
|
return false;
|
|
}
|
|
bytes += size;
|
|
return true;
|
|
};
|
|
if (view.collection === "unresolved_calls") {
|
|
const calls = source.unresolved_calls;
|
|
const end = Math.min(calls.length, view.offset + view.limit);
|
|
for (let index = view.offset; index < end; index += 1) {
|
|
const value = calls[index];
|
|
if (value === undefined) break;
|
|
const row = { source_index: index, value };
|
|
if (!offer(row, unresolvedRows.length)) break;
|
|
unresolvedRows.push(row);
|
|
}
|
|
if (byteLimited && unresolvedRows.length === 0) return tooLarge();
|
|
return ok({
|
|
...common,
|
|
view,
|
|
items: { collection: "unresolved_calls" as const, rows: unresolvedRows },
|
|
byte_limited: byteLimited,
|
|
coverage: pageViewCoverage(
|
|
view.offset,
|
|
unresolvedRows.length,
|
|
calls.length,
|
|
),
|
|
});
|
|
}
|
|
const selected = canonicalJson(source.procedure);
|
|
const endpoints: z.infer<typeof procedureIdentitySchema>[] = [];
|
|
const endpointIndexes = new Map<string, number>();
|
|
let longestIdentityBytes = Buffer.byteLength(selected);
|
|
const planEndpoints = (
|
|
sourceProcedure: z.infer<typeof procedureIdentitySchema> | null,
|
|
targetProcedure: z.infer<typeof procedureIdentitySchema> | null,
|
|
) => {
|
|
const pending: z.infer<typeof procedureIdentitySchema>[] = [];
|
|
const pendingKeys: string[] = [];
|
|
let extra = 0;
|
|
let overflow = false;
|
|
const resolve = (value: z.infer<typeof procedureIdentitySchema> | null) => {
|
|
if (value === null) return { kind: "null" as const };
|
|
const comma = endpoints.length + pending.length > 0 ? 1 : 0;
|
|
const measured = jsonValueByteLength(
|
|
value as JsonValue,
|
|
Math.max(0, PAGE_BYTES - bytes - extra),
|
|
);
|
|
const size = measured + comma;
|
|
// `measured` is a UTF-8 lower bound. It can skip canonicalization only
|
|
// when it already exceeds every stored identity's UTF-8 length.
|
|
if (
|
|
bytes + extra + size > PAGE_BYTES &&
|
|
measured > longestIdentityBytes
|
|
) {
|
|
overflow = true;
|
|
return undefined;
|
|
}
|
|
const key = canonicalJson(value);
|
|
if (key === selected) return { kind: "selected-procedure" as const };
|
|
const existing = endpointIndexes.get(key);
|
|
if (existing !== undefined)
|
|
return { kind: "shared" as const, index: existing };
|
|
const pendingIndex = pendingKeys.indexOf(key);
|
|
if (pendingIndex !== -1)
|
|
return {
|
|
kind: "shared" as const,
|
|
index: endpoints.length + pendingIndex,
|
|
};
|
|
if (bytes + extra + size > PAGE_BYTES) {
|
|
overflow = true;
|
|
return undefined;
|
|
}
|
|
extra += size;
|
|
pending.push(value);
|
|
pendingKeys.push(key);
|
|
const keyBytes = Buffer.byteLength(key);
|
|
if (keyBytes > longestIdentityBytes) longestIdentityBytes = keyBytes;
|
|
return {
|
|
kind: "shared" as const,
|
|
index: endpoints.length + pending.length - 1,
|
|
};
|
|
};
|
|
const resolvedSource = resolve(sourceProcedure);
|
|
const resolvedTarget = resolve(targetProcedure);
|
|
if (
|
|
overflow ||
|
|
resolvedSource === undefined ||
|
|
resolvedTarget === undefined
|
|
)
|
|
return { overflow: true as const };
|
|
return {
|
|
overflow: false as const,
|
|
resolvedSource,
|
|
resolvedTarget,
|
|
pending,
|
|
pendingKeys,
|
|
extra,
|
|
};
|
|
};
|
|
const filtered =
|
|
view.source_address !== undefined || view.target_address !== undefined;
|
|
const references = source.references;
|
|
const visit = function* () {
|
|
if (!filtered) {
|
|
const end = Math.min(references.length, view.offset + view.limit);
|
|
for (let index = view.offset; index < end; index += 1) {
|
|
const reference = references[index];
|
|
if (reference !== undefined) yield [index, reference] as const;
|
|
}
|
|
return;
|
|
}
|
|
let seen = 0;
|
|
for (const [index, reference] of references.entries()) {
|
|
if (
|
|
(view.source_address !== undefined &&
|
|
reference.source_address !== view.source_address) ||
|
|
(view.target_address !== undefined &&
|
|
reference.target_address !== view.target_address)
|
|
)
|
|
continue;
|
|
if (seen >= view.offset && seen < view.offset + view.limit)
|
|
yield [index, reference] as const;
|
|
seen += 1;
|
|
}
|
|
};
|
|
let filteredTotal = 0;
|
|
if (filtered) {
|
|
for (const reference of references) {
|
|
if (
|
|
(view.source_address === undefined ||
|
|
reference.source_address === view.source_address) &&
|
|
(view.target_address === undefined ||
|
|
reference.target_address === view.target_address)
|
|
)
|
|
filteredTotal += 1;
|
|
}
|
|
}
|
|
for (const [index, reference] of visit()) {
|
|
const planned = planEndpoints(
|
|
reference.source_procedure,
|
|
reference.target_procedure,
|
|
);
|
|
if (planned.overflow) {
|
|
byteLimited = true;
|
|
break;
|
|
}
|
|
const row = {
|
|
...reference,
|
|
source_index: index,
|
|
source_procedure: planned.resolvedSource,
|
|
target_procedure: planned.resolvedTarget,
|
|
};
|
|
if (!offer(row as JsonValue, referenceRows.length, planned.extra)) break;
|
|
for (const [offset, value] of planned.pending.entries()) {
|
|
endpointIndexes.set(planned.pendingKeys[offset]!, endpoints.length);
|
|
endpoints.push(value);
|
|
}
|
|
referenceRows.push(row);
|
|
}
|
|
if (byteLimited && referenceRows.length === 0) return tooLarge();
|
|
return ok({
|
|
...common,
|
|
view,
|
|
items: {
|
|
collection: "references" as const,
|
|
endpoints,
|
|
rows: referenceRows,
|
|
},
|
|
byte_limited: byteLimited,
|
|
coverage: pageViewCoverage(
|
|
view.offset,
|
|
referenceRows.length,
|
|
filtered ? filteredTotal : references.length,
|
|
),
|
|
});
|
|
};
|