# Track Specification: metadata_generational_handle_20260624 ## Overview Child 2 of the `metadata_ssdl_defusing_20260624` campaign. Wraps `Metadata` in a `(index, generation)` handle resolved through a registry. Collapses lifetime branches to 1 lookup + 1 generation comparison. **BLOCKED_BY child 1** (the nil sentinel is the fallback path on generation mismatch). ## Current State Audit (master @ child-1-SHIPPED, after metadata_nil_sentinel_20260624) - `NIL_METADATA` sentinel exists (from child 1) - The 6 nil-check functions use sentinel-return - The 3466 branch points in the parent audit include lifetime checks (e.g., "is this handle still valid?") - `src/aggregate.py` and `src/ai_client.py` contain lifetime checks; they should be replaced with handle lookup + generation comparison ## Goals | ID | Goal | Acceptance | |---|---|---| | G1 | `MetadataHandle` (or equivalent) introduced: a `(index: int, generation: int)` pair | The handle type is exported; can be created and queried | | G2 | `MetadataHandleRegistry` (or equivalent) introduced: stores `index -> generation` mapping | The registry has O(1) lookup, bump-generation, and get-with-validation methods | | G3 | Production `Metadata` is wrapped in a handle at the consumer entry points | Consumers can do `handle.registry_lookup()` instead of `if entry.lifetime != current_lifetime: ...` | | G4 | 1 behavioral test for the handle | `tests/test_metadata_generational_handle.py` exists; asserts lookup, generation mismatch returns `NIL_METADATA`, bump invalidates cached lookups | | G5 | Budget gate met: effective-codepaths drop ≥ 20% vs post-child-1 measurement | Re-measurement shows the drop | ## Non-Goals - Touching the 4 other `dict[str, Any]` aliases — out of scope (deferred to follow-up campaign) - Touching the list-typed aggregates — out of scope - Replacing the 3 candidate placeholders — blocked on `any_type_componentization_20260621` - Adding a generational handle for the inner Metadata values within nested structures (the campaign handles the top-level Metadata; nested handles are out of scope) - Cache invalidation (that's child 3; this child just provides the identity) ## Functional Requirements ### FR1: Handle + Registry types In a sensible location (likely `src/aggregate.py` per AGENTS.md §File Naming Convention, OR a NEW module if the user explicitly approves): ```python @dataclass(frozen=True) class MetadataHandle: index: int generation: int class MetadataHandleRegistry: def __init__(self) -> None: self._generations: dict[int, int] = {} def register(self, metadata: Metadata) -> MetadataHandle: # returns a new handle with a fresh index + generation def lookup(self, handle: MetadataHandle) -> Metadata | None: # if handle.generation != self._generations[handle.index], return None # otherwise return the stored Metadata (or sentinel for now) def bump_generation(self, index: int) -> None: # invalidate the cached entry for this index ``` (Exact API up to Tier 2; the contract is: handle + registry with O(1) lookup + generation-based invalidation.) ### FR2: Migrate lifetime-branch code For each site in `src/` that does lifetime checks (e.g., "is this Metadata still the one I cached?"): - Replace with `handle = registry.register(metadata)` + `value = registry.lookup(handle)` + `if value is None: use NIL_METADATA` ### FR3: Behavioral test `tests/test_metadata_generational_handle.py` with at least 3 tests: - `test_register_returns_handle`: assert `registry.register(metadata)` returns a `MetadataHandle` with distinct `index` and `generation` - `test_lookup_returns_none_after_bump`: assert `registry.lookup(handle)` returns None after `registry.bump_generation(handle.index)` - `test_lookup_returns_none_for_unknown_index`: assert `registry.lookup(MetadataHandle(index=999, generation=1))` returns None - `test_lookup_returns_value_for_valid_handle`: assert `registry.lookup(handle)` returns the registered Metadata ## Non-Functional Requirements - NFR1: 1-space indentation - NFR2: CRLF line endings on Windows - NFR3: No comments in source code - NFR4: Per-task atomic commits with git notes - NFR5: No new pip dependencies - NFR6: `Result[T]` returns for fallible registry methods (per `conductor/code_styleguides/error_handling.md`) - NFR7: No new `src/.py` files (per AGENTS.md) — unless the user explicitly approves; default is to put `MetadataHandle` + `MetadataHandleRegistry` in `src/aggregate.py` or another existing module ## Architecture Reference - `src/aggregate.py` (the parent module for `Metadata`) - `NIL_METADATA` (from child 1) — the fallback returned by `lookup` on generation mismatch - `docs/reports/code_path_audit/2026-06-22/AUDIT_REPORT.md` Finding 1 Fix 3 — the Generational Handle proposal - `conductor/code_styleguides/data_oriented_design.md` — canonical DOD reference ## Out of Scope - The 4 other `dict[str, Any]` aliases (deferred) - The 3 candidate placeholders (blocked) - The cache (child 3) - Runtime profiling (Track F from the previous menu; deferred) ## Verification Criteria (Definition of Done) | # | Criterion | Verification command | |---|---|---| | VC1 | `MetadataHandle` and `MetadataHandleRegistry` exist | `grep -rn "class MetadataHandle\|class MetadataHandleRegistry" src/` | | VC2 | Production code uses handle + registry at the entry points | `grep -rn "registry.lookup\|registry.register" src/` returns ≥ 1 hit | | VC3 | Behavioral test exists and passes | `uv run pytest tests/test_metadata_generational_handle.py -v` | | VC4 | Budget gate met | `compute_effective_codepaths(Metadata_profile)` returns number ≥ 20% smaller than post-child-1 measurement | | VC5 | Full test suite remains green | `uv run python scripts/run_tests_batched.py` → 11/11 tiers PASS | | VC6 | 4 audit gates remain clean | weak_types ≤ 112, type_registry in sync, main_thread_imports clean, no_models_config_io clean | ## Risks | # | Risk | Likelihood | Mitigation | |---|---|---|---| | R1 | The handle breaks code that expects raw `Metadata` | medium | The handle is a wrapper; consumers can extract the raw value via `.value` or similar. Behavioral test verifies backwards-compat for the common cases. | | R2 | The registry's lookup is not actually O(1) | low | The registry uses a `dict[int, int]` for `index -> generation`; lookup is O(1) by construction. | | R3 | Budget gate fails (drop < 20%) | low | The 3466 branch points include lifetime checks; replacing with handle lookup should drop the count. If not, the SSDL math is wrong. |