mirror of
https://github.com/morluto/rea.git
synced 2026-11-09 16:26:36 +00:00
* fix(cli): keep captured source Evidence when one-shot delivery never commits A completed observation that was cancelled at final close, failed cleanup, or never reached bundle commit returned its evidence links but dropped the source records the invocation had already captured. The caller could neither read those records nor tell whether they were missing, captured or written. Report the closure at publishCliAnalysisEvidence, the shared boundary every --evidence_output command uses. details.source_closure carries authentic captured records within 64 KiB and explicit omissions: not_captured for a link this invocation holds no record for, and not_inlined for a captured record outside this response. A committed bundle keeps its receipt instead, nothing is written after cancellation, and successful delivery is unchanged. Fixes #8589 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cli): keep the typed failure when collecting source closure cannot finish A captured source with a data-sized evidence_links array made pending.push(...links) exceed V8's call-argument limit. On final-close cancellation or a refused publication that RangeError replaced the original typed failure, which is the one outcome this delivery step exists to preserve. Walk the closure through a Set, which is a deduplicating breadth-first queue, so no link becomes a call argument and each link costs one lookup however often it repeats. undeliveredSourceClosure now owns authentication and never throws: if examining the closure fails, it reports collection_failure with the bounded error type and an unknown closure state, and the original failure is still returned. Fault the final-close tests on the scope the command gave to matching, after matching completed, and assert that close was reached, so a moved close fails instead of passing on an earlier one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
144 lines
5.5 KiB
TypeScript
144 lines
5.5 KiB
TypeScript
import { readArtifactInspectionSources } from "../domain/artifactInspection.js";
|
|
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
|
import { tryParseEvidence, type Evidence } from "../domain/evidence.js";
|
|
import { jsonValueByteLength } from "../domain/jsonValueByteLength.js";
|
|
import { err, ok, type Result } from "../domain/result.js";
|
|
|
|
/** Resolve the embedded publication closure without reinterpreting it in delivery adapters. */
|
|
export const analysisEvidenceSources = (
|
|
complete: Evidence,
|
|
): Result<Evidence[], AnalysisError> => {
|
|
const embedded =
|
|
complete.operation === "inspect_artifact"
|
|
? readArtifactInspectionSources(complete.normalized_result)
|
|
: ok([]);
|
|
if (!embedded.ok)
|
|
return err(embedded.error.retainPartialObservation(complete));
|
|
return ok([...embedded.value, complete]);
|
|
};
|
|
|
|
/** Matches the diagnostics allowance retained for other finalized one-shot work. */
|
|
const SOURCE_CLOSURE_INLINE_BYTES = 64 * 1024;
|
|
/** Identities beyond this stay counted; direct links remain on the observation. */
|
|
const SOURCE_CLOSURE_LISTED_OMISSIONS = 64;
|
|
const SOURCE_CLOSURE_RECOVERY =
|
|
"No bundle was written and this invocation retains nothing after exit. Keep any records shown here, or run the operation again after following remediation; that is a new observation and may differ.";
|
|
|
|
/** What a failed one-shot delivery holds for one record its observation cites. */
|
|
export type SourceClosureOmission = {
|
|
readonly evidence_id: string;
|
|
/**
|
|
* `not_captured`: this invocation holds no such record, so none can be shown.
|
|
* `not_inlined`: captured, but not part of this bounded failure response.
|
|
*/
|
|
readonly reason: "not_captured" | "not_inlined";
|
|
};
|
|
|
|
/** Bounded failure handoff for a completed observation whose bundle never committed. */
|
|
export type UndeliveredSourceClosure = {
|
|
readonly publication: "not_published";
|
|
/** Every cited record is in `records` or embedded in the observation itself. */
|
|
readonly complete: boolean;
|
|
readonly records: Evidence[];
|
|
readonly omitted_count: number;
|
|
readonly omitted: SourceClosureOmission[];
|
|
readonly recovery: string;
|
|
};
|
|
|
|
/** The closure state is unknown because examining it failed. */
|
|
export type UnavailableSourceClosure = {
|
|
readonly publication: "not_published";
|
|
readonly complete: false;
|
|
readonly collection_failure: string;
|
|
readonly recovery: string;
|
|
};
|
|
|
|
const collectSourceClosure = (
|
|
observation: Evidence,
|
|
captured: readonly Evidence[],
|
|
options: { readonly inlineRecords: boolean },
|
|
): UndeliveredSourceClosure | undefined => {
|
|
const inspection = analysisEvidenceSources(observation);
|
|
const embedded = new Map(
|
|
(inspection.ok ? inspection.value : []).map((record) => [
|
|
record.evidence_id,
|
|
record,
|
|
]),
|
|
);
|
|
const held = new Map(captured.map((record) => [record.evidence_id, record]));
|
|
// Set iteration reaches entries added while it runs, which makes this a
|
|
// deduplicating breadth-first queue: directly cited records spend the
|
|
// allowance first, and each link costs one lookup however often it repeats.
|
|
const closure = new Set([observation.evidence_id]);
|
|
for (const evidenceId of observation.evidence_links) closure.add(evidenceId);
|
|
for (const evidenceId of held.keys()) closure.add(evidenceId);
|
|
const records: Evidence[] = [];
|
|
const omitted: SourceClosureOmission[] = [];
|
|
let omittedCount = 0;
|
|
let inlineBytes = options.inlineRecords ? SOURCE_CLOSURE_INLINE_BYTES : 0;
|
|
const omit = (
|
|
evidenceId: string,
|
|
reason: SourceClosureOmission["reason"],
|
|
) => {
|
|
omittedCount += 1;
|
|
if (omitted.length < SOURCE_CLOSURE_LISTED_OMISSIONS)
|
|
omitted.push({ evidence_id: evidenceId, reason });
|
|
};
|
|
for (const evidenceId of closure) {
|
|
if (evidenceId === observation.evidence_id) continue;
|
|
const inObservation = embedded.get(evidenceId);
|
|
const record = inObservation ?? held.get(evidenceId);
|
|
if (record === undefined) {
|
|
omit(evidenceId, "not_captured");
|
|
continue;
|
|
}
|
|
for (const link of record.evidence_links) closure.add(link);
|
|
if (inObservation !== undefined) continue;
|
|
const bytes = jsonValueByteLength(record, inlineBytes);
|
|
if (bytes > inlineBytes) {
|
|
omit(evidenceId, "not_inlined");
|
|
continue;
|
|
}
|
|
records.push(record);
|
|
inlineBytes -= bytes;
|
|
}
|
|
if (closure.size === 1) return undefined;
|
|
return {
|
|
publication: "not_published",
|
|
complete: omittedCount === 0,
|
|
records,
|
|
omitted_count: omittedCount,
|
|
omitted,
|
|
recovery: SOURCE_CLOSURE_RECOVERY,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Describe the source closure of a completed observation after delivery failed
|
|
* before commit. Only already captured records are reported, so an unresolved
|
|
* link stays an explicit omission and its own ancestors remain unknown.
|
|
*
|
|
* This annotates a failure that must reach the caller, so it never throws. If
|
|
* examining the closure fails, the state is reported as unknown, naming only
|
|
* the bounded error type rather than a message that could quote analyzed data.
|
|
*/
|
|
export const undeliveredSourceClosure = (
|
|
partialObservation: unknown,
|
|
captured: readonly Evidence[],
|
|
options: { readonly inlineRecords: boolean },
|
|
): UndeliveredSourceClosure | UnavailableSourceClosure | undefined => {
|
|
try {
|
|
const observation = tryParseEvidence(partialObservation);
|
|
return observation === undefined
|
|
? undefined
|
|
: collectSourceClosure(observation, captured, options);
|
|
} catch (cause: unknown) {
|
|
return {
|
|
publication: "not_published",
|
|
complete: false,
|
|
collection_failure: cause instanceof Error ? cause.name : "unknown",
|
|
recovery: SOURCE_CLOSURE_RECOVERY,
|
|
};
|
|
}
|
|
};
|