Research — Built-In Doctrine Seam Consolidation (Phase 0)

Consolidated from the 4-facet research squad + architecture design pass. The full briefs are in notes/research-synthesis.md; the PR #3117 CI-failure ownership split is in notes/pr3117-ci-failures.txt; the source issues are in notes/source-issues.txt. This file records the design decisions that resolve every open question for the plan.

Decision 1 — One built-in-location authority (built_in_dir(kind))

returning resolve_pack_root("built-in") / kind.plural; route the 9 repository _default_built_in_dir() and the inline charter join sites through it.

is hand-written at ~14. Deriving from ArtifactKind.plural (the existing SSOT) collapses them to one and kills the hardcoded-plural drift class. pack_paths importing artifact_kinds is safe (leaf, no back-import).

closed PR proved unbounded); a helper in each repo base class (still N copies of the plural).

  • Decision: add built_in_dir(kind: ArtifactKind) -> Path to src/doctrine/pack_paths.py,
  • Rationale: built-in location is currently re-derived at ~25 sites via 5 mechanisms; the plural
  • Alternatives considered: leave per-repo joins (status quo — the whack-a-field surface the

Decision 2 — Drop the fail-open built_in_root param

self-resolve via Decision 1. Synthetic test tiers use the SPEC_KITTY_PACKS_ROOT env override.

a real root silently loads zero artefacts. Redefining it as a "flat override that raises" keeps a second resolution path alive (the exact anti-pattern). Dropping it makes the wrong shape unconstructable. Sizing: only 20 files pass built_in_root; ~14 are coupled churn (6 already None, ~5 real-repo-stale = the FR-008 readers, ~9 nested-tmp → flat). The "~60-file" figure conflated the different, already-flat repository built_in_dir= param, which is NOT changed.

authority).

  • Decision: remove DoctrineService.built_in_root and the nested _built_in_dir; repos
  • Rationale: _built_in_dir returns root / kind / "built-in" (pre-move shape) and fails OPEN —
  • Alternatives considered: keep a flat override (rejected — preserves a foot-gun API + a second

Decision 3 — mission_step_contracts = documented carve-out

resolved via importlib.resources on doctrine.missions.built_in_step_contracts.

tree); relocating them IS #3091 (missions Phase-1b), deferred. Naming + gating the exception (raise) honours unification-not-parity — no silent second mechanism. The claim becomes "one seam across all file-based built-in kinds; step-contract is a documented package-resource exception."

a large deferred effort, violating narrow-COMPLETE).

  • Decision: built_in_dir(MISSION_STEP_CONTRACT) RAISES a named error; step-contracts stay
  • Rationale: they were never relocated (they live under missions/, a kernel-layer __file__
  • Alternatives considered: relocate step-contracts now (rejected — couples this narrow mission to

Decision 4 — CI-enforced single-authority ratchet

pack_paths.py joins resolve_pack_root("built-in") or uses a "built-in" string path-part (AST-based, names the site); (b) positive — every shipped kind except the carve-out resolves inside packs/built-in/<plural>/ and the dir exists, with a #3091 carve-out marker; (c) anti-vacuity — the shipped agent_profiles set is non-empty.

sixth resolver fail CI instead of a future relocation. Mirrors test_shared_package_boundary.py.

failed).

  • Decision: add a tests/architectural/ gate: (a) negative — no src/ module outside
  • Rationale: convention is what let the relocation surface unbounded readers; the gate makes a
  • Alternatives considered: rely on review (rejected — the whole mission exists because convention

Decision 5 — Activation-vocabulary unification + live drift fix

ACTIVATION_KEYS from pack_manager.YAML_KEY_MAP (via a cheap exported plain-tuple constant); add a set-equality guard test. Fix the live drift: m_unify_charter_activation_finalize.ACTIVATION_KEYS is missing activated_glossary_packs (10 vs 11) → silently drops glossary activation on migration.

data-loss path. The migration's no-heavy-import constraint is honoured by exporting a plain tuple, not the pydantic-heavy import.

trusts the activation store this migration writes.

  • Decision: derive charter_yaml_io._ACTIVATION_KEYS and the finalize migration's
  • Rationale: same root cause (vocabulary re-derived at the point of use); the drift is a real
  • Cross-mission note (C-004): this must land before Mission 2's charter-resolver retarget, which

Decision 6 — Context.py shim retirement (severable) + provenance sweep (severable)

delete context.py:25-145. 0 production sites.

after confirming they are descriptive, not runtime-resolved (grep for a "load related" reader; the research indicates descriptive). If runtime-resolved, escalate to FR-008.

they can be their own WPs and never block the load-bearing seam work.

  • Decision (FR-011): re-point ~62 from charter.context import _x test imports to leaf modules;
  • Decision (FR-012): sweep ~18 packs/built-in/** related:/source_files: old-path strings —
  • Rationale: lowest-risk, fully independent; both are the same "one source" theme. Severable so

Confirmed non-findings (de-risk the plan)

product bug — the collision pipeline is intact; the fix is the built_in_root=None path (IC-02).

residue is test fixtures + operator strings + provenance + the two vestigial dead-in-prod paths.

are out of scope (C-002, SC-006) — including the accept-snapshot forgotten-regen.

  • The org-pack collision "regression" is stale test setup (built_in_root=src/doctrine), not a
  • The product relocation is essentially complete; every shipped repo already self-resolves. The
  • Only 7 of 41 PR #3117 CI failures are mission-owned; the other 34 are pre-existing/unrelated and