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
| # | Given | Then synthesized_drg is |
|---|---|---|
| F1 | doctrine artifacts already match current charter.yaml + pack inputs | fresh (no synthesize) |
| F2 | only volatile/non-substantive fields differ (timestamps, provenance — cf. _VOLATILE_GRAPH_FIELDS, #1912) | fresh (no synthesize / no churn) |
| F3 | charter.yaml, activation, or a pack input changed substantively | stale (synthesize runs) |
| F4 | doctrine artifact missing | stale (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) viarun_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.gitignorestate) — NOT the local
Guarantees
| # | Given | When | Then |
|---|---|---|---|
| G1 | clean tree, synthesized_drg fresh | charter context renders | 0 tracked-file diffs (only untracked context-state.json may change) |
| G2 | clean tree, synthesized_drg fresh | preflight-gated command runs | charter synthesize is NOT invoked; 0 tracked-file diffs |
| G3 | clean tree | any governed op runs twice | 2nd run: 0 tracked-file diffs |
| G4 | charter.yaml genuinely edited | preflight-gated command runs | synthesize 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.pywith a git-clean assertion afterbuild_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.