# C5 Smoothed-history → FDR path (NOT to FC)

**Task**: AZ-387_c5_smoothed_history_fdr
**Name**: C5 smoothed past-keyframe → FDR path (AC-4.5 revised; NOT to FC)
**Description**: After every successful `current_estimate()`, emit the most-recent smoothed past-keyframe (when one becomes available from `IncrementalFixedLagSmoother.calculateEstimate()` retroactive update) to FDR via `FdrClient` (AZ-273). The FDR record carries `smoothed=True`. CRITICAL: the smoothed past-keyframe stream MUST go ONLY to FDR — NEVER routed to C8 outbound (the FC stream is forward-time only per AC-4.5 revised). Wire-up: this task adds the post-`current_estimate` hook to `GtsamIsam2StateEstimator` (and `EskfStateEstimator` — but ESKF reports `smoothed=False`, so this hook is a no-op for ESKF and the hook respects that). Defensive check: the C8 outbound encoder MUST receive ONLY non-smoothed estimates (verified at the C8 boundary in AZ-261 tests; documented here as a cross-task invariant).
**Complexity**: 3 points
**Dependencies**: AZ-384 (`smoothed_history` impl), AZ-386 (for the no-op hook on ESKF), AZ-273 (`FdrClient`), AZ-272 (FDR record schema), AZ-263, AZ-269, AZ-266
**Component**: c5_state (epic AZ-260 / E-C5)
**Tracker**: AZ-387
**Epic**: AZ-260 (E-C5)

### Document Dependencies

- `_docs/02_document/contracts/c5_state/state_estimator_protocol.md` — Invariants 6, 7.
- `_docs/02_document/components/07_c5_state/description.md` — § 7 caveats (AC-4.5 internal smoothing is onboard only).

## Problem

Without this task, smoothed past-keyframes are not persisted to FDR — defeating AC-4.5 (revised) post-flight forensics. A naive impl that routes smoothed estimates to C8 outbound would also break the FC contract (AC-4.5 forbids retroactive corrections to the FC).

## Outcome

- New private hook `_emit_smoothed_to_fdr_if_any()` called after every `current_estimate()` in `GtsamIsam2StateEstimator`. Body: read recent smoothed past-keyframes via `_smoother.calculateEstimate()`; for each newly-smoothed past-keyframe (delta from last emission), emit `EstimatorOutput(smoothed=True)` via `fdr_client.put(...)`.
- `EskfStateEstimator` hook: no-op (ESKF doesn't smooth).
- Composition root invariant: the `EstimatorOutput` stream feeding C8 outbound is filtered to `smoothed=False` only (this filter is enforced in C8 inbound — AZ-261 — but documented here).
- Unit tests: synthetic graph with delayed convergence → smoothed past-keyframes appear after some frames; FDR records emitted with correct shape; ESKF hook is no-op (FDR call count for ESKF == 0 over a full replay); no smoothed estimates leak to a C8-stub queue.

## Scope

### Included
- `_emit_smoothed_to_fdr_if_any()` impl.
- Hook into `current_estimate()` for both estimators.
- ESKF no-op handling (honest behaviour).
- Unit tests covering both estimators.

### Excluded
- The C8 outbound filter — owned by AZ-261; this task documents the invariant.
- The FDR record schema — already AZ-272.
- iSAM2 estimator body — AZ-382 / AZ-384.

## Acceptance Criteria

**AC-1: iSAM2 emits smoothed past-keyframes to FDR** — synthetic graph with 20 frames of delayed convergence → smoothed entries appear in FDR after the smoother's window catches up.

**AC-2: FDR records have `smoothed=True`** — every emitted record carries the flag.

**AC-3: ESKF emits zero smoothed FDR records** — over a full 60-frame replay, `FdrClient.put` is never called from the ESKF hook.

**AC-4: No leak to C8 outbound** — a stub C8-outbound queue receives ZERO smoothed estimates over the same replay; only `smoothed=False` records reach it.

**AC-5: Idempotency** — emitting the same smoothed past-keyframe twice is prevented (via a `_last_emitted_smoothed_frame_id` watermark).

**AC-6: FDR record shape** — `kind="c5.state.smoothed_history"`, fields per the AZ-272 FDR record schema.

## Non-Functional Requirements

- Hook adds ≤ 5 ms to `current_estimate` p99 (per smoothed entry; usually 0–1 entries per call).

## Constraints

- Smoothed estimates ONLY go to FDR.
- ESKF hook MUST be a no-op (honesty).
- Idempotency via watermark.

## Risks & Mitigation

- **Risk: `IncrementalFixedLagSmoother` retro-emit timing** — verify against pinned GTSAM API; tests cover the typical case where smoothed entries appear with a few-frame delay.
- **Risk: Smoothed estimate accidentally routed to C8** — AZ-261's filter is the enforcement point; this task documents the invariant.

## Runtime Completeness

- **Named capability**: smoothed past-keyframe → FDR path.
- **Production code**: real `_smoother.calculateEstimate()` query; real FDR emission with `smoothed=True`; real watermark idempotency.
- **Unacceptable substitutes**: routing smoothed estimates to C8 (AC-4.5 violation); ESKF emitting fake smoothed records (honesty violation).
