Files
rea/src/domain/browserScenarioDiffValues.ts
T
69f99c5198 feat(environment): time browser scenario records so scenario environments learn a write clock (#6967)
* feat(browser): record when scenario requests were observed and how long they took

Request events carry observed_at, the wall-clock time the provider observed
the request start, and finished or failed events carry the browser's
elapsed_ms when it reports one. Both are optional, so earlier captures still
parse, and scenario comparisons leave them out as a committed built-in rule.
The real-Chrome network lane checks every request and finished response is
timed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(environment): time browser scenario records so their state learns a write clock

Scenario records take startedDateTime from when each request was observed and
time from the browser's duration, as HAR entries are timed. A timed scenario
of the book session learns the same clock and serves the same writes as its
HAR; an untimed one keeps record order and learns no clock, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(environment): document scenario request times and timed scenario records

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(environment): start scenario records at the browser's own request start

The provider observes a request up to tens of milliseconds after the browser
starts it while it reads other responses, so a record starting then and
ending after the browser's duration mixed two clocks. Finished and failed
events now carry the browser's started_at, records prefer it and fall back to
observed_at, and comparisons drop it too. Tests cover the fallback for a
request that never ended or ended twice, a partly untimed capture keeping
record order, comparing an untimed capture with a timed one, and the
real-Chrome lane checks every finished request carries the browser's start
and duration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(environment): document the browser start time, its fallback lag and older readers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(environment): record the scenario timing verification

Gates, the real-Chrome lane, prompt-recorded and re-recorded published
scenario captures built with and without their times, the stale published
environments left unrepublished, and the review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(browser): compare timed completion events in the request-time comparison test

The comparison test's captures had no finished or failed events, so dropping
started_at and elapsed_ms from them went untested. The captures now end one
request and fail the other, the test asserts both carry times, compares them
shifted by seven seconds and against an untimed capture, and a control shows
a changed failure on a timed completion is still reported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-24 22:48:50 +08:00

194 lines
6.1 KiB
TypeScript

import { z } from "zod";
import { retainedBrowserScenarioCaptureSchema } from "./browserScenarioCapture.js";
/** Scenario artifact families compared after step alignment. */
export const browserScenarioArtifactKindSchema = z.enum([
"action_state",
"screenshot",
"dom",
"accessibility",
"url",
"history",
"storage",
"events",
]);
/** Scenario artifact family. */
export type BrowserScenarioArtifactKind = z.infer<
typeof browserScenarioArtifactKindSchema
>;
const normalizableArtifactKindSchema = z.enum([
"action_state",
"dom",
"accessibility",
"url",
"history",
"storage",
"events",
]);
const normalizationRuleSchema = z.strictObject({
rule_id: z
.string()
.trim()
.min(1)
.describe("Stable rule identity; rules execute in rule_id order."),
artifacts: z
.array(normalizableArtifactKindSchema)
.min(1)
.describe("Artifact families whose durable string fields use this rule."),
match: z
.string()
.min(1)
.describe(
"Exact public literal to replace; regular expressions are not used.",
),
replacement: z
.string()
.describe("Recorded replacement text used in normalized comparison."),
});
const normalizationInputSchema = z
.strictObject({
rules: z.array(normalizationRuleSchema).default([]),
})
.superRefine((normalization, context) => {
const ruleIds = new Set<string>();
for (const [index, rule] of normalization.rules.entries()) {
if (ruleIds.has(rule.rule_id))
context.addIssue({
code: "custom",
path: ["rules", index, "rule_id"],
message: "Normalization rule IDs must be unique",
});
ruleIds.add(rule.rule_id);
if (new Set(rule.artifacts).size !== rule.artifacts.length)
context.addIssue({
code: "custom",
path: ["rules", index, "artifacts"],
message: "Normalization rule artifact kinds must be unique",
});
}
});
/** Input for step-aligned comparison of two browser scenario captures. */
export const compareBrowserScenariosInputSchema = z.strictObject({
before_scenario: retainedBrowserScenarioCaptureSchema.describe(
"Earlier complete normalized_result from capture_browser_scenario.",
),
after_scenario: retainedBrowserScenarioCaptureSchema.describe(
"Later complete normalized_result from capture_browser_scenario.",
),
normalization: normalizationInputSchema
.default({ rules: [] })
.describe("Exact-literal rules committed in the comparison result."),
});
/** Parsed browser scenario comparison input. */
export type CompareBrowserScenariosInput = z.infer<
typeof compareBrowserScenariosInputSchema
>;
const evidenceStateSchema = z.enum([
"captured",
"not_requested",
"missing",
"truncated",
]);
const artifactDiffSchema = z.strictObject({
artifact: browserScenarioArtifactKindSchema,
status: z.enum(["changed", "unknown"]),
before_state: evidenceStateSchema,
after_state: evidenceStateSchema,
before_sha256: z
.string()
.regex(/^[a-f0-9]{64}$/u)
.nullable(),
after_sha256: z
.string()
.regex(/^[a-f0-9]{64}$/u)
.nullable(),
reason: z.string().min(1).nullable(),
});
const alignmentFailureSchema = z.strictObject({
code: z.enum([
"duplicate_before_step_id",
"duplicate_after_step_id",
"missing_before_step",
"missing_after_step",
"action_mismatch",
"capture_context_mismatch",
]),
step_id: z.string().min(1).nullable(),
reason: z.string().min(1),
});
export const BROWSER_SCENARIO_BUILT_IN_NORMALIZATION_RULES = [
"ignore_step_elapsed_ms",
"ignore_event_sequence",
"ignore_event_step_index",
"compare_screenshot_content_digest",
"compare_normalized_text_content",
"compare_storage_entries_as_unordered_collection",
"ignore_capture_local_network_transaction_ids",
"ignore_network_request_timing",
"attribute_network_transactions_to_request_step",
"compare_network_transactions_as_unordered_collection",
"preserve_redirect_transaction_ancestry",
] as const;
const builtInNormalizationSchema = z.enum(
BROWSER_SCENARIO_BUILT_IN_NORMALIZATION_RULES,
);
/** Step alignment, recorded normalization, and inline artifact differences. */
export const browserScenarioDiffSchema = z.strictObject({
comparison_kind: z.literal("browser_scenario"),
overall_status: z.enum(["changed", "unchanged", "unknown"]),
normalization: z.strictObject({
built_in_rules: z
.array(builtInNormalizationSchema)
.length(BROWSER_SCENARIO_BUILT_IN_NORMALIZATION_RULES.length),
rules: z.array(normalizationRuleSchema),
sha256: z.string().regex(/^[a-f0-9]{64}$/u),
}),
alignment: z.strictObject({
status: z.enum(["aligned", "partial", "failed"]),
aligned_steps: z.int().min(0),
before_only: z.array(z.string().min(1)),
after_only: z.array(z.string().min(1)),
failures: z.array(alignmentFailureSchema),
}),
steps: z.array(
z.strictObject({
step_id: z.string().min(1),
before_step_index: z.int().min(0),
after_step_index: z.int().min(0),
before_action: z.string().min(1),
after_action: z.string().min(1),
status: z.enum(["changed", "unchanged", "unknown"]),
artifact_diffs: z.array(artifactDiffSchema),
}),
),
artifact_diffs: z.strictObject({
total: z.int().min(0),
}),
limitations: z.array(z.string().min(1)),
});
/** Browser scenario comparison result. */
export type BrowserScenarioDiff = z.infer<typeof browserScenarioDiffSchema>;
/** Reportable changed or unknown scenario artifact. */
export type BrowserScenarioArtifactDiff =
BrowserScenarioDiff["steps"][number]["artifact_diffs"][number];
/** One explicit reason scenario step alignment was not exact. */
export type BrowserScenarioAlignmentFailure =
BrowserScenarioDiff["alignment"]["failures"][number];
/** Comparable capture state. */
export type BrowserScenarioEvidenceState =
BrowserScenarioArtifactDiff["before_state"];
/** Scenario normalization policy after boundary parsing. */
export type BrowserScenarioNormalization =
CompareBrowserScenariosInput["normalization"];
/** One exact literal scenario normalization rule. */
export type BrowserScenarioNormalizationRule =
BrowserScenarioNormalization["rules"][number];