[AZ-387] C5 smoothed-history → FDR side-channel

After every successful current_estimate(), emit one
c5.state.smoothed_history FDR record per newly-smoothed past
keyframe from IncrementalFixedLagSmoother. AC-4.5 (revised): the
smoothed stream goes ONLY to FDR; the C8 outbound forward-time
stream is unaffected.

Idempotency via _smoothed_fdr_watermark_s (smoother-native float
seconds); the same pose key is never emitted twice. Hook is
best-effort — internal failures log warnings but do not raise, so
a smoother divergence cannot contaminate the forward-time path.

Cross-task invariants documented:
- AC-3 ESKF no-op — AZ-386 installs an inert hook on the ESKF.
- AC-4 No C8 leak — enforced at the C8 boundary by AZ-261.

8 new unit tests against AC-1/2/5/6 + robustness (no-FDR-client,
marginals failure). Full suite: 640 passed, 2 skipped.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 07:13:44 +03:00
parent 7cbd17ee83
commit 098aabac0c
5 changed files with 479 additions and 1 deletions
@@ -0,0 +1,74 @@
# 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 01 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).