Skip to content

S1-05 attempt log — ADR-0033 identifier newtypes

Append-only journal. Newest attempt at the bottom.

Attempt 1 — 2026-05-15 — GREEN on first pass

Outcome: All 9 ACs pass. Full suite 1605 passed, ruff clean, mypy --strict clean, coverage 93.39%.

ReAct trace (compressed)

  1. Thought. Story expects PackageManager to be importable from codegenie.probes.node_build_system (or its layer_a/ future home). Grep shows neither location exposes a PackageManager name — the field is typed str | None throughout. The story's Out-of-Scope §"Enum-shaped PackageManager rewrite" forbids rewriting to an Enum, and Notes §"Phase 1 ADR-0013 location matters" allows the import path to adapt to whichever Phase 1 currently ships.
  2. Action. Add a minimal PackageManager: TypeAlias = Literal["bun","pnpm","yarn-classic","yarn-berry","npm"] at the top of src/codegenie/probes/node_build_system.py (between the logger init and the existing Open/Closed seam tuples). Literal values mirror the JSON schema enum at src/codegenie/schema/probes/node_build_system.schema.json:29 (minus null, which the field-level optionality expresses as PackageManager | None).
  3. Observation. Existing signatures (package_manager: str | None) are unchanged — Literal[...] | None is a structural subtype of str | None, so no downstream drift. mypy --strict on src/codegenie/probes/node_build_system.py PASS.
  4. Thought. Write the seven runtime guards from the story's TDD plan; the typecheck file is also collected by pytest but contains no test functions.
  5. Action. tests/unit/types/test_identifiers.py + tests/unit/types/test_identifiers_typecheck.py land before any source change.
  6. Observation. pytest tests/unit/types/ errors with ModuleNotFoundError: No module named 'codegenie.types.identifiers'. RED confirmed.
  7. Action. Land src/codegenie/types/__init__.py + src/codegenie/types/identifiers.py per the story's GREEN block. Use explicit as PackageManager re-export idiom for mypy --strict.
  8. Observation. Tests pass. mypy --strict initially flagged the except ImportError fallback (layer_a.node_build_system doesn't exist yet) — added # type: ignore[no-redef,import-untyped]. Clean.

Precondition surfaced

PackageManager did not exist as a typed alias before this story; the story implicitly assumed Phase 1 already shipped it. The fix is surgical (one constant added; no signature edits) and consistent with Phase 1 ADR-0013's enumerated variants. If Phase 1 wants to amend ADR-0013 to formalize the type alias, that's a Phase 1 follow-up; this story does not edit ADR-0013.

Test naming-collision note

tests/unit/types/ is a leaf package with types as its name — collides with the stdlib types module when pytest auto-creates an __init__.py-less package. The fix that worked: keep tests/unit/types/ without __init__.py so pytest treats it via its rootdir-relative import without registering a types.test_identifiers import path. Other test subpackages (indices, probes, etc.) have __init__.py because their names don't collide with stdlib.

Refactor decisions (design-patterns checklist)

  • Newtype pattern (already prescribed). ✓ Four NewTypes; nominal under mypy, identity-at-runtime.
  • Re-export by import (production ADR-0033 rule).from ... import PackageManager as PackageManager — single source of truth at Phase 1 ADR-0013's owning module.
  • No Pydantic / Enum / dataclass rewrite. ✓ Out-of-scope per the story; primitive obsession solved with the lightest possible mechanism.
  • No premature kernel-extract. ✓ No shared Identifier[T] base; production ADR-0033 says each newtype lives where its consumer family lives, and Rule 2 (simplicity first) wins until friction shows.

Lessons (cross-story)

See _lessons.md — added: "When a Phase 2 story imports a Phase-1-owned name that doesn't actually exist yet, the right surgical fix is a one-line type alias in the owning Phase-1 module, not a redefinition under codegenie.types/."