Contracts

freshness-signal.contract.md

Contract — synthesized_drg freshness signal is no-op-correct

Requirements: FR-006; NFR-001, NFR-003. Invariants: INV-2, INV-6. Surface: src/specify_cli/charter_runtime/freshness/computer.py (+ the preflight consumer charter_runtime/preflight/runner.py:_attempt_auto_refresh).

Statement

The synthesized_drg freshness signal drives whether the preflight auto-refresh shells out to charter synthesize. It MUST report fresh when the on-disk doctrine artifacts already reflect the current authoritative inputs (a genuine no-op), and stale only when a re-synthesize would produce a materially different tracked artifact.

Guarantees

#GivenThen synthesized_drg is
F1doctrine artifacts already match current charter.yaml + pack inputsfresh (no synthesize)
F2only volatile/non-substantive fields differ (timestamps, provenance — cf. _VOLATILE_GRAPH_FIELDS, #1912)fresh (no synthesize / no churn)
F3charter.yaml, activation, or a pack input changed substantivelystale (synthesize runs)
F4doctrine artifact missingstale (synthesize runs)

Rationale / failure modes to avoid

churn → dirty tree (violates INV-1). This is what #2373's residual instance is.

staleness (violates INV-2). The fix MUST NOT trade the churn for silent staleness.

  • Under-fresh (the bug): signal reports stale on F1/F2 → needless synthesize → tracked-doctrine
  • Over-fresh (the anti-fix): signal reports fresh on F3 → real change never regenerates → silent

Test obligations (red-first, doctrine-tracked)

This assertion is RED before the fix (reproduce per LM-1 in a doctrine-tracked checkout).

  • F1/F2: after a clean synthesize, recompute freshness → fresh; run preflight twice → tree clean.
  • F3: mutate charter.yaml → freshness stale → synthesize regenerates.
  • Keep regeneration materialized on disk (INV-6) — do not convert readers to on-demand.

no-op-stability.contract.md

Contract — Governed charter operations are no-op-stable

Requirements: FR-005, FR-006, FR-007; NFR-001, NFR-003. Invariants: INV-1, INV-2, INV-6.

Statement

A governed charter operation — any read/render/gate that consults charter or doctrine state without the user explicitly asking to (re)generate — MUST NOT modify a git-tracked artifact. Running such an operation on an unchanged, doctrine-tracked tree, any number of times, leaves git status --porcelain empty.

Governed operations in scope:

  • spec-kitty charter context [--action …] (render path — build_charter_context)
  • spec-kitty doctor doctrine
  • any preflight-gated command (next, implement, dashboard) via run_charter_preflight

Preconditions

.git/info/exclude masked state. Tests establish this explicitly (remove the exclude / use a fresh clone) so the assertion is meaningful.

  • Clean working tree.
  • .kittify/doctrine/** is git-tracked (the committed .gitignore state) — NOT the local

Guarantees

#GivenWhenThen
G1clean tree, synthesized_drg freshcharter context renders0 tracked-file diffs (only untracked context-state.json may change)
G2clean tree, synthesized_drg freshpreflight-gated command runscharter synthesize is NOT invoked; 0 tracked-file diffs
G3clean treeany governed op runs twice2nd run: 0 tracked-file diffs
G4charter.yaml genuinely editedpreflight-gated command runssynthesize DOES run; doctrine refreshed (INV-2 — no over-suppression)

Non-guarantees

by design.

  • Untracked runtime state (.kittify/charter/context-state.json) MAY be written — it is gitignored
  • An explicit spec-kitty charter synthesize / write command MAY (and should) regenerate artifacts.

Test obligations (red-first)

against a real charter in a doctrine-tracked temp repo (G1/G3).

  • Extend tests/charter/test_context.py with a git-clean assertion after build_charter_context
  • Add a preflight no-op ratchet asserting G2/G3 in tests/specify_cli/charter_runtime/.
  • Add an INV-2 test (G4) so the fix cannot over-suppress genuine staleness.