Implementation Plan: DRG completeness (#2843) — relation-description parity + activation-gate consolidation
Branch: doctrine/drg-completeness-2843 (coord topology) | Date: 2026-07-22 | Spec: kitty-specs/drg-relation-parity-activation-gate-01KY48PD/spec.md Input: Feature specification (hardened by the post-spec adversarial squad) at kitty-specs/drg-relation-parity-activation-gate-01KY48PD/spec.md
Summary
Two independent slices closing the residue PR #2833's landing left open (epic #2466; item 1 → #2847):
- Item B (P1) — activation-gate correctness.
charter/drg.py::_node_is_activatedStep 3 (drg.py:319) compares a directive node's canonical id (DIRECTIVE_001) againstPackContext.activated_directives, which holds config stems (001-architectural-integrity-standard). They never match, so a populated list drops every directive node. Verified LIVE, not merely latent (see research.md, D1): this repo's.kittify/config.yamlpopulatesactivated_directiveswith 26 stems,PackContextstores them un-normalized (pack_context.py:364), and three live callers run the filter — the mainDoctrineService.get()path is exempt, so it silently under-resolves directives in charter-mediated resolution rather than crashing. Fix = route the gate through the existingcharter.kind_vocabulary.resolve_artifact_urn(stem→canonical), collapse the two genuine workarounds, and net-test the five consumers with real observables. - Item A (P2) — relation-description parity. Backfill
RELATION_DESCRIPTIONSfor the 12 undescribedRelationmembers; convert the code-side completeness gate; restructure the one parity-enforced doc surface (doctrine-relationships.md) into 15 per-relation sections and widen_SCOPED_RELATIONS3→15; extendcontext/doctrine.mdas non-enforced prose.
Technical approach (resolver siting decision — research.md D2, refined by the post-plan squad): resolve-in-gate, batched once per call. filter_graph_by_activation builds a resolved dict[kind, frozenset[canonical_urn] | None] once (lifting _build_tension_active_urns from the deleted trio) via the existing resolve_artifact_urn, and _node_is_activated consumes it as a pure membership check — no public gate-signature change, PackContext.activated_* keeps holding stems, and drg.py stays IO-lean. doctrine_root comes from resolve_doctrine_root() (the compiler projection's source, pinned equal by test), org_roots from pack_roots[1:]; unknown stems are skipped-with-report (never raise). Rejected: normalize-at-PackContext-construction (Option B), which changes what the shared field holds and ripples to every stem-expecting reader.
Technical Context
Language/Version: Python 3.11+ Primary Dependencies: internal — charter (drg, kind_vocabulary, pack_context, consistency_check, compiler, reference_resolver, context), doctrine.drg (models); pydantic, ruamel.yaml. No new third-party deps. Storage: N/A (in-memory DRG graph + YAML doctrine artifacts; RELATION_DESCRIPTIONS is a Python dict, not serialized into any .graph.yaml) Testing: pytest — red-first characterization test against the real built-in corpus + this repo's populated config; per-consumer before/after tests with named observables; test_relation_doc_parity.py (content-equality) + tests/doctrine/drg/test_models.py (registry completeness); ruff + mypy --strict Target Platform: Linux/macOS/Windows dev + CI (cross-platform CLI library) Project Type: single (Python library) Performance Goals: N/A — activation lists are small; stem→canonical resolution is cheap/cacheable and runs only in charter-mediated filter paths Constraints: no gate-signature change (roots sourced from PackContext); PackContext.activated_ stays stems; require-canonical (no dual-branch tolerating raw canonical-ids in config, C-002); the references.yaml compiler projection is untouched (C-001); no edge rewiring (C-006); ruff/mypy --strict clean, complexity ≤15, no new suppressions (NFR-004) Scale/Scope: ~2 focused lanes; Item B touches charter/drg.py + consistency_check.py (delete tension-scan trio, re-point _check_graph_kind_parity) + 5 consumer test files; Item A touches drg/models.py, doctrine-relationships.md (restructure), context/doctrine.md, test_relation_doc_parity.py, test_models.py
Charter Check
GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.
Charter present (software-dev-default, mode compact). Relevant binding gates and how this plan complies:
- ATDD-first / red-first (C-011 / DIRECTIVE_041): Item B lands the NFR-001 characterization test (with a GREEN canonical-id control) red through the pre-existing
filter_graph_by_activationentry point before the fix. ✅ planned. - Canonical sources & unification (DIRECTIVE_044): the gate reuses
resolve_artifact_urn— no second normalizer (C-004). ✅ - Close-the-defect-class-by-construction (DIRECTIVE_043): require-canonical at the gate (C-002), not tolerate-both — the stem≠canonical class cannot recur. ✅
- Test-and-typecheck quality gate (DIRECTIVE_030):
ruff+mypy --strictclean, ≤15 complexity, no new suppressions (NFR-004); every new branch/helper gets a focused test (NFR-002). ✅ - Living-documentation-sync (DIRECTIVE_037) / Common-Docs (DIRECTIVE_042): Item A edits existing docs in place (no new/moved files); the just-retired anti-sprawl ratchet does not interact. ✅
- Terminology canon (C-006): run the terminology guard before pushing doctrine/prose. ✅
No charter violations. No Complexity Tracking entries required.
Project Structure
Documentation (this mission)
kitty-specs/drg-relation-parity-activation-gate-01KY48PD/
├── plan.md # This file
├── research.md # Phase 0 — resolver-siting decision (D2) + verify-first LIVE-bug finding (D1) + doc-restructure sizing (D3)
├── data-model.md # Phase 1 — the resolved activation set + Relation registry
├── quickstart.md # Phase 1 — run the red-first + parity gates
├── contracts/ # Phase 1 — activation-gate behavioral contract + relation-completeness contract
└── tasks.md # Phase 2 (/spec-kitty.tasks — NOT created here)
Source Code (repository root)
src/
├── charter/
│ ├── drg.py # Item B: _node_is_activated / filter_graph_by_activation (resolve-in-gate)
│ ├── kind_vocabulary.py # Item B: resolve_artifact_urn (REUSED, not extended unless roots plumbing needs it)
│ ├── consistency_check.py # Item B: delete tension-scan trio; re-point _check_graph_kind_parity KIND→per-ID
│ ├── pack_context.py # Item B: activated_* stays stems (unchanged shape); source of roots
│ ├── compiler.py # Item B: references.yaml projection UNTOUCHED (C-001); closure is a consumer
│ ├── reference_resolver.py # Item B: consumer #2
│ └── context.py # Item B: consumer #5
├── specify_cli/mission_step_contracts/executor.py # Item B: consumer #1
└── doctrine/drg/models.py # Item A: RELATION_DESCRIPTIONS backfill
docs/
├── architecture/doctrine-relationships.md # Item A: restructure → 15 per-relation sections (the single parity surface)
└── context/doctrine.md # Item A: extend as non-enforced prose
tests/
├── charter/ # Item B: 5-consumer before/after tests + red-first characterization
└── doctrine/
├── test_relation_doc_parity.py # Item A: widen _SCOPED_RELATIONS 3→15; update stale non-goal docstrings
└── drg/test_models.py # Item A: convert == {3} pin to == set(Relation) completeness
Structure Decision: Single Python-library project; changes are localized to the charter/ activation layer (Item B) and the doctrine/drg enum + docs/ parity surface (Item A). No new packages or directories.
Complexity Tracking
No Charter Check violations — section intentionally empty.
Implementation Concern Map
> Concerns are NOT work packages. /spec-kitty.tasks translates these into executable WPs.
IC-01 — Activation-gate canonical-URN correctness (Item B core)
- Purpose: Stop the per-ID gate silently dropping directive (and any stem-keyed kind) nodes by resolving activated stems→canonical through the existing resolver.
- Relevant requirements: FR-001, FR-002, FR-004, NFR-001, C-002, C-003, C-004
- Affected surfaces:
charter/drg.py(_node_is_activated,filter_graph_by_activation);charter/kind_vocabulary.py(resolve_artifact_urn, reused); roots sourced fromcharter/pack_context.py - Sequencing/depends-on: none (parallel to IC-04/05). Red-first (NFR-001/C-003) precedes the fix.
- Decided (post-plan squad, see contract): unknown stems → skip-with-report (catch
UnknownArtifactIdError,continue;_check_unknown_referencesreports), NEVER raise (raising throws through all 5 consumers incl. the fail-closed-report_check_graph_kind_parity). Resolve once per filter (lift_build_tension_active_urns), not per-node (complexity ≤15 + fs-walk cost).doctrine_rootfromresolve_doctrine_root()(== compiler-projection source, pinned by test),org_roots=pack_roots[1:]. Compare full URN vsnode.urn; adddrg.py's own singular→ArtifactKindconstant (existing map is inconsistency_check.py, which importsdrg). - Risks: the fix is behavior-changing in this repo's charter-mediated paths (directives previously dropped now retained) — expected and intended, but any test asserting the current buggy filtered output must be updated, not preserved.
IC-02 — Workaround collapse (Item B consolidation)
- Purpose: Remove the two genuine workarounds now that the gate is correct — delete the tension-scan reimplementation so it consumes the one gate, and re-point
_check_graph_kind_parityfrom KIND-granular to per-ID. - Relevant requirements: FR-003, SC-003
- Affected surfaces:
charter/consistency_check.py(_node_is_tension_scan_active/_build_tension_active_urns/_resolve_activated_urns_for_kinddeleted;_check_graph_kind_parityre-pointed; also delete the then-orphaned_DRG_SINGULAR_TO_ACTIVATED_KINDS_MEMBER— its only two uses (:835,:920) both vanish).compiler.pyreferences.yamlprojection explicitly NOT touched (C-001). - Sequencing/depends-on: IC-01 (the gate must be correct before consumers rely on it).
- Risks:
_check_graph_kind_parityKIND→per-ID is a deliberate behavior upgrade (exempt from the "unchanged" clause), owned with its own tests; the tension-scan deletion must preserve verdicts (it already encoded correct per-ID resolution).
IC-03 — Five-consumer regression net (Item B safety)
- Purpose: Prove the corrected gate does not regress any of the five
filter_graph_by_activationconsumers, with real observables. - Relevant requirements: NFR-002, SC-004
- Affected surfaces: tests for
mission_step_contracts/executor.py:182,reference_resolver.py:67,compiler.py:1037closure,consistency_check.py::_check_drg_cross_kind_refs(:424),context.py:928 - Sequencing/depends-on: IC-01.
- Risks: each test must assert a named observable (retained nodes / resolved refs / report field),
None-path byte-identical + populated-path corrected — not a smoke test (NFR-002).
IC-04 — Relation registry + code-side gate (Item A core)
- Purpose: Make
RELATION_DESCRIPTIONScomplete and adjudicate the contested/dormant relations. - Relevant requirements: FR-005, FR-007, NFR-003, C-006
- Affected surfaces:
src/doctrine/drg/models.py;tests/doctrine/drg/test_models.py(convert== {3}→== set(Relation)+ non-empty over all) - Sequencing/depends-on: none (parallel to IC-01/02/03).
- Risks:
appliesvsscopeis an adjudication (distinctness floor: descriptions differ);vocabulary/refines/delegates_tounemitted everywhere vsenhances/overrides/replaces0-in-built-in-by-design (org-pack overlay) — describe by actual emission status, no edge rewiring (C-006).
IC-05 — Doc parity surface restructure (Item A docs)
- Purpose: Bring the single parity-enforced doc surface into lockstep and extend the glossary prose.
- Relevant requirements: FR-006, FR-008, NFR-003, SC-002
- Affected surfaces:
docs/architecture/doctrine-relationships.md(restructure into 15 per-relation###sections — ~7 new, split the groupedenhances/overrides/replacesheading, trim Lineage/Delegation prose to registry text);tests/doctrine/test_relation_doc_parity.py(widen_SCOPED_RELATIONS3→15, update stale non-goal docstrings);docs/context/doctrine.md(non-enforced prose) - Sequencing/depends-on: IC-04 (descriptions must exist before the doc can match them).
- /tasks slicing note (priti): splittable into 05a (parity-surface restructure +
_SCOPED_RELATIONSwiden + stale-docstring fixes — test-gated, verbatim-precision) and 05b (context/doctrine.mdnon-enforced prose — editorial), both depending only on IC-04 and parallel to each other. - Risks: this is a doc restructure, not a constant bump (per-relation headings + verbatim body equality); the content-equality comparator raises
LookupErroron a missing heading, so every relation needs its own section.