/** * Envelope normalization + structural diff vs a golden (T11889-B). * * The self-improvement loop replays a scenario (see {@link "./replay.js"}) and * diffs each captured envelope against the golden expected envelope. This module: * * 1. {@link normalizeEnvelope} — strips the VOLATILE `meta` fields so only * deterministic structure is compared. Two classes are stripped: * timing/trace fields (`timestamp`, `requestId`, `duration_ms` / * `durationMs`) AND the per-run session-lineage UUIDs (`sessionId`, * `originSessionId`, `executionSessionId`). The latter are non-deterministic: * the live `Dispatcher` defaults `executionSessionId`/`originSessionId` to a * fresh `randomUUID()` per request * (`ensureRequestSessionLineage`), then stamps all three onto `response.meta` * — so a real envelope carries UUIDs the hand-authored golden cannot know. * Stripping them prevents a permanent phantom DHQ that self-fires on every * clean run. (The runtime field is `meta` with snake_case `duration_ms`; the * spec wording uses `_meta`/`durationMs`, so both spellings and both * container keys are stripped defensively.) * 2. {@link diffEnvelopes} — structurally compares the normalized envelope set * against the golden, producing `{ regressions: DiffEntry[] }`. Zero * regressions on a golden match; N on injected divergence. The `meta` * container is compared as a SUBSET (only golden-declared meta keys are * asserted) so deterministic-but-golden-absent infra keys * (`specVersion`, `schemaVersion`, `transport`, `mvi`, `contextVersion`, * `strict`, `x-cleo-transport`, …) stamped by the runtime are NOT phantom * regressions. The semantic payload (`data`/`error`) is still compared by * full key-union — a missing or extra payload key IS a regression. * 3. {@link computeQuestionHash} — sha256 of the normalized regression * signature (op coordinates + diff path set) so the SAME regression maps to * the SAME open DHQ row (the idempotency partial-UNIQUE index in T11889-A). * * `extractFieldFromResult` from `@cleocode/lafs` is reused for targeted-field * asserts where a full structural compare would be too brittle. * * This module is PURE — no DB, no native handle, no `cleo` mutation. * Import-time side-effect-free. * * @module @cleocode/core/selfimprove/envelope-diff * @epic T11889 * @task T11912 */ import type { ReplayEnvelope } from './replay.js'; import type { GoldenEntry, ScenarioOp } from './scenario.js'; /** * A normalized envelope: the replayed envelope with volatile `meta` fields removed. * * Structurally identical to {@link ReplayEnvelope} except every volatile * meta field is absent. Stored as a plain JSON-serializable record so it can be * compared deeply and hashed deterministically. */ export type NormalizedEnvelope = Record; /** * A single structural difference between a normalized envelope and its golden. */ export interface DiffEntry { /** Zero-based index of the op in the scenario whose envelope diverged. */ opIndex: number; /** The op coordinate (`domain.operation`) for the diverging op. */ opCoord: string; /** JSON-pointer-ish path to the diverging value (e.g. `data/tasks/0/id`). */ path: string; /** The value found in the replayed (actual) normalized envelope. */ actual: unknown; /** The value found in the golden (expected) envelope. */ expected: unknown; } /** The result of diffing a replayed envelope set against a golden. */ export interface EnvelopeDiffResult { /** All structural regressions; empty ⇒ the replay matched the golden. */ regressions: DiffEntry[]; } /** * Strip the volatile `meta`/`_meta` fields from an envelope. * * Returns a NEW object; the input is not mutated. The volatile timing/trace fields * AND the per-run session-lineage UUIDs (`sessionId`, `originSessionId`, * `executionSessionId`) are removed — see {@link VOLATILE_META_FIELDS}. The stable * meta remnant (`gateway`, `domain`, `operation`, `source`, …) is preserved so * structural identity (which operation produced the envelope) still compares. * * Note: deterministic-but-golden-absent infra meta keys (`specVersion`, * `transport`, …) are NOT stripped here — they are tolerated by the SUBSET compare * in {@link diffEnvelopes} instead, so the strip set stays exactly the volatile * fields and nothing semantic. * * @param envelope - The replayed envelope to normalize. * @returns The normalized envelope with volatile meta fields removed. * * @example * ```ts * const norm = normalizeEnvelope(envelope); * // norm.meta has no timestamp / requestId / duration_ms / *SessionId * ``` */ export declare function normalizeEnvelope(envelope: ReplayEnvelope): NormalizedEnvelope; /** * Diff a set of replayed envelopes against the golden envelope set. * * Normalizes each replayed envelope (volatile meta stripped) and structurally * compares it against the positionally-aligned golden entry. The op coordinate * for each `DiffEntry` is taken from `ops[opIndex]`. Callers MUST pass aligned * arrays (`ops.length === replayed.length === golden.length`); the function * diffs up to the shortest length and reports an extra regression for any length * mismatch. * * The `meta` container is compared as a SUBSET (only golden-declared meta keys are * asserted) so deterministic-but-golden-absent infra keys the runtime stamps * (`specVersion`, `transport`, …) are not phantom regressions; the semantic * payload (`data`/`error`) keeps a full key-union compare where an extra or * missing key IS a regression. Together with the volatile-lineage strip in * {@link normalizeEnvelope}, a real live envelope that matches the golden body * yields ZERO regressions. * * @param ops - The scenario ops (for op-coordinate labelling). * @param replayed - The captured envelopes, one per op (replay order). * @param golden - The golden expected entries, positionally aligned with `ops`. * @returns `{ regressions }` — empty when the replay matches the golden. * * @example * ```ts * const { regressions } = diffEnvelopes(scenario.ops, replayed, golden.envelopes); * if (regressions.length === 0) { /* happy path — no DHQ, no PR *\/ } * ``` */ export declare function diffEnvelopes(ops: ScenarioOp[], replayed: ReplayEnvelope[], golden: GoldenEntry[]): EnvelopeDiffResult; /** * Extract a targeted field from a normalized envelope's `data` payload. * * Reuses the LAFS `--field` extractor (`extractFieldFromResult`) for cases where * a full structural diff is too brittle and only a specific field's value matters * (e.g. assert `data.tasks[0].id` without comparing the whole envelope). * * @param normalized - A normalized envelope (from {@link normalizeEnvelope}). * @param field - The field name to extract from the envelope's `data` payload. * @returns The extracted value, or `undefined` when absent. * * @example * ```ts * const id = extractTargetedField(normalizeEnvelope(env), 'id'); * ``` */ export declare function extractTargetedField(normalized: NormalizedEnvelope, field: string): unknown; /** * Compute the sha256 `question_hash` of a diff result's normalized signature. * * The hash is over the {@link regressionSignature} (op coordinates + diff path * set), so it is stable across runs of the SAME regression and feeds the * idempotency partial-UNIQUE index (`ux_selfimprove_dhq_open`, T11889-A): one * open DHQ row per `question_hash`. An empty regression set hashes the empty * signature deterministically (callers should not persist on the happy path). * * @param result - The diff result whose regressions are hashed. * @returns The lowercase hex sha256 of the regression signature. * * @example * ```ts * const hash = computeQuestionHash(diffEnvelopes(ops, replayed, golden)); * ``` */ export declare function computeQuestionHash(result: EnvelopeDiffResult): string; //# sourceMappingURL=envelope-diff.d.ts.map