Files
reverse-engineering-anything/src/domain/browserScenarioDiffEvidence.ts
T
morlutoandCursor Agent b663c79552 fix(domain): replace bare parser errors with typed codes (#8057)
* fix(domain): replace bare parser errors with typed codes

OpenStep syntax, canonical JSON digests, managed-graph source checks,
frame fingerprints, and reference-flow invariants threw Error and
published the sentence. Each refusal now carries a code. Tests assert
that code, including OpenStep position, instead of the previous prose.

* test(domain): pin OpenStep refusal codes and positions

The syntax cases accepted any code and any offset. Each container
refusal now expects its code and source position, including the deep
suffix that changes (x from unterminated_array to expected_separator.
Scalar failures stay identical to the pinned decoder.

* test(filesystem): assert OpenStep keyed-archive codes

The deep-suffix keyed archive cases compared ArtifactReaderFailure
message text with the pinned decoder sentence. They now require
reason format and the OpenStep code and source position on the cause.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-10-30 05:23:10 +08:00

233 lines
7.9 KiB
TypeScript

import type {
BrowserScenarioCompleteness,
BrowserScenarioCompletenessSection,
BrowserScenarioStep,
BrowserStepArtifacts,
} from "./browserScenarioCaptureValues.js";
import type { RetainedBrowserScenarioCapture } from "./browserScenarioCapture.js";
import {
fingerprintScenarioNetworkTransactions,
ScenarioNetworkFingerprintError,
scenarioNetworkTransactions,
scenarioNetworkCoverageReason,
} from "./browserScenarioNetworkComparison.js";
import type {
BrowserScenarioArtifactKind,
BrowserScenarioEvidenceState,
BrowserScenarioNormalizationRule,
} from "./browserScenarioDiffValues.js";
/** Capture-state input consumed by browser scenario artifact comparison. */
export type CaptureState<Value> =
| { readonly state: "captured"; readonly value: Value }
| { readonly state: "not_requested" }
| { readonly state: "missing"; readonly reason: string }
| {
readonly state: "truncated";
readonly observed: number;
readonly retained: number;
readonly reason: string;
};
const includesCompletenessSection = (
completeness: BrowserScenarioCompleteness,
collection: "missing_sections" | "truncated_sections",
section: BrowserScenarioCompletenessSection,
): boolean => {
const sections: readonly BrowserScenarioCompletenessSection[] =
completeness[collection];
return sections.includes(section);
};
/** Project one captured artifact through its step-level completeness ledger. */
export const artifactEvidenceState = <Value>(
artifact: BrowserScenarioArtifactKind,
state: CaptureState<Value>,
step: BrowserScenarioStep,
): BrowserScenarioEvidenceState => {
if (state.state !== "captured") return state.state;
const section = artifact === "action_state" ? "action" : artifact;
if (
includesCompletenessSection(
step.completeness,
"truncated_sections",
section,
)
)
return "truncated";
return includesCompletenessSection(
step.completeness,
"missing_sections",
section,
)
? "missing"
: "captured";
};
const EVENT_SECTIONS = [
"events",
"frames",
"workers",
"popups",
"websockets",
] as const;
/** Project event evidence through capture- and step-level completeness. */
export const eventEvidenceState = (
capture: RetainedBrowserScenarioCapture,
step: BrowserScenarioStep,
): BrowserScenarioEvidenceState => {
const listed = (collection: "missing_sections" | "truncated_sections") =>
EVENT_SECTIONS.some(
(section) =>
includesCompletenessSection(
capture.completeness,
collection,
section,
) ||
includesCompletenessSection(step.completeness, collection, section),
);
if (capture.events.dropped > 0 || listed("truncated_sections"))
return "truncated";
return listed("missing_sections") ? "missing" : "captured";
};
/** Select stable action fields for normalized comparison. */
export const actionState = (step: BrowserScenarioStep) => ({
action: step.action,
status: step.status,
before_url: step.before_url,
after_url: step.after_url,
error: step.error,
});
type StorageSnapshot = Extract<
BrowserStepArtifacts["storage"],
{ state: "captured" }
>["value"];
/** Encoding provenance is not itself a target-storage difference. */
export const storageProjection = ({
cookies,
local_storage,
session_storage,
}: StorageSnapshot) => ({
cookies,
local_storage,
session_storage,
});
/** Legacy UTF-8 fingerprints have irreversibly lost unpaired code units. */
export const storageFingerprintCoverageReason = (
...states: readonly BrowserStepArtifacts["storage"][]
): string | undefined =>
states.some(
(state) =>
state.state === "captured" &&
state.value.fingerprint_version === undefined &&
[
state.value.cookies,
state.value.local_storage,
state.value.session_storage,
].some((entries) =>
entries.some(({ value_state }) => value_state === "hashed"),
),
)
? "Legacy storage fingerprints used lossy UTF-8; recapture both sides with code-unit-preserving storage fingerprints before comparing values."
: undefined;
/** One step's ordered events and a single indexed network graph. */
export interface ScenarioStepEvents {
readonly events: readonly unknown[];
readonly network_transactions: readonly string[];
}
/** Build one graph and fingerprint index per capture, then select in linear total work. */
export const prepareScenarioEvents = (
capture: RetainedBrowserScenarioCapture,
rules: readonly BrowserScenarioNormalizationRule[],
): {
readonly coverageReason: string | undefined;
readonly forStep: (stepIndex: number) => ScenarioStepEvents;
} => {
const eventsByStep = new Map<number, unknown[]>();
for (const { sequence: _sequence, step_index, ...event } of capture.events
.items) {
if ("transaction_id" in event) continue;
const events = eventsByStep.get(step_index) ?? [];
events.push(event);
eventsByStep.set(step_index, events);
}
const nodes = scenarioNetworkTransactions(capture);
const fingerprints = fingerprintScenarioNetworkTransactions(nodes, rules);
const networkByStep = new Map<number, string[]>();
for (const [index, node] of nodes.entries()) {
const step = networkByStep.get(node.step_index) ?? [];
const fingerprint = fingerprints[index];
if (fingerprint === undefined) throw new ScenarioNetworkFingerprintError();
step.push(fingerprint);
networkByStep.set(node.step_index, step);
}
for (const fingerprintsForStep of networkByStep.values())
fingerprintsForStep.sort();
return {
coverageReason: scenarioNetworkCoverageReason(capture, nodes),
forStep: (stepIndex) => ({
events: eventsByStep.get(stepIndex) ?? [],
network_transactions: networkByStep.get(stepIndex) ?? [],
}),
};
};
/** Different content selection is missing comparability, not an observed website change. */
export const networkContentCoverageReason = (
before: RetainedBrowserScenarioCapture,
after: RetainedBrowserScenarioCapture,
): string | undefined => {
const left = before.scenario.network_content;
const right = after.scenario.network_content;
return left.request_body !== right.request_body ||
left.response_body !== right.response_body ||
left.header_values !== right.header_values
? "Network content selections differ; capture both sides with matching content selection before comparing network evidence."
: undefined;
};
/** Different event-family selection is missing comparability, not an observed website change. */
export const eventFamilyCoverageReason = (
before: RetainedBrowserScenarioCapture,
after: RetainedBrowserScenarioCapture,
): string | undefined => {
const left = before.scenario.event_families;
const right = after.scenario.event_families;
if (left === undefined || right === undefined)
return "Event family selection was not recorded in at least one retained capture; event evidence remains unknown. Recapture both sides with explicit matching selections.";
const leftFamilies = new Set(left);
const rightFamilies = new Set(right);
return leftFamilies.size === rightFamilies.size &&
[...leftFamilies].every((family) => rightFamilies.has(family))
? undefined
: `Event family selections differ (${familyList(leftFamilies)} before, ${familyList(rightFamilies)} after); capture both sides with matching event families before comparing event evidence.`;
};
const familyList = (families: ReadonlySet<string>): string =>
families.size === 0 ? "none" : [...families].sort().join(", ");
/** Wrap one value as captured comparison evidence. */
export const captured = <Value>(value: Value): CaptureState<Value> => ({
state: "captured",
value,
});
/** Retain stable screenshot identity without embedding image bytes. */
export const screenshotProjection = (
screenshot: Extract<
BrowserStepArtifacts["screenshot"],
{ state: "captured" }
>["value"],
) => ({
sha256: screenshot.sha256,
bytes: screenshot.bytes,
media_type: screenshot.media_type,
});