Contracts
project-sync-store-layout.md
Project Sync Store Layout Contract
Authority
This mission-local contract defines the internal project storage boundary. It does not replace contracts/cli-saas-current-api.yaml in the explicitly attested SaaS candidate checkout.
Canonical resolver
Input: canonical UUID value only. Output: <runtime-root>/projects/<lowercase-hyphenated-uuid>/sync/sync.db plus sibling egress.lock and non-sensitive migration reports.
Rules:
1. Parse as UUID once; reject missing/nil/malformed values. 2. Render only the canonical UUID, which is deterministic ASCII. 3. Resolve runtime root only through get_runtime_root() so SPEC_KITTY_HOME and platform semantics remain canonical. 4. Do not accept caller-provided store paths on live APIs. 5. A component opened through context A must assert its on-disk owner UUID is A before reading or mutating. 6. Consent, epochs, journal, delivery, outbox/body, target/admission, and cutover metadata share this one SQLite transaction boundary. 7. ProjectSyncStore.unit_of_work() exclusively owns live SQLite connections and outer transactions. Component repositories accept that unit; no live journal/ledger/queue/control adapter may call sqlite3.connect() or commit() itself. Nested work uses explicit savepoints. 8. ProjectSyncStore.layout_generation() is the only current-version writer authority. It returns a generation-bound write permit under the machine layout lock immediately before insert; stale permits retry/redirect exactly once and project_only permits can never name a legacy destination.
Live component contract
| Component | Required capability | Cross-project behavior |
|---|---|---|
| journal | ProjectSyncContext/unit of work | Atomically assign monotonic capture sequence+epoch and reject mismatched UUID before insert. |
| delivery attempt/result | same unit of work | Persist attempt before I/O and result after reconciliation; reject IDs not owned by store. |
| outbox/body queue | same store capability | Assign epoch; reject task UUID mismatch before insert/drain. |
| consent/epoch control | sole store-owned writer | Only explicit opt-in/out or refusal migration may mutate; opt-in begins at current tail and opt-out seals. |
| target/admission control | store-owned writer | Exact server/account/Private-Teamspace/project tuple; cannot mutate local consent. |
| admission operation outbox | store-owned writer | Persist operation key/expected generation/audience before remote mutation; retry same identity. |
| history disclosure | explicit preview/confirm writer | Bind immutable cohort/hash/actor/generations; ordinary selection cannot mint. |
| daemon deny hint | discovery-only writer | May record only bounded deny/revoke; cannot represent or infer grant. |
| purge/status/doctor | explicit project context or explicit legacy diagnostic mode | Never fall back from project context to shared live state. |
Egress eligibility
eligible = kill_switch_allows
and consent.state == granted
and context.consent_generation == current_generation
and row.epoch_id == current_eligible_epoch
and target.ready
and context.target_identity == current_target_identity
and admission.state == admitted
and context.admission_generation == current_admission_generation
Selection and final transport both enforce eligibility. Every Event, LocalCommit, body, and history/preflight item carries source UUID, current admission generation, and binding audience. Before network I/O, a durable attempt records native idempotency/correlation, generations, payload hash/reference, deadline, and recovery mode. The final check, transport start, and genuine result record occur under a bounded lease. Opt-out cancels unstarted attempts and, while holding that barrier, discovers every orphaned old-generation attempt left after process death. It reconciles with the original identity or freezes the attempt as terminal_unknown before returning. No later automatic recovery may promote that state to success or resend.
Capture epoch contract
Local rows may be captured without consent. Capture sequence and epoch assignment are one transaction. Explicit opt-in records the current inclusive sequence tail; only later sequences in the new epoch are ordinary candidates. Opt-out seals without deleting. Target changes/re-opt-in do not select sealed rows. A history action must preview exact IDs/count/hash, then explicitly confirm under unchanged consent/target/admission audience; it is the only capability permitted to select the cohort.
Daemon discovery contract
A fresh deny/revoke hint at <runtime-root>/projects/.deny-hints/<uuid>.json may skip sync.db. It is atomically published after the decision commit, removed after opt-in, expires by bounded TTL, and has no granted value. Missing, expired, malformed, generation-mismatched, pending, or possibly granted state opens authority. Benchmark evidence records every database/table open.
Legacy rule
Legacy state is migration/diagnostic input only. Before migration implementation, ProjectSyncStore publishes the one layout-generation/write-permit API. Every current-version journal, delivery, event-outbox, body/offline, foreground, background, daemon, and CLI writer uses it immediately before insert, redirecting/retrying after cutover. Migration consumes this authority, quiesces recognized daemons, obtains a strictly read-only logical SQLite snapshot with explicit WAL/SHM treatment, copies/verifies, and atomically publishes project-only cutover. No live fallback delivers legacy state; only unrecognized old binaries can create late residue, which is diagnosed and non-deliverable.
Candidate contract input
Core contract consumers take an explicit SaaS candidate checkout path, expected commit, and expected SHA-256 digest. They read contracts/cli-saas-current-api.yaml from that checkout and fail if the checkout commit or digest differs. Relative ambient sibling lookup, branch name, and version string are forbidden as proof inputs.
sender-and-migration-matrix.md
Sender and Migration Acceptance Matrix
Live sender census
Every row must be bound to a ProjectSyncContext and durable DeliveryAttempt, covered by a denial test, admitted positive control, both revoke orderings, kill-before-send/during-response/before-result, per-write UUID/generation/audience capture, and architecture census.
| Sender class | Representative surface | Required identity source |
|---|---|---|
| direct dispatcher | delivery/dispatcher.py | selected event/store context |
| emitter WebSocket | sync/emitter.py | envelope project UUID/context |
| daemon publish | sync/daemon.py, sync/runtime.py | enumerated project store control state |
| event relay | runtime event emitter/client | frame/envelope context |
| body drain | sync/body_queue.py, sync/body_transport.py | task project/store context |
| final/exit sync | CLI/final sync orchestration | explicit project context |
| reconnect/local commit | sync/local_commit.py | stored frame project UUID/context |
| history import | sync/history_import/ | import project context |
| tracker-hosted | tracker/SaaS adapter | hosted consent plus separate Channel 2 |
| generic SaaS client | sync/client.py/SaaS client | explicit project context |
No row may substitute current working directory, active target, login, global environment, or discovery index. Local consent is project-wide, but server admission is valid only for the exact normalized server, authenticated account/canonical Private Teamspace, project UUID, and generation in the context.
Revocation orderings
1. Paused before transport start: opt-out acquires the barrier, advances/seals, cancels the work, and no request/result occurs. 2. Transport already started: the sender holds its result lease; opt-out waits; the genuine result is written under the old generation; opt-out then seals/returns; no later network write or success record occurs.
Crash orderings
For each transport family, kill the sender after the durable attempt but before send, during response uncertainty, and after remote acceptance but before local result commit. Recovery uses the same Event ID, admission operation key, content hash, git hash, or history action/event identity and records duplicate/success truthfully. Add the compound ordering kill during response -> immediate opt-out -> late recovery: opt-out must reconcile or irrevocably freeze the orphan as terminal_unknown before acknowledgement, and the late recovery must be unable to record success or resend. A non-reconcilable transport never silently retries.
Migration data classes
| Source class | Destination | Consent effect |
|---|---|---|
| valid canonical UUID event | owning sync.db journal table and capture epoch | none; pre-consent rows remain sealed |
| matching delivery outcome | owning sync.db delivery table | none; preserve status/attempts/target/generation |
| matching body/offline task | owning sync.db outbox table and capture epoch | none |
| legacy explicit refusal | owning project control state | may write migrated_refusal |
| legacy grant/path/repo default/old UUID cache | report only | never grant; explicit re-consent required |
| missing/malformed/nil/blank UUID | legacy quarantine | none; permanently non-deliverable |
| conflicting identity or ledger ghost | legacy quarantine/error report | none; fail closed |
Fault injection phases
Run migration in a subprocess and hard-kill before/after daemon/current-writer quiesce, read-only logical snapshot, each table copy, verification, staged publication, cutover marker, redirect, and restart. Verify exact committed IDs/statuses/attempts/targets/timestamps/hashes and explicit main/WAL/SHM treatment; do not instantiate schema-migrating source constructors. For every current-version journal, delivery, event-outbox, body/offline, foreground, background, daemon, and CLI writer, pause before its layout permit/insert and exercise both orderings: the write is included in the legacy snapshot or redirected once to the project store, never both. An unrecognized old-binary post-cutover write is non-deliverable residue. No rerun duplicates or redelivers.
Six-project acceptance
- A: current locally eligible epoch and exact-target remote admission — only project allowed to appear in conforming CLI request bytes or persist.
- B: locally refused.
- C: no decision.
- D: opted out during paused transport.
- E: identity-less legacy rows in quarantine.
- F: valid UUID admitted to a different target/account/team; bypass client attempts the wrong binding.
The core-owned real CLI scenario captures exact HTTP/WebSocket bytes and proves B–F are absent, then records stale-generation terminal parking. The separately owned SaaS evidence submits B–F through bypass/legacy adapters and proves correlated refusal with zero durable/readable/broadcast effects. Event ID sets and a unique foreign marker make negative evidence non-vacuous; selection counts alone are insufficient. A shared checksum manifest binds the exact core/SaaS/tombstone commits and canonical contract digest while preserving these non-overlapping proof owners.