PR #305 Review Resolution Plan
Context: A prior code review of PR #305 (specs 046, 048, 054) identified two critical and several major issues. This document records the agreed resolution approach and the concrete implementation plan for both tracks.
Branch: feature/agent-profile-implementation (057)
Date: 2026-03-25
Architect: Architect Alphonso (profile: architect)
Issues to Resolve
| ID | Issue | Severity | Track |
|---|---|---|---|
| C1 | doctrine imports specify_cli — violates stated dependency direction |
Critical | Track 2 |
| C2 | CI coverage/mypy does not include src/doctrine/ |
Critical | Track 1 |
| M1 | 8 bare except Exception: in _default_shipped_dir() methods |
Major | Track 1 |
| M2 | _PLURALS/_PATTERNS dicts re-allocated on every property access |
Major | Track 1 |
| M3 | resolve_doctrine_root() fallback chain has no logging |
Minor | Track 1 |
| M4 | except Exception: pass in context.py YAML/JSON parsers |
Major | Track 1 |
| 241 | --mission naming collision in create-mission command |
Design gap | Track 1 |
Track 1 — Boyscouting PR
Form: Single PR, 6 independent commits. No mission spec required.
Commit 1 — CI: add src/doctrine to mypy and missing coverage jobs
File: .github/workflows/ci-quality.yml
- Mypy invocation (line ~268): add
src/doctrine# Before python -m mypy --strict src/specify_cli src/charter # After python -m mypy --strict src/specify_cli src/charter src/doctrine - Integration, slow, and e2e test steps: add
--cov=src/doctrine - Fast tests already include
--cov=src/doctrine— no change needed
Commit 2 — M1: narrow except Exception in _default_shipped_dir() (8 files)
Files (identical change in all 8):
src/doctrine/agent_profiles/repository.py
src/doctrine/directives/repository.py
src/doctrine/mission_step_contracts/repository.py
src/doctrine/paradigms/repository.py
src/doctrine/procedures/repository.py
src/doctrine/styleguides/repository.py
src/doctrine/tactics/repository.py
src/doctrine/toolguides/repository.py
Change: Replace except Exception: with except (ModuleNotFoundError, TypeError): in each
_default_shipped_dir() method. Matches the same reasoning used in resolve_doctrine_root():
importlib.resources.files() raises exactly these two types on failure.
Commit 3 — M2: hoist _PLURALS/_PATTERNS to module-level constants
File: src/doctrine/artifact_kinds.py
Both dicts are currently re-created on every property access. Move them to module-level constants above the class. Properties become single-line lookups. No logic change.
# Before (inside properties — re-allocated on every call)
@property
def plural(self) -> str:
_PLURALS: dict[str, str] = {"directive": "directives", ...}
return _PLURALS[self.value]
# After (module-level — allocated once)
_PLURALS: dict[str, str] = {"directive": "directives", ...}
_PATTERNS: dict[str, str] = {"directive": "*.directive.yaml", ...}
class ArtifactKind(StrEnum):
@property
def plural(self) -> str:
return _PLURALS[self.value]
Commit 4 — M3: add logging.debug() to resolve_doctrine_root() fallback chain
Files: src/specify_cli/charter/catalog.py and src/charter/catalog.py
Add import logging and log = logging.getLogger(__name__). Insert debug calls at each
fallback step:
# After importlib attempt fails:
log.debug("doctrine: importlib.resources lookup failed, trying dev layout")
# After dev layout succeeds:
log.debug("doctrine: resolved via dev layout at %s", dev_root)
# After package asset fallback (charter/catalog.py only):
log.debug("doctrine: resolved via package asset root fallback")
Commit 5 — M4: narrow except Exception: in context.py YAML/JSON parsers
Files: src/specify_cli/charter/context.py and src/charter/context.py
Two locations in each file:
| Location | Guard target | Replace with |
|---|---|---|
_load_references — YAML parse |
ruamel.yaml parse + file read | except (YAMLError, UnicodeDecodeError, OSError): |
_load_state — JSON parse |
json.loads + file read |
except (json.JSONDecodeError, UnicodeDecodeError, OSError): |
Add from ruamel.yaml import YAMLError to import blocks. json is already imported.
Commit 6 — #241: rename create-mission --mission to --mission-type
File: src/specify_cli/cli/commands/agent/feature.py
The create-mission command uses --mission to mean "mission type" (e.g., software-dev,
documentation). All other commands use --mission for "feature slug". This is the surviving
naming collision from issue #241.
Change:
- Add
--mission-typeas canonical parameter - Keep
--missionashidden=Truedeprecated alias, emitting the same deprecation warning pattern established in WP01 - Add
_resolve_mission_type(mission_type, mission)helper in_flag_utils.pyalongside the existingresolve_mission_or_feature()
Track 2 — Architectural Fix PR
Form: Targeted PR with ADR. No full mission spec. Sequential commits.
Problem: src/doctrine/missions/primitives.py and glossary_hook.py import from
specify_cli.glossary.*, violating the rule that doctrine has no dependency on
specify_cli. The two files need different solutions:
| File | Problem | Solution |
|---|---|---|
primitives.py |
Imports 4 data types from specify_cli.glossary.* |
Option A: move types to doctrine/shared/ |
glossary_hook.py |
Imports GlossaryAwarePrimitiveRunner — a rich orchestration class that cannot move to doctrine |
Lazy imports: defer specify_cli import to call time |
Decision: Option A (move types into doctrine). src/kernel/ was available as a fallback
but is not needed — these types have natural ownership in doctrine.
Step 0 — Write the ADR (first commit)
File: docs/adr/2.x/2026-03-XX-glossary-type-ownership.md
Documents:
- Glossary primitive types are owned by
doctrine, re-exported byspecify_cli glossary_hook.pyremains indoctrine/missions/as an intentional bridge but defers specify_cli imports to call time — the only sanctioned use of lazy cross-boundary imports- Rejected: moving
GlossaryAwarePrimitiveRunnerto doctrine (too deeply coupled to specify_cli middleware) src/kernel/was available (Option C) but not needed for these types
Step 1 — Create src/doctrine/shared/glossary_types.py
Collect all types in dependency order. All dependencies are stdlib only.
# src/doctrine/shared/glossary_types.py
"""Glossary primitive types owned by doctrine, consumed by specify_cli."""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import StrEnum
# Moved from specify_cli.glossary.scope
class GlossaryScope(StrEnum): ...
# Moved from specify_cli.glossary.strictness
class Strictness(StrEnum):
OFF = "off"
MEDIUM = "medium"
MAX = "max"
# Moved from specify_cli.glossary.extraction
@dataclass(frozen=True)
class ExtractedTerm:
surface: str
source: str
confidence: float
original: str = ""
# Moved from specify_cli.glossary.models (nested types first)
@dataclass(frozen=True)
class TermSurface: ...
class ConflictType(StrEnum): ...
class Severity(StrEnum): ...
@dataclass
class SenseRef: ...
@dataclass
class SemanticConflict: ...
# Moved from specify_cli.glossary.checkpoint
@dataclass(frozen=True)
class ScopeRef:
scope: GlossaryScope
version_id: str
Update src/doctrine/shared/__init__.py to re-export all new types.
No changes to src/doctrine/pyproject.toml — all stdlib, no new dependencies.
Step 2 — Update doctrine/missions/primitives.py imports
# Before (4 specify_cli imports)
from specify_cli.glossary.checkpoint import ScopeRef
from specify_cli.glossary.extraction import ExtractedTerm
from specify_cli.glossary.models import SemanticConflict
from specify_cli.glossary.strictness import Strictness
# After (single doctrine import)
from doctrine.shared.glossary_types import (
ExtractedTerm,
SemanticConflict,
ScopeRef,
Strictness,
)
No other changes to the file body.
Step 3 — Convert doctrine/missions/glossary_hook.py to lazy imports
GlossaryAwarePrimitiveRunner and read_glossary_check_metadata orchestrate specify_cli's
full glossary pipeline and cannot move to doctrine. Move their imports inside the function body:
# Module level — OK after Step 2
from doctrine.shared.glossary_types import Strictness
def execute_with_glossary(
...,
runtime_strictness: Strictness | None = None,
...,
) -> Any:
# Lazy import: specify_cli dependency deferred to call time (see ADR-XX)
from specify_cli.glossary.attachment import (
GlossaryAwarePrimitiveRunner,
read_glossary_check_metadata,
)
...
Add module docstring: """Bridge module: defers specify_cli imports to call time. See ADR-XX."""
Step 4 — Add backward-compat re-exports in specify_cli/glossary/
The 30–40 usages of these types throughout specify_cli.glossary.* must continue to work
without any call-site changes. Replace each class definition with a re-export:
| File | Change |
|---|---|
specify_cli/glossary/strictness.py |
Replace Strictness class def with from doctrine.shared.glossary_types import Strictness |
specify_cli/glossary/extraction.py |
Replace ExtractedTerm class def with re-export |
specify_cli/glossary/models.py |
Replace TermSurface, ConflictType, Severity, SenseRef, SemanticConflict with re-exports |
specify_cli/glossary/scope.py |
Replace GlossaryScope class def with re-export |
specify_cli/glossary/checkpoint.py |
Replace ScopeRef class def with re-export |
All existing from specify_cli.glossary.X import Y call sites continue to work unchanged.
Step 5 — Boundary isolation test
Add tests/doctrine/test_isolation.py:
"""Verify doctrine can be imported without pulling in specify_cli."""
import subprocess
import sys
def test_doctrine_does_not_import_specify_cli(tmp_path):
"""doctrine.missions.primitives must import cleanly without specify_cli on PYTHONPATH."""
src_root = Path(__file__).parents[2] / "src"
result = subprocess.run(
[sys.executable, "-c", "import doctrine; import doctrine.missions.primitives"],
env={
**os.environ,
"PYTHONPATH": str(src_root / "doctrine") + os.pathsep + str(src_root / "kernel"),
},
capture_output=True,
text=True,
)
assert result.returncode == 0, (
f"doctrine imported specify_cli at module load time:\n{result.stderr}"
)
Commit Sequence for Track 2
1. docs: ADR — glossary type ownership (doctrine owns, specify_cli re-exports)
2. feat(doctrine): create doctrine/shared/glossary_types.py with moved types
3. fix(doctrine): update doctrine/shared/__init__.py exports
4. fix(doctrine): update primitives.py to import from doctrine.shared
5. fix(doctrine): convert glossary_hook.py to lazy specify_cli imports
6. fix(specify_cli): add re-exports in glossary/{strictness,extraction,models,scope,checkpoint}.py
7. test: add doctrine isolation test (import without specify_cli)
Each commit must stay green: ruff check, mypy --strict, pytest tests/doctrine/.
Sequencing Between Tracks
Track 1 (boyscouting PR)
└── merge to mission branch
Track 2 (architectural fix PR)
└── merge to mission branch
PR #305 ready for final review → merge to develop
Track 1 and Track 2 are independent of each other and can be worked in parallel. Neither blocks the other. Both must land before PR #305 moves out of draft.
What This Does NOT Address
- #327 — Doctrine-mission compiler: Separate feature, backlog after PR #305 merges.
- Skills/doctrine mission boundary: Part of #327 planning.
charter/compiler.pyandinterview.pytest coverage gap: Flagged for a follow-on spec; not a PR #305 blocker.