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 rejects edge, 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.

  • coherent MUST NOT be false solely because unreconciled_tensions is non-empty
  • pair is always the sorted URN pair (lexicographically smaller first) — the same
  • resolution_paths always 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.