Contracts
adversarial-evidence-contract.md
Contract: adversarial-evidence disposition
Every contested finding raised by a profile-loaded adversarial squad against this mission is recorded with exactly one disposition; none is silently dropped.
- accepted — the finding changed the spec/plan; cite where.
- changed — a modified version was adopted; cite the modification.
- deferred_with_rationale — not adopted now; record why and where it is tracked (e.g. Non-Goal, follow-up issue).
The live ledger for this mission is in ../research.md §"Adversarial Evidence Ledger", covering the pre-spec squad (architect / paula / debugger / renata) and the post-spec squad (renata / paula / priti). Any new adversarial pass (post-tasks, pre-merge) appends rows there.
decisions-doctor.md
Contract: decisions-index reconciler (spec-kitty doctor subcommand)
Surface: src/specify_cli/cli/commands/_decisions_doctor.py, mirroring the _mission_state_doctor.py extraction shape PR #4813 establishes (structure only).
Canonical authority (corrected)
The reconciler rebuilds decisions/index.json from the decision event log, applying the decisions forward event→IndexEntry mapping — NOT status.reducer.reduce() (that reduces a different spec_kitty_events status-lane schema and cannot be reused here).
Single fold, no second reducer. Today the forward writer (decisions/service.py open_decision/_terminal_command) builds the IndexEntry from CLI params and derives the event from it (emit_decision_opened(..., entry=entry)). The reconciler needs the inverse event→IndexEntry fold. To avoid a second source of truth for "what a decision event means":
the forward path is refactored to share the same mapping, so they cannot drift).
- Factor one canonical
event → IndexEntryfold and have the reconciler consume it (ideally - The reconciler must not hand-roll an independent interpretation of the opened/resolved payloads.
Command
spec-kitty doctor decisions --mission <handle> [--json] [--repair]
entries with no backing event).
under the sidecar index.json.lock. No-op (exit 0, "clean", no write) when they already agree.
- Diagnose (default): read-only; report entries in the log missing from the index (and index
--repair: rebuildindex.jsonfrom the log via the canonical fold, written atomically
Invariants
DecisionPointOpened + resolved/terminal events round-trip every IndexEntry field the index needs (no field reconstructed wrong or dropped on --repair). If a field does not round-trip today, that is a prerequisite fix, surfaced before the reconciler is trusted.
- I9 (index reconstructible from the log) must be proven, not asserted: a task asserts the
- Repair never invents entries absent from the log, never drops log-backed entries.
- Runs under the same sidecar lock as the write path (I8) so a concurrent open/resolve cannot race.
Acceptance
- Round-trip test: every IndexEntry field is recoverable from the opened+resolved events (proves I9).
- Seed a diverged corpus (log=8, index=5) →
--repairyields index==log==8 (SC-002). - Repair on an agreeing corpus is a no-op;
--jsonoutput is stable/machine-readable.
no-follow-open.md
Contract: canonical no-follow module (kernel.no_follow)
Layer: src/kernel/ (so kernel.locks may consume it without violating kernel ↛ specify_cli).
Module hoist (whole module, not just the open primitive)
The existing src/specify_cli/core/no_follow.py is hoisted in full to src/kernel/no_follow.py. specify_cli.core.no_follow becomes a thin re-export (from kernel.no_follow import *) with an __all__ identical to today's, so all existing importers need zero edits.
Full preserved public API (all six symbols; identity preserved): NoFollowPathError, chmod_fd, fd_relative_dir_ops_supported, open_no_follow, read_text_no_follow, write_text_no_follow.
except NoFollowPathError at specify_cli/git_common/gitignore_manager.py:18 (and peers) still catches.
NoFollowPathErrormust remain a single class object re-exported (not redefined), soopen_no_follow(path, flags, mode=0o666)keeps its exact signature.
Behavior (open_no_follow)
surfaced as NoFollowPathError (does not follow, does not truncate, does not exit-0).
semantics; documented residual risk (NFR-001 POSIX-scoped).
explicitly; the shared lock does not — C-003).
- Adds
O_NOFOLLOW(POSIX) to the flags; a symlink at the final component raisesELOOP→ - Windows:
O_NOFOLLOWresolves to0(getattr(os, "O_NOFOLLOW", 0)) — degrades to current - Does not add
O_EXCLby default (fresh-create callers, e.g. credential temp writes, pass it
Consumers routed through this primitive (the 4 defect write-sites + the lock)
kernel.locks._LockCore.open_fd, kernel.locks.force_release, zeitgeist_client/credentials.py, tracker/credentials.py, migration/mission_state.py (its lock), runtime/next/_tmp_namespace.py, runtime/asset_preparation.py (sentinel). Scope note: this mission routes the lock + these defect sites through the canonical helper. The ~7 already-correct pre-existing hand-rolled O_NOFOLLOW sites elsewhere are NOT in scope (see spec Non-Goals / research D1 census).
Acceptance
raises NoFollowPathError (never exit-0). Tested per consumer path (SC-001), isolated HOME.
(coordination/atomic_write, git_common/gitignore_manager, intake/brief_writer, session_presence/writers/markdown_rules, skills/{command_installer,installer,manifest_store}, tool_surface/{bundles/projection, providers/agent_profiles, providers/session_presence}, upgrade/migrations/m_3_2_8) in addition to the 13 kernel.locks consumers.
- A planted symlink at the target path leaves the pointed-at file's bytes unchanged AND the call
- Repoint blast radius: WP01 acceptance runs the 11
specify_cli.core.no_followimporters tests/architectural/test_layer_rules.pyandtest_lock_primitive_ban.pystay green.