Implementation Plan: Glossary Pack Doctrine Kind
Branch: research/glossary-doctrine-artefact | Date: 2026-07-21 | Spec: spec.md Input: Feature specification from kitty-specs/glossary-pack-doctrine-kind-01KY30SW/spec.md
Summary
Mission A (keystone) of the Glossary Doctrine Overhaul program. Introduce GLOSSARY_PACK as a first-order, charter-activatable doctrine ArtifactKind, add a src/doctrine/glossaries/ repository that loads *.glossary-pack.yaml packs, wire the kind through the DRG + charter activation/cascade the same way directive is wired, ship a built-in spec-kitty-core pack carrying the 104 canonical terms migrated from .kittify/glossaries/spec_kitty_core.yaml, and prove the built-in pack resolves as a loaded DRG node. Enforcement fields (aliases/banned_synonyms/synonyms_to_avoid) are present in the schema but unwired (Mission B); the runtime src/glossary/ and the casing gate are left untouched (Mission C/B).
The technical approach is a copy-the-directive-kind exercise plus one greenfield package and one data migration — deliberately additive, no behaviour change to the runtime glossary. The authoritative build map is in research.md, distilled from the pre-spec research squad (glossary-research/01-doctrine-glossary-kind.md).
Technical Context
Language/Version: Python 3.11+ Primary Dependencies: existing doctrine subsystem (BaseDoctrineRepository, DRG models/loader, charter.activation_engine/cascade, DoctrineService), ruamel.yaml for pack files; pytest / mypy --strict / ruff for gates Storage: on-disk YAML — built-in .glossary-pack.yaml pack file(s) under src/doctrine/glossaries/ plus a shipped .graph.yaml DRG fragment; no database Testing: pytest (unit + tests/architectural/ gates), ATDD red-first per WP; targeted surfaces tests/doctrine/, tests/charter/, tests/architectural/ Target Platform: cross-platform CLI/library (Linux, macOS, Windows) Project Type: single project (CLI + library) Performance Goals: spec-kitty doctor doctrine --json < 2 s with the built-in pack loaded (NFR-005) Constraints: underscore URN glossary_pack: (NFR-001); three mirrored kind-lists move in lockstep (C-005); NO coupling into runtime src/glossary/ (C-002); cyclomatic complexity ≤ 15; mypy --strict + ruff zero-warning; ≥ 90% new-code coverage (NFR-004) Scale/Scope: 1 new ArtifactKind, 1 new package (src/doctrine/glossaries/), ~10–12 touched surfaces (enum, DRG models + loader/extractor, kind_vocabulary, pack_context, activations, org_pack_loader, charter/drg.py, DoctrineService, doctor), 1 built-in 104-term pack
Charter Check
GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.
implementation. Keystone acceptance tests: the underscore-URN regression (NFR-001), the kind-list drift-guard (C-005), the 104-term migration fidelity test (NFR-002), and the non-vacuous "resolves as a loaded node" guard (NFR-003). — PASS (planned)
canonical directive wiring through the real doctrine/charter chain — no improvised loader or hand-rolled activation. — PASS
(test_org_pack_augmentation.py:411-431) binds only _ALLOWED_KINDS ↔ _ORG_DRG_CANONICAL_KINDS — it does NOT cover _BUILTIN_ARTIFACT_KINDS, the list that delivers default-on (squad F2). We EXTEND it to a genuine three-way equality (concrete floor) + a positive "glossary_packs ∈ default activated_kinds" assertion. The "resolves as a loaded node" guard adds a negative-control arm (remove the graph fragment/emission → resolution goes RED) so it is non-vacuous. — PASS (planned, extension required — the current guard is insufficient as-is)
in Mission A it is additive and the runtime seed remains the authority for the casing gate (dual-home is temporary and tracked to Mission B/C). — PASS (with documented transition)
__all__; the new src/doctrine/glossaries/ package declares __all__. — PASS (planned)
in per-lane worktrees; operator merges. — PASS
- ATDD-first (C-011, binding). Every WP commits a RED-first acceptance test before
- Canonical sources & unification (DIRECTIVE_044). The kind is added by copying the
- Architectural gate discipline (DIRECTIVE_043). The existing kind-list drift-guard
- Single canonical authority. The pack becomes the canonical doctrine home for terminology;
__all__convention (C-007). Any touched module undersrc/charter/MUST declare/extend- Terminology canon. No
feature*aliases; the kind/token isglossary_pack/glossary-pack. — PASS - Git/workflow (DIRECTIVE_045). Planning artifacts on the integration branch; implementation
No violations requiring Complexity Tracking.
Project Structure
Documentation (this mission)
kitty-specs/glossary-pack-doctrine-kind-01KY30SW/
├── plan.md # This file
├── research.md # Phase 0 — build map + decisions (folded from pre-spec squad)
├── data-model.md # Phase 1 — GlossaryPack / GlossaryTerm / kind / DRG node
├── quickstart.md # Phase 1 — how to verify the kind loads + activates
├── contracts/ # Phase 1 — pack YAML schema + doctrine-integration contract
└── tasks.md # Phase 2 — /spec-kitty.tasks output (NOT created here)
Source Code (repository root)
> Naming reconciliation (squad M4). Every existing kind keeps plural == dir == accessor > (service._built_in_dir(<plural>) → <root>/<plural>/built-in; dirs directives, tactics, > agent_profiles, …). The kind's plural is glossary_packs, so the package dir and accessor > are glossary_packs — NOT glossaries. The built-in dir is built-in (hyphen).
src/doctrine/
├── artifact_kinds.py # (edit) GLOSSARY_PACK enum member + _PLURALS["glossary_pack"]="glossary_packs" + _PATTERNS; keep OUT of _NON_AUGMENTATION_ELIGIBLE_KINDS; from_operator_token/CHARTER_KIND_TOKENS derive free
├── drg/
│ ├── models.py # (edit) NodeKind.GLOSSARY_PACK; underscore URN glossary_pack:; fence comment vs retiring runtime GLOSSARY/GLOSSARY_SCOPE
│ ├── loader.py # (context) globs src/doctrine/*.graph.yaml at ROOT, non-recursive
│ ├── org_pack_loader.py # (edit) add "glossary_packs" to _ORG_DRG_KIND_ALIASES dict (NOT the derived frozenset)
│ └── migration/extractor.py # (edit) NEW per-kind emission block in extract_artifact_edges (extract helper for complexity ≤15); regenerate graph
├── glossary_packs/ # (NEW package)
│ ├── __init__.py # declares __all__
│ ├── models.py # GlossaryPack aggregate + GlossaryTerm (ALL seed fields; confidence: float)
│ ├── repository.py # GlossaryPackRepository(BaseDoctrineRepository) — glob *.glossary-pack.yaml
│ └── built-in/
│ └── spec-kitty-core.glossary-pack.yaml # 104 migrated canonical terms (all fields)
├── glossary_pack.graph.yaml # (NEW, GENERATED by extractor) root-level DRG fragment — the resolution seam
└── service.py # (edit) DoctrineService.glossary_packs accessor + _built_in_dir("glossary_packs")
src/specify_cli/cli/commands/
└── _doctrine_health.py # (edit, WP05) glossary-pack counts + invalid→unhealthy (doctor doctrine)
src/charter/
├── pack_context.py # (edit) _BUILTIN_ARTIFACT_KINDS += "glossary_packs"; activated_glossary_packs field + _read_activated_glossary_packs + from_config/from_activation wiring
├── activations.py # (edit) _ALLOWED_KINDS += "glossary_packs"; _SINGULAR_TO_PLURAL_KIND += {"glossary_pack":"glossary_packs"}
├── consistency_check.py # (edit) _CLI_KIND_TO_DRG_SINGULAR += {"glossary-pack":"glossary_pack"}
├── pack_manager.py # (edit expected-value tests) YAML_KEY_MAP derives glossary-pack → activated_glossary_packs
└── drg.py # (edit) two kind-map entries
tests/
├── doctrine/glossary_packs/ # repository + model + duplicate-surface validation tests
├── charter/ # activation/cascade + default-on assertion; update exact-set tests (test_pack_manager*, test_packcontext_has_all_ten→eleven)
└── architectural/ # THREE-WAY drift-guard extension (+_BUILTIN); URN regression; non-vacuous resolves-as-loaded-node guard (with negative-control arm); C-002 import-boundary; standing pack⟺seed parity
Structure Decision: Single-project layout. New code is one greenfield package (src/doctrine/glossary_packs/) plus edits to existing doctrine/charter surfaces that host the analogous directive wiring. Built-in pack assets live at src/doctrine/glossary_packs/built-in/; the generated DRG fragment ships at the doctrine package root (src/doctrine/glossary_pack.graph.yaml) because load_built_in_graph globs *.graph.yaml non-recursively there (squad B1). Explicitly NOT under .kittify/glossaries/ (the seed contract belongs to the runtime retired in Mission C).
Implementation Concern Map
> Implementation concerns are NOT work packages. /spec-kitty.tasks translates these into WPs. > Recommended sequence (post-squad): IC-01 → IC-02 → IC-04 → IC-03 → IC-05 → IC-06 → IC-07. > IC-04 (graph emission/resolution) moved EARLIER — it holds the two blockers and is the > load-bearing risk, not a lightweight finisher.
IC-01 — Kind registration (ArtifactKind + DRG node + token)
with the correct underscore URN.
_PLURALS["glossary_pack"]="glossary_packs", _PATTERNS; keep out of _NON_AUGMENTATION_ELIGIBLE_KINDS), src/doctrine/drg/models.py (NodeKind.GLOSSARY_PACK, the underscore-URN regex/prefix assertion, comment fence vs retiring runtime GLOSSARY/GLOSSARY_SCOPE).
CHARTER_KIND_TOKENS, and YAML_KEY_MAP derive from the enum once the member exists and stays out of the exclusion set — no logic edit to kind_vocabulary.py/pack_manager.py (only their exact-set tests, see IC-03). NB: from_operator_token is on the enum (artifact_kinds.py:128), YAML_KEY_MAP in pack_manager.py — not kind_vocabulary.py.
{template, asset} (explicit membership assertion).
- Purpose: Make
GLOSSARY_PACKa real, charter-activatable kind and a DRG-addressable node - Relevant requirements: FR-001, FR-002, FR-003, FR-010; NFR-001; C-001.
- Affected surfaces:
src/doctrine/artifact_kinds.py(enum memberGLOSSARY_PACK="glossary_pack", - Sequencing/depends-on: none (foundation).
- Free (verified derived):
from_operator_token('glossary-pack')→glossary_pack, - Risks: URN underscore-vs-hyphen trap (NFR-001 regression is the guard); accidental inclusion in
IC-02 — Glossary-pack repository + schema + service accessor
via DoctrineService.glossary_packs.
fields* — surface, definition, confidence: float, status, see_also, introduced_in_mission, synonyms_to_avoid, aliases, banned_synonyms; repository.py inheriting BaseDoctrineRepository glob .glossary-pack.yaml; __init__.py with __all__); src/doctrine/service.py (.glossary_packs accessor + _built_in_dir("glossary_packs")).
first draft dropped); confidence is a float, not enum/str; None default for optional list fields (decided); duplicate term-surface → validation error (needs a synthetic fixture — the seed has no dups); enforcement fields round-trip for Mission B.
- Purpose: Load
*.glossary-pack.yamlpacks intoGlossaryPack/GlossaryTermand expose them - Relevant requirements: FR-004, FR-005; C-004 (enforcement fields present, unwired).
- Affected surfaces: new
src/doctrine/glossary_packs/package (models.pywith **ALL seed - Sequencing/depends-on: IC-01.
- Risks: schema must carry EVERY seed field (seed has
see_also/introduced_in_missionthe
IC-03 — Charter activation wiring (ALL surfaces) + default-on + three-way drift-guard
by default, and make the drift-guard actually protect default-on.
activated_glossary_packs field + _read_activated_glossary_packs + from_config/from_activation wiring; activations._ALLOWED_KINDS + _SINGULAR_TO_PLURAL_KIND; drg/org_pack_loader._ORG_DRG_KIND_ALIASES (the dict behind _ORG_DRG_CANONICAL_KINDS); consistency_check._CLI_KIND_TO_DRG_SINGULAR; charter/drg.py two map entries; update exact-set tests (test_pack_manager*, test_packcontext_has_all_ten_activated_fields 10→11). Extend the drift-guard (test_org_pack_augmentation.py:411-431) to a three-way equality including _BUILTIN_ARTIFACT_KINDS, plus a positive default-on assertion.
+ the root graph fragment (IC-04). No config.yaml entry, no suggests/requires edge.
the extended guard is the counter; all list keys are plural strings ("glossary_packs").
- Purpose: Make the kind activate/cascade/deactivate generically, ship the built-in pack active
- Relevant requirements: FR-006, FR-007, FR-008, FR-009; C-005; SC-003.
- Affected surfaces (squad-expanded):
pack_context._BUILTIN_ARTIFACT_KINDS+ - Default-on mechanism (named): three-state
Nonedefault +_BUILTIN_ARTIFACT_KINDSmembership - Sequencing/depends-on: IC-01, IC-02, IC-04 (needs the fragment for resolution).
- Risks: omitting
_BUILTIN_ARTIFACT_KINDSsilently disables default-on with a green suite —
IC-04 — Extractor emission + ROOT graph fragment + non-vacuous resolution guard (load-bearing)
pack resolves as a loaded node — proven by a fakeable-failure guard.
extract_artifact_edges globbing glossary_packs/built-in/*.glossary-pack.yaml (extract a helper to hold complexity ≤15; the function is at the C901 ceiling); regenerate + commit the generated src/doctrine/glossary_pack.graph.yaml at the doctrine root (NOT nested); non-vacuous resolves-as-loaded-node test in tests/architectural/.
package root (loader globs there non-recursively); the guard MUST include a negative-control arm (remove fragment/emission → resolution RED), not just assert node presence.
- Purpose: Emit the pack's own DRG nodes and ship the generated root fragment so the built-in
- Relevant requirements: FR-011; NFR-003.
- Affected surfaces:
drg/migration/extractor.py— a new per-kind emission block in - Sequencing/depends-on: IC-01, IC-02. Do this EARLY — it is the resolution seam.
- Risks:
_KIND_MAPalone is insufficient (edge-targets only); the fragment must sit at the
IC-05 — Built-in spec-kitty-core pack + full-fidelity migration + standing parity
parity with the seed until Mission C.
a standing pack⟺seed parity test over the full key-set (not a one-shot snapshot) that stays green until Mission C deletes the seed; round-trip test on the 3 real synonyms_to_avoid terms.
obvious four fields — parity must be full-key superset; the seed file is READ, never modified (C-003).
- Purpose: Author the built-in pack, migrate all 104 terms with true zero loss, and keep it in
- Relevant requirements: FR-006; NFR-002; SC-002.
- Affected surfaces:
src/doctrine/glossary_packs/built-in/spec-kitty-core.glossary-pack.yaml; - Sequencing/depends-on: IC-02.
- Risks: the seed carries
see_also/introduced_in_mission/synonyms_to_avoidbeyond the
IC-06 — Doctor reporting + performance
doctor fast.
freebie); a synthetic invalid-pack fixture for the unhealthy branch; a doctor < 2 s assertion.
- Purpose: Surface glossary-pack counts + health; invalid packs never reported healthy; keep
- Relevant requirements: FR-012; NFR-005; SC-001.
- Affected surfaces:
_doctrine_health.py(profile-centric today — real new code, not a - Sequencing/depends-on: IC-02, IC-03.
- Risks: an invalid member pack must degrade aggregate health (not silently pass).
IC-07 — Boundary + regression guards (previously unowned requirements)
from src/doctrine/glossary_packs/ (C-002); a regression pass over the pre-existing glossary/doctrine/architectural suites incl. test_glossary_canonical_terms.py + test_no_legacy_terminology.py (SC-004).
runtime — the exact failure C-002 exists to prevent.
- Purpose: Prove Mission A stays additive and regresses nothing.
- Relevant requirements: C-002, SC-004.
- Affected surfaces: an architectural import-boundary test forbidding any
src/glossaryimport - Sequencing/depends-on: IC-02 (the package must exist to guard its imports).
- Risks: without the import-boundary gate, a future edit could silently recouple to the dying