Files
reverse-engineering-anything/src/domain/webCaptureFrameComparison.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

61 lines
2.2 KiB
TypeScript

import type { WebPageInspection } from "./browserObservationSchemas.js";
import { digestCanonicalValue } from "./canonicalDigest.js";
/** Frame-forest invariant. The code is the stable identity. */
class WebCaptureFrameError extends Error {
readonly _tag = "WebCaptureFrameError";
constructor(readonly code: "missing_ready_frame" | "missing_parent_frame") {
super(code);
this.name = "WebCaptureFrameError";
}
}
/** Compare complete frame forests without treating capture-local IDs as identity. */
export const fingerprintWebCaptureFrames = (
frames: WebPageInspection["frames"],
): string | null => {
const byId = new Map(frames.map((frame) => [frame.frame_id, frame]));
if (byId.size !== frames.length) return null;
const remaining = new Map(frames.map((frame) => [frame.frame_id, 0]));
for (const frame of frames) {
if (frame.parent_frame_id === null) continue;
const count = remaining.get(frame.parent_frame_id);
if (count === undefined) return null;
remaining.set(frame.parent_frame_id, count + 1);
}
const ready = frames.filter((frame) => remaining.get(frame.frame_id) === 0);
const children = new Map<string, string[]>();
const roots: string[] = [];
let examined = 0;
for (let index = 0; index < ready.length; index += 1) {
const frame = ready[index];
if (frame === undefined)
throw new WebCaptureFrameError("missing_ready_frame");
const fingerprint = digestCanonicalValue({
url: frame.url,
origin: frame.origin,
children: (children.get(frame.frame_id) ?? []).sort(),
});
examined += 1;
const parentId = frame.parent_frame_id;
if (parentId === null) {
roots.push(fingerprint);
continue;
}
const siblings = children.get(parentId) ?? [];
siblings.push(fingerprint);
children.set(parentId, siblings);
const count = (remaining.get(parentId) ?? 0) - 1;
remaining.set(parentId, count);
if (count === 0) {
const parent = byId.get(parentId);
if (parent === undefined)
throw new WebCaptureFrameError("missing_parent_frame");
ready.push(parent);
}
}
// Cyclic ancestry never reaches the leaf queue and cannot prove equality.
return examined === frames.length ? digestCanonicalValue(roots.sort()) : null;
};