Contracts
migrate-opposed-by.md
Contract: spec-kitty migrate subcommand for opposed_by (FR-015)
Resolved decision (research.md): new subcommand modeled on the existing src/specify_cli/migration/backfill_identity.py + src/specify_cli/cli/commands/migrate_cmd.py one-shot-rewrite pattern. No deprecation window — the schema drops opposed_by from additionalProperties: false in the same release this command ships in.
Invocation
spec-kitty migrate rewrite-opposed-by [--pack PATH] [--dry-run] [--json]
(Exact subcommand name confirmed at tasks time against migrate_cmd.py's existing naming convention — rewrite-opposed-by is the working name for this contract.)
Behavior
1. Scan the target pack's authored YAML (directive/tactic/paradigm sources) for opposed_by usages. 2. For each usage, classify per D2/D1:
hand-authored in_tension_with edge.
linking to) a NodeKind.ANTI_PATTERN-marked target node. 3. Remove the opposed_by key from the source YAML once rewritten. 4. --dry-run reports the planned rewrites (source → edge) without writing. 5. Exit non-zero with a clear diagnostic (not a raw Pydantic validation traceback) if an opposed_by entry cannot be unambiguously classified — this is a manual_review case surfaced to the operator, not a silent best-guess rewrite.
- A tension-style entry (competing rule, no anti-pattern semantics) → rewrite to a
- An anti-pattern-rejection-style entry → rewrite to a
rejectsedge, creating (or
Precedent alignment
Mirrors backfill_identity.py's shape: idempotent (a second run against an already-migrated pack is a no-op), verifiable after the fact (an equivalent of doctor identity — e.g. charter lint or a dedicated check — should report zero remaining opposed_by usages post-migration), and safe to run against an already-clean pack.
Failure mode this contract exists to prevent
Without this command, removing opposed_by from additionalProperties: false schemas breaks any downstream/org-pack YAML that authored it, with only a schema validation error pointing at the symptom, not the fix. This contract's exit-code and diagnostic requirements ensure the operator is pointed at spec-kitty migrate rewrite-opposed-by, not left to reverse-engineer the new edge model by hand.
tension-finding.md
Contract: tension_unreconciled finding
Emitted by two surfaces that must render this finding identically (SC-001): the spec-kitty charter pack consistency-check command (existing --json flag pattern, src/specify_cli/cli/commands/charter/pack.py) and the charter activate warning path. Both consume the same TensionFinding value (data-model.md) — this contract is the shape both surfaces must agree on, not two independent formats.
JSON shape (consistency-check --json output)
Added to the existing ConsistencyReport JSON payload as a new top-level key, alongside coherent, unknown_references, etc.:
{
"coherent": true,
"unreconciled_tensions": [
{
"type": "tension_unreconciled",
"pair": ["directive:024-locality-of-change", "directive:025-boy-scout-rule"],
"resolution_paths": ["deactivate one side", "activate a reconciler"]
}
]
}
Invariants:
(NFR-001) — this key is additive/advisory.
pair authored in either direction, or found from either endpoint, produces exactly one entry (INV-001, Edge Case: symmetric authoring drift).
finding with only one path fails SC-001.
NFR-001 (a no-op checker returning [] fails the requirement) — the acceptance test for this contract MUST assert on a positive finding, not merely absence-of-error.
coherentMUST NOT befalsesolely becauseunreconciled_tensionsis non-emptypairis always the sorted URN pair (lexicographically smaller first) — the sameresolution_pathsalways has exactly 2 entries, both present verbatim as shown — a- Absence of a pair from this list, when both sides are active, is a defect for
charter activate warning text
Surfaced alongside existing activation warnings (FR-010) using the same pair + resolution_paths data — human-readable, not JSON, but naming both artefacts and both resolution paths verbatim (SC-001 applies to both surfaces identically).
Example:
Warning: directive:024-locality-of-change is in tension with
directive:025-boy-scout-rule. Resolve by: (1) deactivating one side, or
(2) activating a reconciler that bridges both (a single reconciler bridging
both sides, or two distinct active reconcilers each bridging one side).
Non-finding case (edge case coverage)
Given an in_tension_with pair where only one side is in the active set: no tension_unreconciled entry is emitted for that pair (spec.md US1, Acceptance Scenario 3) — the check only fires on co-activated pairs.
Error case (fail-closed)
If the DRG load required to compute this finding throws (malformed graph, missing node, etc.), the error MUST surface in ConsistencyReport.verification_errors (an existing field) — never silently reduce to an empty unreconciled_tensions list (FR-009: "the DRG load fails closed"). A swallowed exception masquerading as "no tensions found" is indistinguishable from the correct empty case and is exactly the defect FR-009 exists to prevent.