/** * variable-recall — footprintjs's VARIABLE slices, joined to agent vocabulary. * * footprintjs answers "what happened to this variable?" in the address space it * owns: commit indices, runtimeStageIds, state keys (`keyTimeline` / * `forwardSliceForKey`, `footprintjs/trace`). An agent debugger asks the same * question in a DIFFERENT vocabulary: which LOOP was that, which injected fact * or tool result is that write, and what would REMOVING it cost. This module is * the join between the two — and nothing more: * * - **Pure assembly.** Every fact here was already recorded. No new capture, * no new scorer, no embedder, no LLM. `joinVariableSlice` re-labels; it * never measures. * - **Loop lifting.** Each moment gets the `loopIndex` of the frame that * contains its `runtimeStageId` (the walk's own `buildWriterFrameIndex` — * one resolver, not two). * - **Agent identity.** A WRITE whose value the trajectory carries is run * through the SAME `defaultSuspectClassifier` the localizer uses, so a * write joins 1:1 with a `Suspect` (`injectionId` / `toolName`) and gets * the `AblationSpec` that would remove it. Writers the classifier cannot * name get NO hook — an honest absence, never a fabricated one. * - **Honesty rides along verbatim.** footprintjs's `HonestyNote`s, the * missing-reason, and `readsCoverage` are copied, not re-worded. * * The one JUDGEMENT this module makes is {@link DataflowCoverage} — see the * long comment on `dataflowCoverage()`. It is what lets `walkToRoot` replace a * text-similarity guess with a recorded dataflow edge, and it is deliberately * strict. * * @beta Beta feature (RFC-003 Part B). The API works and is tested, but may * change before GA. */ import type { ForwardSlice, HonestyNote, KeyMoment, KeyTimeline, MissingSliceReason, ReadsCoverage, StateKey } from 'footprintjs/trace'; import { type SuspectClassifier } from './localize.js'; import { type Trajectory } from './trajectory.js'; import type { AblationSpec, ContextBugArtifacts, SuspectKind } from './types.js'; /** * How exactly this log records what a key's value FED — the one judgement in * this module, and the gate on `walkToRoot`'s deterministic hop. * * - `'exact'` — every recorded `fed` edge for this key carries per-write * provenance (`FedBasis: 'per-write'`, the `writeProvenance: 'reads-prefix'` * dial): a downstream write is linked to this value BECAUSE that write's * recorded read-prefix names the key, and a write whose prefix omits it is * excluded exactly. * - `'conservative'` — at least one edge is stage-level co-occurrence * (`'conservative-fed-edges'`), or the log carries no recorded reads at all. * Sound over-approximation; NEVER presented as exact. * - `'unknown'` — no forward slice was supplied, OR the key has no recorded * `fed` edge at all. The second case is the trap: with zero edges there is * also zero conservative edge, so "no conservative edges" would be VACUOUSLY * true and a key nothing ever read would masquerade as the most exact key in * the run. It is demoted here instead. */ export type DataflowCoverage = 'exact' | 'conservative' | 'unknown'; /** * One moment in a key's life ({@link KeyMoment}), lifted into agent vocabulary. * The footprintjs fields are carried through UNCHANGED — this type only ADDS. */ export interface AgentKeyMoment { readonly kind: 'write' | 'read'; /** Commit ARRAY position — the write's own, or the READING stage's. */ readonly commitIdx: number; readonly runtimeStageId: string; readonly stageId: string; readonly stageName: string; /** `'write'` moments only: the trace verb. */ readonly verb?: KeyMoment['verb']; /** `'read'` moments only: the commitIdx of the write whose value it saw. */ readonly fromWriteIdx?: number; /** * The ReAct loop this moment happened in. ABSENT when the step is not in any * frame (run prelude, root-seeded setup) — absence is information, not a gap. */ readonly loopIndex?: number; /** * `'write'` moments the classifier recognized: what KIND of agent source this * write produced. When one write produced several sources (an injection slot * commits an array of records), this is the FIRST one's kind and * {@link AgentVariableSlice.ablations} carries them all. */ readonly suspectKind?: SuspectKind; /** The suspect identity (`injectionId` / `toolName`) — joins a `Suspect` 1:1. */ readonly suspectId?: string; } /** * The counterfactual attached to one classifiable WRITE: "to test this write, * re-run without this source". A HOOK, not a verdict — running it is the * consumer's `AblationRunner` (§B2: only ablation verdicts are causal claims). */ export interface VariableAblationHook { /** runtimeStageId of the write that introduced the source. */ readonly writerId: string; /** `injectionId` / `toolName` — the localizer's suspect identity. */ readonly suspectId: string; readonly kind: SuspectKind; /** What to remove for the counterfactual re-run. */ readonly spec: AblationSpec; } /** * One variable's recorded life, in agent vocabulary — the artifact BOTH a human * board ({@link variableToBacktrackTrace}) and the walk read. * * JSON-safe by construction (flat moments, no graph, no live references): the * same object can be persisted, sent over the wire, or handed to an LLM tool. */ export interface AgentVariableSlice { /** The state key asked about (footprintjs's normalised string form). */ readonly key: string; /** Writes and reads in commit order. EMPTY when `missing`. */ readonly moments: readonly AgentKeyMoment[]; /** Present ONLY when there are no moments — footprintjs's honest absence. */ readonly missing?: MissingSliceReason; /** How exactly this log records what the value fed — see {@link DataflowCoverage}. */ readonly coverage: DataflowCoverage; /** One per classifiable write — see {@link VariableAblationHook}. */ readonly ablations: readonly VariableAblationHook[]; /** footprintjs's honesty notes, VERBATIM (both doors' notes, de-duplicated). */ readonly notes: readonly HonestyNote[]; /** Which `KeysReadSource` strategy resolved reads (honesty/debug). */ readonly keysReadKind: string; /** Reads telemetry — `stepsWithReads === 0` means reads were not recorded. */ readonly readsCoverage?: ReadsCoverage; } export interface JoinVariableSliceOptions { /** * The FORWARD slice of the same key (`forwardSliceForKey`) — the ONLY carrier * of per-edge exactness (`ForwardEdge.basis`). Without it `coverage` is * `'unknown'` and the walk will not take a dataflow hop. */ readonly forward?: ForwardSlice; /** Override the suspect classifier (default `defaultSuspectClassifier`). */ readonly classify?: SuspectClassifier; } export interface TraceVariableOptions extends JoinVariableSliceOptions { /** Reuse an already-assembled trajectory (avoids re-slicing the run). */ readonly trajectory?: Trajectory; /** Exclusive commit-array-index bound — the variable as it stood BEFORE this idx. */ readonly before?: number; } /** * Join a footprintjs variable slice to the agent's loops and sources. * * @param slice A `keyTimeline(...)` result — the flat, JSON-safe life of one * key. (Chosen over the backward `sliceForKey` deliberately: the walk needs * moments in commit order, and a timeline cannot be mistaken for a DAG.) * @param trajectory `assembleTrajectory(artifacts)` — supplies loop frames AND * the committed values used for classification. * @param opts `forward` (the exactness carrier) and `classify`. * * Nothing here is measured: every field is a re-label of something already * recorded. See the module doc. */ export declare function joinVariableSlice(slice: KeyTimeline, trajectory: Trajectory, opts?: JoinVariableSliceOptions): AgentVariableSlice; /** * One call, from a recorded run to a variable's joined life: `keyTimeline` + * `forwardSliceForKey` + `assembleTrajectory` + {@link joinVariableSlice}. * * Reads come from the snapshot's execution tree (zero setup, `readTracking` ≠ * `'off'`); exactness comes from the run's `writeProvenance` dial. Both facts * ride out on the result (`readsCoverage`, `coverage`) rather than being * assumed. * * @example * ```ts * const life = traceVariable({ snapshot }, 'systemPromptInjections'); * life.coverage; // 'exact' | 'conservative' | 'unknown' * life.moments.filter((m) => m.kind === 'write').map((m) => m.loopIndex); * ``` */ export declare function traceVariable(artifacts: ContextBugArtifacts, key: StateKey, opts?: TraceVariableOptions): AgentVariableSlice;