Phase 0 Research: Doctrine Tension as First-Class DRG Edges
All [NEEDS CLARIFICATION] markers from spec.md were resolved before this mission reached plan (D1/D2/D3, resolved by the operator via the squad-escalation path — see decisions/). Two additional plan-phase decisions were resolved via the Decision Moment Protocol during this planning session. No open clarifications remain (spec-kitty agent decision verify returns status: clean).
Decision: FR-015 downstream compatibility mechanism
src/specify_cli/migration/backfill_identity.py, wired via src/specify_cli/cli/commands/migrate_cmd.py) that rewrites org-pack-authored opposed_by usages into in_tension_with/rejects edges. No deprecation window — the schema drops opposed_by from additionalProperties: false in the same release the migration ships in.
pattern (backfill_identity.py + doctor identity verification loop) for exactly this shape of problem: a schema/model change that could silently break org-pack consumers. Reusing it keeps the fix discoverable and consistent, and D1 only required a migration/deprecation path — it did not require carrying the legacy field forward.
exact mis-encoding (D1's finding: extractor maps opposed_by to replaces, producing a nonsensical mutual cycle) that this mission exists to retire. A window would mean two representations of tension coexist for N releases.
onto every downstream consumer for a mechanically-derivable rewrite (opposed_by → in_tension_with, or → rejects for anti-pattern targets per D2); higher error surface than a tool doing it once.
- Decision: New
spec-kitty migratesubcommand (module modeled on - Rationale: This repo already has an established one-shot-rewrite migration
- Alternatives considered:
- Deprecation window (warn for N releases before removal): rejected — prolongs the
- Diagnostic/warning only, no auto-rewrite: rejected — pushes manual YAML editing
- (Decision record:
01KY239G8EAZD7D1K37N5NM7NX)
Decision: bulk-edit classification (C-007)
covers all 8 standard categories for the 21-site opposed_by/Contradiction removal.
exception. Several of the 21 sites are semantic rewrites (FR-006/FR-007 migrate content to new edge types), not pure deletions, so the per-category guardrail (distinguishing code_symbols/serialized_keys needing manual_review from tests_fixtures safe to rename) is load-bearing, not boilerplate.
exemption requires every site to be a straight deletion, which FR-006/FR-007 contradict (content moves to new edges, it does not simply disappear).
- Decision: Mission runs under
change_mode: bulk_edit;occurrence_map.yaml - Rationale: C-007's own text frames
bulk_editas the default, exemption as the - Alternatives considered: Exempt as pure-removal — rejected, since a pure-removal
- (Decision record:
01KY23N4RBCGWVGZ4QJFFFSJPH)
Domain research: symmetric, non-transitive relation storage
lexicographically-smaller URN (C-002); query both directions via the existing DRGGraph.edges_from/edges_to helpers (confirmed present — Assumption A3); the consistency-checker keys findings on the sorted URN pair for dedup.
B→A would require keeping two edges in sync) and needs no new graph primitive.
C-002 directly: doubles authoring surface and reopens the divergent-reason drift problem named in the Edge Cases section (symmetric authoring drift).
- Decision: Store
in_tension_withas one canonical edge, source = the - Rationale: Avoids a duplicate-edge bookkeeping problem (authoring both
A→Band - Alternatives considered: Store both directions as separate edges — rejected by
Domain research: cascade exclusion mechanism
engine's REFERENCE_RELATIONS allowlist (exclusion by omission); guard with a regression test asserting they are absent (FR-013/C-003), since there is no "excluded relations" list to positively assert against.
allowlist with no per-kind special-casing (C-003). Adding a denylist would introduce a second list that must be kept in sync with the allowlist — a single-canonical- authority violation.
(if relation in {IN_TENSION_WITH, ...}: skip) — rejected: reintroduces per-kind logic the cascade engine's design deliberately avoids, and only the allowlist omission is testable as a true regression (a denylist could silently rot if a new relation is added later and nobody adds it to the denylist).
- Decision: Omit
in_tension_with/reconciles_tension/rejectsfrom the cascade - Rationale: The cascade engine's existing contract is pure-reachability off an
- Alternatives considered: Explicit relation-kind check in the cascade engine
Domain research: anti-pattern/smell node representation (D2 follow-through)
first-class NodeKind.ANTI_PATTERN/SMELL, threaded through ArtifactKind, _SINGULAR_TO_PLURAL, _SINGULAR_TO_PER_KIND_FIELD, the activation filter (_node_is_activated), and cascade _kind_of — plus a new tags: list[str] field on DRGNode (Pydantic v2's extra='ignore' default means an un-modelled key silently drops on load, so tags must be modelled explicitly for any marker to round-trip).
tags field today; adding one is required, not optional, for D2's marker to survive a load/save round trip.
exactly what D2 rejected (an anti-pattern is not an active paradigm; conflating the two lets an anti-pattern get cascaded/activated as if it were a live rule).
- Decision (already resolved as D2; researched here for the concrete wiring): new
- Rationale: Confirmed by reading
src/doctrine/drg/models.py—DRGNodehas no - Alternatives considered: Tag on existing
paradigm/tactickind — this is
Codebase verification: unrelated Contradiction symbol
(shared/models.py, directives/models.py, paradigms/models.py, tactics/models.py) are the DRG-model Contradiction this mission removes. The remaining files (charter_runtime/lint/checks/contradiction.py and its two importers, plus its dedicated test) define an unrelated ContradictionChecker — a charter-lint text-contradiction check with no opposed_by occurrence and no relationship to the DRG model. This confirms NFR-002's explicit carve-out ("the dead-symbol gate scoped to that symbol, not the word 'Contradiction' which occurs unrelated elsewhere") is not hypothetical — the ambiguity is real in this codebase today. Recorded as do_not_change exceptions in occurrence_map.yaml.
- Finding:
grep -rln "Contradiction"returns 13 files, but only 4