From 5441ea2017f6592529ecc5804924c906dc8bbdb3 Mon Sep 17 00:00:00 2001 From: Oleksandr Bezdieniezhnykh Date: Wed, 13 May 2026 23:23:22 +0300 Subject: [PATCH] [AZ-508] Consolidate _iso_ts_now into helpers/iso_timestamps Batch 48 / Cycle 1 (greenfield Step 7). Closes cumulative review batches 31-33 F2 and 28-30 F3 by replacing the duplicated private _iso_ts_now() one-liners with a single Layer-1 helper: src/gps_denied_onboard/helpers/iso_timestamps.py iso_ts_now() -> str Output format matches the canonical FDR _TS fixture (YYYY-MM-DDTHH:MM:SS.ffffffZ); no FDR schema change. Migrated call-sites (3): c7_inference/onnx_trt_ep_runtime, c7_inference/thermal_publisher, plus the 3 c6_tile_cache callers that previously imported from the local c6_tile_cache/_timestamp shim (now deleted, superseded by the Layer-1 helper). Spec drift resolved (Choose A, user-approved): spec listed 5 call sites + +00:00 regex; on-disk reality at batch start is 3 sites + Z-suffix matching every existing helper and the FDR _TS fixture. Spec preamble + AC-2 regex updated in the task file; documented in batch_48_cycle1_report.md. Tests: 9 new AC tests (AC-1..AC-7 + Layer-1 invariant + public-surface defensive); 216 focused tests pass including the unmodified AZ-272 FDR schema suite and AZ-270 / AZ-507 layering lints. Verdict: PASS (no findings). Co-authored-by: Cursor --- _docs/02_document/module-layout.md | 7 + ...08_hygiene_iso_timestamps_consolidation.md | 8 +- .../batch_48_cycle1_report.md | 222 ++++++++++++++++++ .../reviews/batch_48_review.md | 126 ++++++++++ _docs/_autodev_state.md | 4 +- ...05-11_d_cross_cve_1_opencv_pin_deferred.md | 3 + .../components/c6_tile_cache/_timestamp.py | 21 -- .../c6_tile_cache/cache_budget_enforcer.py | 2 +- .../c6_tile_cache/freshness_gate.py | 2 +- .../postgres_filesystem_store.py | 2 +- .../c7_inference/onnx_trt_ep_runtime.py | 14 +- .../c7_inference/thermal_publisher.py | 7 +- src/gps_denied_onboard/helpers/__init__.py | 2 + .../helpers/iso_timestamps.py | 33 +++ tests/unit/test_az508_iso_timestamps.py | 191 +++++++++++++++ 15 files changed, 596 insertions(+), 48 deletions(-) rename _docs/02_tasks/{todo => done}/AZ-508_hygiene_iso_timestamps_consolidation.md (90%) create mode 100644 _docs/03_implementation/batch_48_cycle1_report.md create mode 100644 _docs/03_implementation/reviews/batch_48_review.md delete mode 100644 src/gps_denied_onboard/components/c6_tile_cache/_timestamp.py create mode 100644 src/gps_denied_onboard/helpers/iso_timestamps.py create mode 100644 tests/unit/test_az508_iso_timestamps.py diff --git a/_docs/02_document/module-layout.md b/_docs/02_document/module-layout.md index c3998f8..5ad3807 100644 --- a/_docs/02_document/module-layout.md +++ b/_docs/02_document/module-layout.md @@ -360,6 +360,13 @@ Bootstrap reference: `_docs/02_tasks/todo/AZ-263_initial_structure.md`. Architec - **Owned by**: AZ-264. - **Consumed by**: c2_vpr, c2_5_rerank, c3_matcher. +### shared/helpers/iso_timestamps + +- **File**: `src/gps_denied_onboard/helpers/iso_timestamps.py` +- **Purpose**: Single Layer-1 UTC ISO-8601 timestamp source for FDR record envelopes (`iso_ts_now() -> str`, format `YYYY-MM-DDTHH:MM:SS.ffffffZ` matching the canonical FDR `_TS` fixture). Replaces the duplicated private `_iso_ts_now` one-liners that previously lived inside `c6_tile_cache` (3 modules — already locally consolidated under `_timestamp.py`) and `c7_inference` (2 modules). Stateless free function; stdlib only. +- **Owned by**: AZ-264 (AZ-508 consolidation task). +- **Consumed by**: c6_tile_cache (`cache_budget_enforcer`, `postgres_filesystem_store`, `freshness_gate`), c7_inference (`onnx_trt_ep_runtime`, `thermal_publisher`); future C10/C11 FDR producers should import this helper rather than redefining the one-liner locally. + ### shared/frame_source - **Directory**: `src/gps_denied_onboard/frame_source/` diff --git a/_docs/02_tasks/todo/AZ-508_hygiene_iso_timestamps_consolidation.md b/_docs/02_tasks/done/AZ-508_hygiene_iso_timestamps_consolidation.md similarity index 90% rename from _docs/02_tasks/todo/AZ-508_hygiene_iso_timestamps_consolidation.md rename to _docs/02_tasks/done/AZ-508_hygiene_iso_timestamps_consolidation.md index 8f8ede3..f4655d6 100644 --- a/_docs/02_tasks/todo/AZ-508_hygiene_iso_timestamps_consolidation.md +++ b/_docs/02_tasks/done/AZ-508_hygiene_iso_timestamps_consolidation.md @@ -2,7 +2,9 @@ **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). +**Description**: Replace the duplicated 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). + +> **Spec drift note (Batch 48, 2026-05-13)**: this task was authored before c6 had locally consolidated its three copies into `components/c6_tile_cache/_timestamp.py`, and before `tensorrt_runtime.py` removed its local copy. Actual call-sites needing migration at implementation time: 3 (c6 `_timestamp.py` shim, c7 `onnx_trt_ep_runtime.py`, c7 `thermal_publisher.py`). Also, AC-2 originally specified a `+00:00` regex copied from a misread of `test_az272_fdr_record_schema.py`; the canonical FDR `ts` format on disk is the `Z`-suffix variant (`_TS = "2026-05-11T00:00:00.000000Z"`), produced by all three existing local helpers. AC-2 regex below has been corrected to `Z`; this preserves the AC-5 "FDR schema tests pass unmodified" invariant and the Excluded clause "no schema change". **Complexity**: 2 points **Dependencies**: AZ-263_initial_structure, AZ-266_log_module **Component**: helpers (epic AZ-264 / E-CC-HELPERS) @@ -73,9 +75,9 @@ 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 +Given the existing FDR record format that the three local helpers produced (`%Y-%m-%dT%H:%M:%S.%fZ`, canonical FDR `_TS` fixture) 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 +Then the output matches `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z$` and is parseable by `datetime.fromisoformat(value.replace("Z", "+00:00"))` into a UTC-aware datetime **AC-3: Monotonicity under normal clock** Given two `iso_ts_now()` calls separated by at least 1 microsecond diff --git a/_docs/03_implementation/batch_48_cycle1_report.md b/_docs/03_implementation/batch_48_cycle1_report.md new file mode 100644 index 0000000..0a1abb0 --- /dev/null +++ b/_docs/03_implementation/batch_48_cycle1_report.md @@ -0,0 +1,222 @@ +# Batch 48 / Cycle 1 — Implementation Report + +**Date**: 2026-05-13 +**Tasks**: AZ-508 — ISO-timestamp helper consolidation (2pt) +**Total complexity**: 2 points +**Result**: PASS (per-batch code review) +**Jira tracker state**: AZ-508 transitioned To Do → In Progress (prior +session) → In Testing (this batch) + +## Scope + +Hygiene PBI from cumulative reviews batches 28–30 (Finding F3) and +batches 31–33 (Finding F2). Consolidates the duplicated private +`_iso_ts_now()` one-liners across `c6_tile_cache` and `c7_inference` +into a single Layer-1 helper at +`src/gps_denied_onboard/helpers/iso_timestamps.py` (`iso_ts_now() -> str`, +RFC 3339 UTC microsecond `Z`-suffix). Closes F2 before the upcoming +C2 batches (AZ-339 / AZ-340) and the C4/C3 batches (AZ-358 / AZ-349) +would have added the 8th and 9th copies of the helper. + +## Spec Drift Resolution (User-Approved Choose A) + +The task spec assumed: +- **5** call-sites needing migration. +- AC-2 format regex `\.\d{6}\+00:00$`. +- "FDR records standardize on `+00:00` per `test_az272_fdr_record_schema.py`". + +On-disk reality at Batch 48 start: +- **3** call-sites: c6's three callers had already been locally + consolidated under `src/gps_denied_onboard/components/c6_tile_cache/_timestamp.py` + (export `iso_ts_now`, same `Z`-suffix format); `tensorrt_runtime.py` + carries no local `_iso_ts_now` definition. +- All 3 existing helpers produce `%Y-%m-%dT%H:%M:%S.%fZ` (`Z` suffix). +- The canonical FDR `ts` fixture is `_TS = "2026-05-11T00:00:00.000000Z"` + (also `Z` suffix). The `+00:00` strings in that test file belong to + *other* DTO fields (manifest `generated_at_iso`, `observed_at_iso`), + not the FDR `ts` envelope. + +Picking the spec's `+00:00` literally would have silently changed FDR +record `ts` bit-shape — explicitly **Excluded** by the spec ("no schema +change") and would have broken AC-5 (FDR schema tests pass unmodified). + +User selected **Choose A**: Z-format helper, byte-identical to existing +3 helpers + FDR `_TS` fixture; AC-2 regex corrected in the spec; drift +documented in this report and in the spec's preamble. + +## Files Changed + +### Production (new) + +- `src/gps_denied_onboard/helpers/iso_timestamps.py` — + `iso_ts_now() -> str`. Stateless free function; stdlib `datetime` + + `timezone` only; format + `datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")`. + +### Production (modified) + +- `src/gps_denied_onboard/helpers/__init__.py` — re-exports `iso_ts_now` + through the helpers package facade (alphabetical insertion in both + the import block and `__all__`). +- `src/gps_denied_onboard/components/c6_tile_cache/cache_budget_enforcer.py` — + import path `c6_tile_cache._timestamp` → `helpers.iso_timestamps` + (one-line edit; call-sites untouched). +- `src/gps_denied_onboard/components/c6_tile_cache/postgres_filesystem_store.py` — + same path swap (2 call-sites untouched). +- `src/gps_denied_onboard/components/c6_tile_cache/freshness_gate.py` — + same path swap (2 call-sites untouched). +- `src/gps_denied_onboard/components/c7_inference/onnx_trt_ep_runtime.py` — + removed local `def _iso_ts_now`; added top-of-file + `from gps_denied_onboard.helpers.iso_timestamps import iso_ts_now as _iso_ts_now` + (preserves the `_iso_ts_now` symbol at 2 call-sites); removed the + now-unused `from datetime import datetime, timezone`. +- `src/gps_denied_onboard/components/c7_inference/thermal_publisher.py` — + same pattern (1 call-site preserved); removed the now-unused + `from datetime import datetime, timezone`. + +### Production (deleted) + +- `src/gps_denied_onboard/components/c6_tile_cache/_timestamp.py` — + the c6-internal consolidation shim is no longer needed; the three + c6 callers now import directly from `helpers.iso_timestamps`. The + Layer-1 helper supersedes the Layer-2 component-local one. + +### Tests (new) + +- `tests/unit/test_az508_iso_timestamps.py` — 9 tests: + - `test_ac1_import_and_call_returns_str` (AC-1) + - `test_ac2_format_matches_canonical_regex` (AC-2 format) + - `test_ac2_fromisoformat_roundtrip_yields_utc_aware_datetime` (AC-2 parseability) + - `test_ac3_two_successive_calls_are_non_decreasing` (AC-3) + - `test_ac4_no_other_iso_ts_now_definition_exists_in_src` (AC-4 + AST-walk over `src/`) + - `test_ac4_c6_and_c7_callers_import_from_helpers` (AC-4 explicit + call-site sweep — 5 files) + - `test_ac6_helper_uses_stdlib_only` (AC-6, stdlib whitelist) + - `test_helper_is_layer_1_no_component_imports` (Constraint + § Layer-1 discipline) + - `test_helper_public_surface_is_minimal` (defensive: `__all__` + is exactly `["iso_ts_now"]`) + +### Docs (modified) + +- `_docs/02_document/module-layout.md` — appended + `### shared/helpers/iso_timestamps` row to the helpers section. +- `_docs/02_tasks/todo/AZ-508_hygiene_iso_timestamps_consolidation.md` — + drift note added to the description; AC-2 regex corrected from + `\+00:00$` to `Z$`. +- `_docs/03_implementation/reviews/batch_48_review.md` (new) — + per-batch code review (PASS). +- `_docs/_process_leftovers/2026-05-11_d_cross_cve_1_opencv_pin_deferred.md` — + replay-attempt timestamp + reason updated (PyPI shows `gtsam==4.2.1` + but `requires_dist: numpy<2.0.0`; block unchanged). + +## Acceptance Criteria Coverage + +| AC | Description | Status | Test | +|----|-------------|--------|------| +| AC-1 | Helper exists, importable, callable, returns `str` | PASS | `test_ac1_import_and_call_returns_str` | +| AC-2 | Format regex match + `fromisoformat` round-trip | PASS | `test_ac2_format_matches_canonical_regex`, `test_ac2_fromisoformat_roundtrip_yields_utc_aware_datetime` | +| AC-3 | Lexicographic monotonicity under normal clock | PASS | `test_ac3_two_successive_calls_are_non_decreasing` | +| AC-4 | All call sites migrated; no stray definitions | PASS | `test_ac4_no_other_iso_ts_now_definition_exists_in_src`, `test_ac4_c6_and_c7_callers_import_from_helpers` | +| AC-5 | FDR schema tests pass unmodified | PASS | `tests/unit/test_az272_fdr_record_schema.py` runs unchanged (35 tests pass) | +| AC-6 | No new third-party dependencies | PASS | `test_ac6_helper_uses_stdlib_only` + `pyproject.toml` unchanged | +| AC-7 | AZ-270 layering lint passes | PASS | `tests/unit/test_az270_compose_root.py::test_ac6_only_compose_root_imports_concrete_strategies` | + +## Test Results + +- **Focused suite** (tests/unit/test_az508_iso_timestamps.py + + test_az272_fdr_record_schema.py + tests/unit/c6_tile_cache/ + + tests/unit/c7_inference/): **216 passed, 53 environment-skipped, 0 failed** + in 7.97s. Environment-gated skips (Docker compose for c6 integration, + CUDA / TensorRT / Jetson hardware for c7) — unchanged from Batch 47. +- **Focused per-helper**: 9/9 new AZ-508 tests PASS in 1.36s. +- **Layering lint**: AZ-270 + AZ-507 lints PASS (no cross-component + import violations introduced). +- **Lint**: `ruff check` clean on every file authored or modified by + this batch. 3 pre-existing ruff findings (B905, RUF022, RUF023) in + `onnx_trt_ep_runtime.py` left untouched per + `coderule.mdc` "fix pre-existing lints only if in the modified area". + +## Architectural Decisions + +1. **Layer-1 helper supersedes the c6 internal `_timestamp.py`**. + The c6-internal consolidation shipped earlier (cumulative review + 28–30 F3) was a Layer-2 stop-gap pending the cross-component + consolidation. With the Layer-1 helper now in place, the c6 shim + is redundant and was deleted; the three c6 callers now import + the cross-component helper directly. This collapses two + abstractions into one. + +2. **Preserve `_iso_ts_now` symbol at c7 call-sites via import alias**. + The c7 modules each have 1–2 in-method `_iso_ts_now()` call-sites. + Rather than edit every call-site, the new module-level + `from gps_denied_onboard.helpers.iso_timestamps import iso_ts_now as _iso_ts_now` + keeps the local symbol name unchanged → minimal diff per c7 file + and minimal risk of accidentally missing a call-site. Matches + the task spec's preferred form (Scope § Included bullet 2). + +3. **Stdlib-only helper, no Clock dependency**. The helper is wall-clock + metadata about when an FDR record envelope was emitted — explicitly + NOT a payload field that might need an injectable Clock for tests. + The task spec calls this out (Excluded § "Any change to flight_clock + / wall_clock"), and the new module docstring reiterates it so + future contributors do not route `age_seconds`-style fields through + it. + +4. **AST-walk AC-4 test instead of a grep gate**. AC-4 originally + contemplated a CI-level `grep -rn "def _iso_ts_now" src/` gate. The + in-repo test in `test_az508_iso_timestamps.py` uses an AST walk + instead — it catches both `def _iso_ts_now` AND + `def iso_ts_now` defined anywhere outside the helper, with file + paths in the assertion message, and runs as part of the standard + unit suite (no separate CI hook needed). The same test also enforces + the consumer-side import discipline so the regression surface is + "the import or the test breaks", not "a stray definition lingers". + +## Carried-over Findings (from prior batches) + +- **F1 from cumulative review 43–45 / Batch 46 / Batch 47**: closed by + this batch. `_iso_ts_now` is now in exactly one place. +- **F2 from Batch 46 / Batch 47** (spec→impl drift on C7 API names for + AZ-339 / AZ-340 / AZ-358 / AZ-349): **NOT addressed in this batch**; + belongs to a spec-hygiene PBI to be filed before AZ-339 starts. +- **D-CROSS-CVE-1 opencv pin leftover**: replay attempted at Batch 48 + bootstrap; PyPI shows `gtsam==4.2.1` (newer than 4.2 referenced in + the leftover) but still pins `numpy<2.0.0`. Block unchanged; + leftover timestamp updated. + +## New Findings (per Batch 48 code review) + +None. + +## Jira Tracker + +- AZ-508 was already `In Progress` at Batch 48 start (transitioned in + a prior `/autodev` session per the state file). Read-back via + `getJiraIssue` confirmed `status.name == "In Progress"` before any + source edits. Will transition `In Progress → In Testing` after this + batch's commit lands. +- Task spec archived to `_docs/02_tasks/done/AZ-508_hygiene_iso_timestamps_consolidation.md` + as part of the implement skill Step 13. + +## Next + +With AZ-508 closed, the carried-over F1 from batches 43–47 is resolved +and the next C2 / C4 / C3 batches can ship without re-adding the +helper. Top Batch 49 candidates (per Batch 47's "Next" list, updated): + +- **AZ-339** — MegaLoc + MixVPR strategies (5pt). Same skeleton as + UltraVPR (B47) but two strategies; F2 spec-hygiene PBI may want to + ship first. +- **AZ-340** — SelaVPR + EigenPlaces + SALAD strategies (5pt). Closes + the C2 alternative-backbone buffet. +- **AZ-358** — C4 OpenCV/GTSAM pose estimator (5pt). Changes pace from + C2-heavy streak; opens C4. +- **AZ-389** — C5 internal orthorectifier for mid-flight tiles (3pt). +- **Spec-hygiene PBI** for F2 (C7 API drift in AZ-339 / AZ-340 / AZ-358 / + AZ-349 specs) — strongly recommended before AZ-339. + +Per autodev orchestrator: Batch 48 is the third batch since the last +cumulative review (batches 43–45). The implement skill Step 14.5 +triggers a **cumulative review batches 46–48** before Batch 49 starts. diff --git a/_docs/03_implementation/reviews/batch_48_review.md b/_docs/03_implementation/reviews/batch_48_review.md new file mode 100644 index 0000000..105e7e8 --- /dev/null +++ b/_docs/03_implementation/reviews/batch_48_review.md @@ -0,0 +1,126 @@ +# Code Review Report — Batch 48 + +**Batch**: 48 (Cycle 1) +**Tasks**: AZ-508 — ISO-timestamp helper consolidation (2 pt) +**Date**: 2026-05-13 +**Verdict**: **PASS** + +## Findings + +_No findings._ + +## Phase Summary + +### Phase 1 — Context Loading + +- Task spec: `_docs/02_tasks/todo/AZ-508_hygiene_iso_timestamps_consolidation.md` +- Cumulative review F2 (batches 31–33) + F3 (batches 28–30) — origin of the + hygiene PBI. +- Changed source files: 1 new helper + 1 helper package re-export + + 3 c6 import rewires + 2 c7 local-def → import rewires + 1 deleted + c6 internal shim. +- Changed test files: 1 new (`tests/unit/test_az508_iso_timestamps.py`, + 9 tests covering AC-1..AC-7 + Layer-1 invariant). +- Changed docs: `module-layout.md` (new `shared/helpers/iso_timestamps` + entry) + task spec (drift note + corrected AC-2 regex). + +### Phase 2 — Spec Compliance (AC-by-AC) + +| AC | Verdict | Evidence | +|----|---------|----------| +| AC-1 | PASS | `test_ac1_import_and_call_returns_str` | +| AC-2 | PASS | `test_ac2_format_matches_canonical_regex` + `test_ac2_fromisoformat_roundtrip_yields_utc_aware_datetime` | +| AC-3 | PASS | `test_ac3_two_successive_calls_are_non_decreasing` | +| AC-4 | PASS | `test_ac4_no_other_iso_ts_now_definition_exists_in_src` (AST walk over `src/`) + `test_ac4_c6_and_c7_callers_import_from_helpers` | +| AC-5 | PASS | `tests/unit/test_az272_fdr_record_schema.py` runs unmodified, 100% pass | +| AC-6 | PASS | `test_ac6_helper_uses_stdlib_only` (AST whitelist) — `pyproject.toml` unchanged | +| AC-7 | PASS | `tests/unit/test_az270_compose_root.py::test_ac6_only_compose_root_imports_concrete_strategies` passes | + +**Spec drift note**: the task spec originally listed 5 call-sites and a +`+00:00` regex. On-disk reality at implementation time: 3 call-sites +(c6 already locally consolidated under `_timestamp.py`; `tensorrt_runtime.py` +had no local copy) and the canonical FDR `ts` format is `Z`-suffix per +the `_TS = "2026-05-11T00:00:00.000000Z"` fixture. Spec + AC-2 regex +updated in this batch; no behavioral drift from the spec's actual intent +("single Layer-1 helper, byte-identical to existing locals, no FDR +schema change"). User explicitly approved the path via Choose A. + +### Phase 3 — Code Quality + +- **SRP**: pure stateless free function; no class wrapper (per Constraint + § "no class wrapping"). +- **Error handling**: no exception suppression; stdlib `datetime.now` + cannot raise under normal operation. +- **Naming**: `iso_ts_now` matches the canonical name used by all three + pre-existing local helpers — no naming churn in call sites + (`from … import iso_ts_now as _iso_ts_now` alias preserved in the c7 + files). +- **Complexity**: 1 line. n/a. +- **DRY**: net deletion — eliminates duplication (the explicit goal). +- **Test quality**: assertions cover format, parseability, monotonicity, + absence of stray definitions, import-path enforcement, layer-1 + invariant, and stdlib-only constraint. +- **Dead code**: removed unused `from datetime import datetime, timezone` + imports from both c7 files after the local `_iso_ts_now` defs were + removed (adjacent-hygiene, permitted by `coderule.mdc`). + +### Phase 4 — Security Quick-Scan + +No SQL, no `subprocess`, no `eval` / `exec`, no secrets, no untrusted +input handling. Helper is stdlib-only and side-effect-free. n/a. + +### Phase 5 — Performance Scan + +`datetime.now(timezone.utc).strftime(...)` is the same call all three +previous local helpers made; zero runtime delta. No new allocations on +the hot path. + +### Phase 6 — Cross-Task Consistency + +Single task in batch — no inter-task interface concerns. Helper aligns +with the existing 8-helper convention (single-file Layer-1 modules under +`src/gps_denied_onboard/helpers/`, re-exported from `helpers/__init__.py`). + +### Phase 7 — Architecture Compliance + +- **Layer direction**: helper sits at Layer 1 (Foundation / shared); + consumers in c6 (Layer 2 Infrastructure) and c7 (Layer 2 + Infrastructure) import strictly downward. ✓ +- **Public API respect**: consumers import via + `gps_denied_onboard.helpers.iso_timestamps` (or the `helpers` package + facade after the `__init__.py` re-export). Internal modules of c6/c7 + are not imported across components. ✓ +- **No new cyclic deps**: the helper imports only stdlib; no possible + cycle. ✓ +- **Duplicate symbols**: eliminated by design — the AC-4 AST walk + asserts zero other `iso_ts_now` / `_iso_ts_now` definitions remain + anywhere under `src/`. ✓ +- **Cross-cutting concern home**: moved from per-component locals to + the canonical `helpers/` directory. ✓ +- **AZ-507 layering lint**: `test_ac6_only_compose_root_imports_concrete_strategies` + passes — helper imports are allowed from components. ✓ +- **AZ-270 lint**: passes. ✓ + +## Pre-existing Findings (NOT introduced by this batch) + +The following ruff findings remain in +`src/gps_denied_onboard/components/c7_inference/onnx_trt_ep_runtime.py` +but are pre-existing (verified by re-running ruff against `HEAD` before +this batch's edits): + +- **B905** at line 547 — `zip()` without `strict=`. +- **RUF022** at lines 88–98 — `__all__` not sorted. +- **RUF023** at lines 127–134 — `__slots__` not sorted. + +Per `coderule.mdc` "Pre-existing lint errors should only be fixed if +they're in the modified area" — left untouched. Candidate for a future +c7 hygiene PBI. + +## Verdict Justification + +- Critical findings: 0 +- High findings: 0 +- Medium findings: 0 +- Low findings: 0 + +**PASS** — proceed to commit per implement skill Step 11. diff --git a/_docs/_autodev_state.md b/_docs/_autodev_state.md index f8ad9e7..cbff520 100644 --- a/_docs/_autodev_state.md +++ b/_docs/_autodev_state.md @@ -8,9 +8,9 @@ status: in_progress sub_step: phase: 7 name: batch-loop - detail: "batch 48 — AZ-508 (ISO timestamp helper consolidation)" + detail: "cumulative review batches 46-48 due before next batch" retry_count: 0 cycle: 1 tracker: jira -last_completed_batch: 47 +last_completed_batch: 48 last_cumulative_review: batches_43-45 diff --git a/_docs/_process_leftovers/2026-05-11_d_cross_cve_1_opencv_pin_deferred.md b/_docs/_process_leftovers/2026-05-11_d_cross_cve_1_opencv_pin_deferred.md index f689ba2..b905a75 100644 --- a/_docs/_process_leftovers/2026-05-11_d_cross_cve_1_opencv_pin_deferred.md +++ b/_docs/_process_leftovers/2026-05-11_d_cross_cve_1_opencv_pin_deferred.md @@ -1,6 +1,9 @@ # D-CROSS-CVE-1 opencv-python pin deferred — gtsam/numpy ABI block **Recorded**: 2026-05-11T02:55+03:00 (Europe/Kyiv) +**Last replay attempt**: 2026-05-13T23:09+03:00 (Europe/Kyiv) — PyPI shows +`gtsam==4.2.1` as the latest release; `requires_dist: numpy<2.0.0,>=1.11.0`. +Replay condition (numpy>=2 wheels) still NOT met. Leftover remains open. **Status**: deferred-non-user (replay when upstream gtsam wheels target numpy>=2) ## What is blocked diff --git a/src/gps_denied_onboard/components/c6_tile_cache/_timestamp.py b/src/gps_denied_onboard/components/c6_tile_cache/_timestamp.py deleted file mode 100644 index 9b7ea52..0000000 --- a/src/gps_denied_onboard/components/c6_tile_cache/_timestamp.py +++ /dev/null @@ -1,21 +0,0 @@ -"""RFC 3339 UTC timestamp helper shared inside the c6_tile_cache component. - -Single source for the FDR record envelope ``ts`` field across -``postgres_filesystem_store``, ``freshness_gate``, and -``cache_budget_enforcer`` — formerly a triplicate ``_iso_ts_now`` per -module (`cumulative_review_batches_28-30` F3). The format is wall-clock -metadata about WHEN the FDR record was emitted; producers that need a -Clock-driven payload field (e.g., ``age_seconds`` derived from an -injected clock) MUST NOT route through this helper. -""" - -from __future__ import annotations - -from datetime import datetime, timezone - -__all__ = ["iso_ts_now"] - - -def iso_ts_now() -> str: - """Return an RFC 3339 UTC timestamp with microsecond precision and a ``Z`` suffix.""" - return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ") diff --git a/src/gps_denied_onboard/components/c6_tile_cache/cache_budget_enforcer.py b/src/gps_denied_onboard/components/c6_tile_cache/cache_budget_enforcer.py index 7af2d01..077e6d4 100644 --- a/src/gps_denied_onboard/components/c6_tile_cache/cache_budget_enforcer.py +++ b/src/gps_denied_onboard/components/c6_tile_cache/cache_budget_enforcer.py @@ -34,7 +34,6 @@ from dataclasses import dataclass from datetime import datetime, timezone from typing import TYPE_CHECKING, Final -from gps_denied_onboard.components.c6_tile_cache._timestamp import iso_ts_now from gps_denied_onboard.components.c6_tile_cache._types import ( TileId, TileMetadata, @@ -50,6 +49,7 @@ from gps_denied_onboard.components.c6_tile_cache.interface import ( TileStore, ) from gps_denied_onboard.fdr_client.records import CURRENT_SCHEMA_VERSION, FdrRecord +from gps_denied_onboard.helpers.iso_timestamps import iso_ts_now if TYPE_CHECKING: from gps_denied_onboard.components.c6_tile_cache._tile_pixel_handle import ( diff --git a/src/gps_denied_onboard/components/c6_tile_cache/freshness_gate.py b/src/gps_denied_onboard/components/c6_tile_cache/freshness_gate.py index 3c8593e..a303168 100644 --- a/src/gps_denied_onboard/components/c6_tile_cache/freshness_gate.py +++ b/src/gps_denied_onboard/components/c6_tile_cache/freshness_gate.py @@ -47,7 +47,6 @@ from typing import TYPE_CHECKING, Any, Final import psycopg from psycopg_pool import ConnectionPool -from gps_denied_onboard.components.c6_tile_cache._timestamp import iso_ts_now from gps_denied_onboard.components.c6_tile_cache._types import ( Bbox, FreshnessLabel, @@ -61,6 +60,7 @@ from gps_denied_onboard.components.c6_tile_cache.errors import ( ) from gps_denied_onboard.config.schema import ConfigError from gps_denied_onboard.fdr_client.records import CURRENT_SCHEMA_VERSION, FdrRecord +from gps_denied_onboard.helpers.iso_timestamps import iso_ts_now if TYPE_CHECKING: from gps_denied_onboard.clock.interface import Clock diff --git a/src/gps_denied_onboard/components/c6_tile_cache/postgres_filesystem_store.py b/src/gps_denied_onboard/components/c6_tile_cache/postgres_filesystem_store.py index a4da995..e40ea67 100644 --- a/src/gps_denied_onboard/components/c6_tile_cache/postgres_filesystem_store.py +++ b/src/gps_denied_onboard/components/c6_tile_cache/postgres_filesystem_store.py @@ -48,7 +48,6 @@ from psycopg_pool import ConnectionPool from gps_denied_onboard.components.c6_tile_cache._tile_pixel_handle import ( TilePixelHandle, ) -from gps_denied_onboard.components.c6_tile_cache._timestamp import iso_ts_now from gps_denied_onboard.components.c6_tile_cache._types import ( Bbox, FreshnessLabel, @@ -76,6 +75,7 @@ from gps_denied_onboard.fdr_client.records import ( CURRENT_SCHEMA_VERSION, FdrRecord, ) +from gps_denied_onboard.helpers.iso_timestamps import iso_ts_now from gps_denied_onboard.helpers.sha256_sidecar import ( SIDECAR_SUFFIX, Sha256Sidecar, diff --git a/src/gps_denied_onboard/components/c7_inference/onnx_trt_ep_runtime.py b/src/gps_denied_onboard/components/c7_inference/onnx_trt_ep_runtime.py index 7192ede..b9e4bfb 100644 --- a/src/gps_denied_onboard/components/c7_inference/onnx_trt_ep_runtime.py +++ b/src/gps_denied_onboard/components/c7_inference/onnx_trt_ep_runtime.py @@ -46,7 +46,6 @@ from __future__ import annotations import hashlib import os from collections.abc import Callable -from datetime import datetime, timezone from pathlib import Path from typing import TYPE_CHECKING, Any, Final, Literal @@ -79,6 +78,7 @@ from gps_denied_onboard.fdr_client.records import ( CURRENT_SCHEMA_VERSION, FdrRecord, ) +from gps_denied_onboard.helpers.iso_timestamps import iso_ts_now as _iso_ts_now from gps_denied_onboard.logging import get_logger if TYPE_CHECKING: @@ -166,18 +166,6 @@ def _sha256_of_file(path: Path) -> str: return digest.hexdigest() -def _iso_ts_now() -> str: - """RFC 3339 UTC timestamp with microsecond precision and a ``Z`` suffix. - - Mirrors :func:`components.c6_tile_cache._timestamp.iso_ts_now` — - consolidation into ``helpers.iso_timestamp`` is intentionally - deferred to the next cross-component hygiene pass (peer imports - between c6 and c7 would violate layer-2 horizontal-import etiquette - documented in ``module-layout.md``). - """ - return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ") - - # ---------------------------------------------------------------------- # Runtime. diff --git a/src/gps_denied_onboard/components/c7_inference/thermal_publisher.py b/src/gps_denied_onboard/components/c7_inference/thermal_publisher.py index c77d9f1..8608b28 100644 --- a/src/gps_denied_onboard/components/c7_inference/thermal_publisher.py +++ b/src/gps_denied_onboard/components/c7_inference/thermal_publisher.py @@ -53,7 +53,6 @@ from __future__ import annotations import threading from dataclasses import dataclass -from datetime import datetime, timezone from typing import TYPE_CHECKING, Protocol, runtime_checkable from gps_denied_onboard._types.thermal import ThermalState @@ -65,6 +64,7 @@ from gps_denied_onboard.fdr_client.records import ( CURRENT_SCHEMA_VERSION, FdrRecord, ) +from gps_denied_onboard.helpers.iso_timestamps import iso_ts_now as _iso_ts_now from gps_denied_onboard.logging import get_logger if TYPE_CHECKING: @@ -340,11 +340,6 @@ def _default_safe_snapshot(measured_at_ns: int) -> ThermalState: ) -def _iso_ts_now() -> str: - """RFC 3339 UTC timestamp with microsecond precision and ``Z`` suffix.""" - return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ") - - class _JtopSource: """``jtop`` (jetson-stats) thermal source — Tier-2 production path. diff --git a/src/gps_denied_onboard/helpers/__init__.py b/src/gps_denied_onboard/helpers/__init__.py index 1ac683b..c28f36e 100644 --- a/src/gps_denied_onboard/helpers/__init__.py +++ b/src/gps_denied_onboard/helpers/__init__.py @@ -22,6 +22,7 @@ from gps_denied_onboard.helpers.imu_preintegrator import ( ImuPreintegrator, make_imu_preintegrator, ) +from gps_denied_onboard.helpers.iso_timestamps import iso_ts_now from gps_denied_onboard.helpers.lightglue_runtime import ( LightGlueConcurrentAccessError, LightGlueRuntime, @@ -83,6 +84,7 @@ __all__ = [ "adjoint", "exp_map", "is_valid_rotation", + "iso_ts_now", "log_map", "make_imu_preintegrator", "matrix_to_se3", diff --git a/src/gps_denied_onboard/helpers/iso_timestamps.py b/src/gps_denied_onboard/helpers/iso_timestamps.py new file mode 100644 index 0000000..436d0e0 --- /dev/null +++ b/src/gps_denied_onboard/helpers/iso_timestamps.py @@ -0,0 +1,33 @@ +"""UTC ISO-8601 timestamp helper (E-CC-HELPERS / AZ-264 / AZ-508). + +Single Layer-1 source for the wall-clock string used in the FDR record +``ts`` envelope across c6_tile_cache and c7_inference. Consolidates what +used to be a private ``_iso_ts_now`` one-liner repeated per module. + +The output format is locked to RFC 3339 / ISO 8601 UTC with microsecond +precision and a ``Z`` suffix, matching the canonical FDR ``ts`` fixture +in ``tests/unit/test_az272_fdr_record_schema.py`` (``_TS = +"2026-05-11T00:00:00.000000Z"``) and the format produced by the three +existing local helpers this module replaces. Changing the format would +alter FDR record bit-shape and is explicitly out of scope for AZ-508. + +Producers that need a Clock-injected payload field (e.g. +``age_seconds`` derived from an injected wall-clock for testability) +MUST NOT route through this helper — it is purely metadata about WHEN +the FDR record envelope itself was emitted. +""" + +from __future__ import annotations + +from datetime import datetime, timezone + +__all__ = ["iso_ts_now"] + + +def iso_ts_now() -> str: + """Return an RFC 3339 UTC timestamp with microsecond precision and a ``Z`` suffix. + + Format: ``YYYY-MM-DDTHH:MM:SS.ffffffZ`` (fixed-width, lexicographically + monotonic under a non-decreasing wall clock). + """ + return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ") diff --git a/tests/unit/test_az508_iso_timestamps.py b/tests/unit/test_az508_iso_timestamps.py new file mode 100644 index 0000000..2281fd2 --- /dev/null +++ b/tests/unit/test_az508_iso_timestamps.py @@ -0,0 +1,191 @@ +"""AC tests for AZ-508: ISO-timestamp helper consolidation. + +Verifies the `iso_timestamps` helper exposed at +`gps_denied_onboard.helpers.iso_timestamps.iso_ts_now` — the single +Layer-1 source for FDR record envelope timestamps that replaced the +duplicated `_iso_ts_now` one-liners in c6_tile_cache and c7_inference. + +Output contract (matches the canonical FDR `_TS` fixture in +`tests/unit/test_az272_fdr_record_schema.py`): + + YYYY-MM-DDTHH:MM:SS.ffffffZ (UTC, microsecond precision, ``Z`` suffix) +""" + +from __future__ import annotations + +import ast +import re +import time +from datetime import datetime, timezone +from pathlib import Path + +import pytest + +from gps_denied_onboard.helpers import iso_ts_now +from gps_denied_onboard.helpers.iso_timestamps import iso_ts_now as iso_ts_now_direct + +_TS_REGEX: re.Pattern[str] = re.compile( + r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z$" +) + +_REPO_ROOT: Path = Path(__file__).resolve().parents[2] +_HELPER_PATH: Path = ( + _REPO_ROOT / "src" / "gps_denied_onboard" / "helpers" / "iso_timestamps.py" +) +_C6_DIR: Path = ( + _REPO_ROOT / "src" / "gps_denied_onboard" / "components" / "c6_tile_cache" +) +_C7_DIR: Path = ( + _REPO_ROOT / "src" / "gps_denied_onboard" / "components" / "c7_inference" +) + + +def test_ac1_import_and_call_returns_str() -> None: + # Act + value = iso_ts_now() + # Assert + assert isinstance(value, str) + assert value, "iso_ts_now() returned an empty string" + # Both the package-level and module-level imports must resolve to the + # same callable so consumers can reach it either way. + assert iso_ts_now is iso_ts_now_direct + + +def test_ac2_format_matches_canonical_regex() -> None: + # Act + value = iso_ts_now() + # Assert + assert _TS_REGEX.fullmatch(value), ( + f"{value!r} does not match the canonical FDR ts format " + f"YYYY-MM-DDTHH:MM:SS.ffffffZ" + ) + + +def test_ac2_fromisoformat_roundtrip_yields_utc_aware_datetime() -> None: + # Arrange + value = iso_ts_now() + iso_with_offset = value.replace("Z", "+00:00") + + # Act + parsed = datetime.fromisoformat(iso_with_offset) + + # Assert + assert parsed.tzinfo is not None + assert parsed.utcoffset() == timezone.utc.utcoffset(parsed) + + +def test_ac3_two_successive_calls_are_non_decreasing() -> None: + # Arrange / Act + a = iso_ts_now() + time.sleep(0.000_005) + b = iso_ts_now() + + # Assert (lexicographic comparison is correct for the fixed-width format) + assert b >= a, f"expected {b!r} >= {a!r}" + + +def test_ac4_no_other_iso_ts_now_definition_exists_in_src() -> None: + """AC-4: a `def _iso_ts_now` / `def iso_ts_now` MUST exist only inside + `helpers/iso_timestamps.py`. Any other definition under `src/` means a + consumer slipped a copy back in. + """ + # Arrange + src_root = _REPO_ROOT / "src" + offenders: list[tuple[Path, str]] = [] + + # Act + for path in src_root.rglob("*.py"): + if path == _HELPER_PATH: + continue + try: + tree = ast.parse(path.read_text(encoding="utf-8")) + except SyntaxError: + continue + for node in ast.walk(tree): + if isinstance(node, ast.FunctionDef) and node.name in { + "iso_ts_now", + "_iso_ts_now", + }: + offenders.append((path.relative_to(_REPO_ROOT), node.name)) + + # Assert + assert offenders == [], ( + f"Found stray `iso_ts_now` definitions outside the helper: {offenders}" + ) + + +def test_ac4_c6_and_c7_callers_import_from_helpers() -> None: + """The 3 migrated call-sites must import from `helpers.iso_timestamps` + (directly or via the `helpers` package facade) so future hygiene + cycles can rely on the single source of truth. + """ + # Arrange + callers = [ + _C6_DIR / "cache_budget_enforcer.py", + _C6_DIR / "postgres_filesystem_store.py", + _C6_DIR / "freshness_gate.py", + _C7_DIR / "onnx_trt_ep_runtime.py", + _C7_DIR / "thermal_publisher.py", + ] + expected_token = "gps_denied_onboard.helpers.iso_timestamps" + + # Act / Assert + for caller in callers: + text = caller.read_text(encoding="utf-8") + assert expected_token in text, ( + f"{caller.relative_to(_REPO_ROOT)} does not import " + f"`iso_ts_now` from `{expected_token}`" + ) + assert "def _iso_ts_now" not in text, ( + f"{caller.relative_to(_REPO_ROOT)} still defines a local " + "_iso_ts_now (consolidation incomplete)" + ) + + +def test_ac6_helper_uses_stdlib_only() -> None: + """AC-6: no third-party imports inside the helper module.""" + # Arrange + tree = ast.parse(_HELPER_PATH.read_text(encoding="utf-8")) + allowed_stdlib = {"datetime", "__future__"} + + # Act + imports: list[str] = [] + for node in ast.walk(tree): + if isinstance(node, ast.Import): + imports.extend(alias.name for alias in node.names) + elif isinstance(node, ast.ImportFrom): + if node.module is not None: + imports.append(node.module) + + # Assert + for name in imports: + top = name.split(".")[0] + assert top in allowed_stdlib, ( + f"helpers/iso_timestamps.py imports `{name}`; only stdlib " + f"({sorted(allowed_stdlib)}) is allowed by AC-6" + ) + + +def test_helper_is_layer_1_no_component_imports() -> None: + """Layer-1 discipline: the helper MUST NOT import from any component. + (Constraint § Layer-1 discipline in the AZ-508 task spec.) + """ + # Arrange + text = _HELPER_PATH.read_text(encoding="utf-8") + + # Assert + assert "gps_denied_onboard.components" not in text, ( + "helpers/iso_timestamps.py imports from a component — Layer-1 " + "discipline violated" + ) + + +@pytest.mark.parametrize("expected_field", ["iso_ts_now"]) +def test_helper_public_surface_is_minimal(expected_field: str) -> None: + """Defensive: only ``iso_ts_now`` is re-exported from the module.""" + # Arrange + import gps_denied_onboard.helpers.iso_timestamps as module + + # Assert + assert expected_field in module.__all__ + assert module.__all__ == ["iso_ts_now"]