# Hygiene — Consolidate `_iso_ts_now` helpers into `helpers/iso_timestamps.py`

**Task**: AZ-508_hygiene_iso_timestamps_consolidation
**Name**: ISO-timestamp helper consolidation
**Description**: Replace five identical private `_iso_ts_now()` one-liners across c6_tile_cache + c7_inference with a single Layer-1 helper `src/gps_denied_onboard/helpers/iso_timestamps.py` exposing `iso_ts_now() -> str`. Closes cumulative review batches 31–33 Finding F2 (Low / Maintainability) and the earlier 28–30 cumulative review Finding F3 (the 3 c6 copies).
**Complexity**: 2 points
**Dependencies**: AZ-263_initial_structure, AZ-266_log_module
**Component**: helpers (epic AZ-264 / E-CC-HELPERS)
**Tracker**: AZ-508
**Epic**: AZ-264 (E-CC-HELPERS)

### Document Dependencies

- `_docs/02_document/common-helpers/` — the canonical home for shared utilities. This task adds a 9th entry. No new doc file required (the helper is mechanical enough to be self-documenting); the module-layout.md helpers table gains a row.
- `_docs/03_implementation/cumulative_review_batches_31-33_cycle1_report.md` § F2 — the finding being closed.
- `_docs/03_implementation/cumulative_review_batches_28-30_cycle1_report.md` § F3 — the earlier c6 instances also closed here.

## Problem

Five modules across two components each define a private one-line UTC-ISO timestamp helper:

- `src/gps_denied_onboard/components/c7_inference/tensorrt_runtime.py` (AZ-298)
- `src/gps_denied_onboard/components/c7_inference/onnx_trt_ep_runtime.py` (AZ-299)
- `src/gps_denied_onboard/components/c6_tile_cache/postgres_filesystem_store.py` (AZ-305)
- `src/gps_denied_onboard/components/c6_tile_cache/freshness_gate.py` (AZ-307)
- `src/gps_denied_onboard/components/c6_tile_cache/cache_budget_enforcer.py` (AZ-308)

Each returns `datetime.now(timezone.utc).isoformat(timespec="microseconds")` (or the equivalent UTC-with-microseconds string) for FDR record timestamps. The function is mechanical, deterministic, and not component-specific. Every additional C7/C6/C10 task that emits FDR records is at risk of adding the 6th, 7th, … copy unless a Layer-1 helper exists.

## Outcome

- A new helper module `src/gps_denied_onboard/helpers/iso_timestamps.py` exposing a single public function `iso_ts_now() -> str` (no class wrapper; helpers prefer free functions when stateless).
- The five existing component modules import `iso_ts_now` from the helper and delete their local `_iso_ts_now()` definitions. Call sites keep working without renaming (the local symbol can re-alias the import).
- A new unit test `tests/unit/helpers/test_iso_timestamps.py` covering format, timezone, and monotonicity contracts.
- A row added to `_docs/02_document/module-layout.md`'s helpers section (or the `_docs/02_document/common-helpers/` index) listing the new helper alongside the existing 8.

## Scope

### Included

- Create `src/gps_denied_onboard/helpers/iso_timestamps.py`:
  ```python
  from datetime import datetime, timezone

  def iso_ts_now() -> str:
      return datetime.now(timezone.utc).isoformat(timespec="microseconds")
  ```
  The `timespec="microseconds"` argument is what produces the `.ffffff` suffix that the FDR record format already relies on. Re-confirm by reading the existing `_iso_ts_now()` definitions before final write.
- Replace the five local `_iso_ts_now()` definitions with imports from the helper. Preferred form:
  ```python
  from gps_denied_onboard.helpers.iso_timestamps import iso_ts_now as _iso_ts_now
  ```
  This keeps the call-site symbol names unchanged, so no other line of each component module needs editing.
- Add `tests/unit/helpers/test_iso_timestamps.py` covering:
  - AC-1: format regex match (`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}\+00:00$`)
  - AC-2: returned string parses back via `datetime.fromisoformat(...)` to a UTC-aware datetime
  - AC-3: two successive calls produce monotonically non-decreasing strings (string comparison is correct for ISO-8601 UTC microsecond format)
- Add the helper to the `_docs/02_document/module-layout.md` helpers row table (or the `common-helpers/` index, whichever is the canonical inventory).

### Excluded

- Any change to `flight_clock`, `wall_clock`, or other adapter-style time sources. Those are domain-specific clocks; this helper is a low-level UTC-ISO formatter.
- Bumping FDR schema versions. The output format is bit-identical to the current local definitions; no schema change.
- Migrating timestamp call sites outside the five flagged files. Any future emitter should use the helper from the start; existing non-flagged emitters are out of scope.
- Creating a `Clock` Protocol or replacing `datetime.now(timezone.utc)` with an injectable clock. That is a larger architectural change (testability) not in scope here.
- Touching FDR record schema tests (`tests/unit/test_az272_fdr_record_schema.py`). They should continue to pass without modification.

## Acceptance Criteria

**AC-1: Helper exists at the canonical path**
Given a fresh checkout
When `from gps_denied_onboard.helpers.iso_timestamps import iso_ts_now` is run
Then the import succeeds; the function is callable; the returned value is a `str`

**AC-2: Output format is byte-identical to the previous local helpers**
Given the existing FDR record format that the five local helpers produced
When `iso_ts_now()` is called
Then the output matches `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}\+00:00$` and is parseable by `datetime.fromisoformat(...)` into a UTC-aware datetime

**AC-3: Monotonicity under normal clock**
Given two `iso_ts_now()` calls separated by at least 1 microsecond
When the returned strings are compared lexicographically
Then the second is `>=` the first (string comparison is valid for fixed-width ISO-8601 UTC microsecond format)

**AC-4: All five call sites migrated**
Given a `grep -rn "def _iso_ts_now" src/` after the task lands
When the search runs
Then zero matches are found in `c6_tile_cache/` or `c7_inference/` (or anywhere under `src/`); the helper is the only definition

**AC-5: FDR record schema tests still pass without modification**
Given the existing `tests/unit/test_az272_fdr_record_schema.py` suite
When the suite runs after this task
Then every previously-passing test still passes; no test file is modified

**AC-6: No new third-party dependencies**
Given `pyproject.toml`
When the task lands
Then the dependency set is unchanged (only stdlib `datetime` + `timezone` are used)

**AC-7: AZ-270 lint still passes**
Given the helpers module lives at Layer 1 (`src/gps_denied_onboard/helpers/`)
When `tests/unit/test_az270_compose_root.py::test_ac6` runs
Then the test passes (the lint only walks `src/gps_denied_onboard/components/`, so a helper import is allowed)

## Non-Functional Requirements

**Performance**
- `datetime.now(timezone.utc).isoformat(timespec="microseconds")` is the same call the existing local helpers make; this task adds zero runtime cost.

**Compatibility**
- Python stdlib only. No new third-party dependencies.

**Reliability**
- Deterministic format string; no platform-specific formatting branches.

## Unit Tests

| AC Ref | What to Test | Required Outcome |
|--------|-------------|------------------|
| AC-1 | Import + call | Returns str |
| AC-2 | Regex match + `fromisoformat` round-trip | Both pass |
| AC-3 | Two successive calls with `time.sleep(0.000002)` between | `second >= first` |
| AC-4 | Grep gate at PR-CI level (or covered by an existing import-discipline test) | Zero matches outside the helper module |
| AC-5 | Run `tests/unit/test_az272_fdr_record_schema.py` after the migration | All tests pass without modification |
| AC-7 | Run AZ-270 lint test | Pass |

## Constraints

- The helper is a pure free function. No class wrapping, no instance state.
- The helper MUST NOT import from `gps_denied_onboard.components.*` (Layer 1 discipline).
- Call sites in c6_tile_cache + c7_inference MAY keep the local `_iso_ts_now` alias for minimal diff (`from … import iso_ts_now as _iso_ts_now`). This is preferred over editing every call site.
- The output format is locked at `isoformat(timespec="microseconds")` — switching to seconds or nanoseconds would change FDR record bit-shape and is out of scope.

## Risks & Mitigation

**Risk 1: One of the five local helpers actually differs subtly (e.g., omits microseconds, uses `'Z'` suffix)**
- *Risk*: Migrating naively changes the FDR record format for some kinds.
- *Mitigation*: Before final write, read each of the five existing definitions and diff their output format. If any differ, document the discrepancy in this task's report and either (a) align the divergent caller to the canonical format (FDR records standardize on `+00:00` per existing `test_az272_fdr_record_schema.py` fixtures), or (b) split into two helpers if a real domain difference exists. AC-5's unaltered schema test suite is the safety net.

**Risk 2: Future emitter forgets the helper exists and adds a 6th local copy**
- *Risk*: The pattern recurs.
- *Mitigation*: Add a one-line note in `_docs/02_document/common-helpers/` index referencing this helper, and in module-layout.md's helpers table. Code review of future C7/C6/C10/C11 batches should reject local timestamp helpers.

**Risk 3: A circular import (helpers → components)**
- *Risk*: The helpers/ package somehow back-imports from components.
- *Mitigation*: The helper only uses stdlib `datetime` + `timezone`. No risk of cycle.

## Runtime Completeness

- **Named capability**: a single Layer-1 UTC-ISO timestamp helper that replaces five duplicated private one-liners.
- **Production code that must exist**: real `iso_ts_now()` function at the documented path; real import + delete in each of the five flagged modules.
- **Allowed external stubs**: none. This task is pure consolidation.
- **Unacceptable substitutes**: keeping one or more local `_iso_ts_now()` "for parity" (defeats the consolidation); using `datetime.utcnow()` (deprecated, returns naive datetime — would break the timezone-aware contract); changing the format string (would invalidate FDR records).
