Files
reverse-engineering-anything/src/domain/analysisView/browserScenarioNetworkView.ts
T
fc43e285ea feat(browser): retain compact scenario summaries and selected views (#7120)
Retain complete browser scenario Evidence while delivering compact summaries and selected step, event, transaction and text views through shared CLI/MCP workflows. Preserve initial CLI failures across cleanup retries and validate with real Chromium/TodoMVC.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 19:03:06 +08:00

194 lines
6.2 KiB
TypeScript

import { z } from "zod";
import type { RetainedBrowserScenarioCapture } from "../browserScenarioCapture.js";
import { isImmutableJsonSnapshot } from "../immutableJson.js";
import { compareAsciiStrings } from "../canonicalOrdering.js";
const count = z.int().nonnegative();
const statusClass = z.enum([
"1xx",
"2xx",
"3xx",
"4xx",
"5xx",
"failed",
"unknown",
]);
/** Counts concern retained transaction identities, including those without a request event. */
export const browserScenarioNetworkSummarySchema = z.strictObject({
selected: z.boolean().nullable(),
transactions: count.nullable(),
without_request: count,
ambiguous_transactions: count,
failed_transactions: count,
by_origin: z.array(
z.strictObject({ origin: z.string().nullable(), transactions: count }),
),
by_resource_type: z.array(
z.strictObject({
resource_type: z.string().nullable(),
transactions: count,
}),
),
by_status_class: z.array(
z.strictObject({ status_class: statusClass, transactions: count }),
),
});
interface TransactionGroup {
readonly transaction_id: string;
readonly source_item_indices: readonly number[];
readonly origin: string | null;
readonly resource_type: string | null;
readonly status_class: z.infer<typeof statusClass>;
readonly has_request: boolean;
readonly failed: boolean;
readonly ambiguous: boolean;
}
interface TransactionFacts {
indices: number[];
origins: Set<string | null>;
resourceTypes: Set<string>;
statuses: Set<number>;
requests: number;
failed: boolean;
}
/** Sealed historical captures own this metadata-only index until their retained lifetime ends. */
const transactionsByCapture = new WeakMap<
object,
readonly TransactionGroup[]
>();
/** Keep all observed alternatives; one missing event never erases its transaction. */
export const browserScenarioTransactionGroups = (
capture: RetainedBrowserScenarioCapture,
): readonly TransactionGroup[] => {
const reusable = isImmutableJsonSnapshot(capture);
const known = reusable ? transactionsByCapture.get(capture) : undefined;
if (known !== undefined) return known;
const groups = new Map<string, TransactionFacts>();
for (const [index, event] of capture.events.items.entries()) {
if (!("transaction_id" in event)) continue;
let group = groups.get(event.transaction_id);
if (group === undefined) {
group = {
indices: [],
origins: new Set(),
resourceTypes: new Set(),
statuses: new Set(),
requests: 0,
failed: false,
};
groups.set(event.transaction_id, group);
}
group.indices.push(index);
if (event.kind === "network-content") continue;
group.origins.add(event.url.origin);
group.resourceTypes.add(event.resource_type);
if (event.kind === "request") group.requests += 1;
if (event.kind === "response") group.statuses.add(event.status);
if (event.kind === "request-failed") group.failed = true;
}
const result = [...groups].map(
([transaction_id, group]): TransactionGroup => {
const status = group.statuses.values().next().value;
return Object.freeze({
transaction_id,
source_item_indices: Object.freeze(group.indices),
origin:
group.origins.size === 1
? (group.origins.values().next().value ?? null)
: null,
resource_type:
group.resourceTypes.size === 1
? (group.resourceTypes.values().next().value ?? null)
: null,
status_class:
group.statuses.size === 1 && status !== undefined
? (`${String(Math.floor(status / 100))}xx` as z.infer<
typeof statusClass
>)
: group.statuses.size === 0 && group.failed
? "failed"
: "unknown",
has_request: group.requests > 0,
failed: group.failed,
ambiguous:
group.requests > 1 ||
group.statuses.size > 1 ||
group.origins.size > 1 ||
group.resourceTypes.size > 1,
});
},
);
if (reusable) transactionsByCapture.set(capture, result);
return result;
};
const increment = <Key>(values: Map<Key, number>, key: Key): void => {
values.set(key, (values.get(key) ?? 0) + 1);
};
const sorted = <Key extends string | null>(
values: ReadonlyMap<Key, number>,
): [Key, number][] =>
[...values].sort(([left], [right]) =>
left === right
? 0
: left === null
? -1
: right === null
? 1
: compareAsciiStrings(left, right),
);
/** Null selection/counts preserve legacy unknowns and unrequested network observation. */
export const summarizeBrowserScenarioNetwork = (
capture: RetainedBrowserScenarioCapture,
): z.infer<typeof browserScenarioNetworkSummarySchema> => {
const groups = browserScenarioTransactionGroups(capture);
const contentSelected = Object.values(capture.scenario.network_content).some(
Boolean,
);
const selected =
contentSelected ||
capture.scenario.event_families?.includes("network") === true
? true
: capture.scenario.event_families === undefined
? null
: false;
const origins = new Map<string | null, number>();
const resources = new Map<string | null, number>();
const statuses = new Map<z.infer<typeof statusClass>, number>();
let withoutRequest = 0;
let ambiguous = 0;
let failed = 0;
for (const group of groups) {
if (!group.has_request) withoutRequest += 1;
if (group.ambiguous) ambiguous += 1;
if (group.failed) failed += 1;
increment(origins, group.origin);
increment(resources, group.resource_type);
increment(statuses, group.status_class);
}
return {
selected,
transactions: selected === true || groups.length > 0 ? groups.length : null,
without_request: withoutRequest,
ambiguous_transactions: ambiguous,
failed_transactions: failed,
by_origin: sorted(origins).map(([origin, transactions]) => ({
origin,
transactions,
})),
by_resource_type: sorted(resources).map(
([resource_type, transactions]) => ({ resource_type, transactions }),
),
by_status_class: sorted(statuses).map(([status_class, transactions]) => ({
status_class,
transactions,
})),
};
};