spec-kitty / spec-kitty/spec-kitty
Architecture: guarantee every workspace-bound event has SQLite or git durability
- Dominant language
- Python
- Stars
- 1.6k
- Forks
- 165
- Avg merge
- 14h 22m
- Merged PRs (30d)
- 343
Description
## Problem
Architectural invariant requested by Robert:
> Every meaningful event that is done with spec-kitty and is eventually meant to go to workspace has to be in the SQLite database, or it has to be in git in `.kittify` or `/kitty-specs/`.
>
> Audit all of the data intended for workspace and show that it is guaranteed to get there.
Current answer after auditing latest `main`: **not guaranteed end-to-end today**.
SaaS is reasonably durable **after receipt**: batch ingress persists an `Event` row plus a `BatchIntakeItem` before worker materialization/drain. The weak points are before receipt: CLI producer paths that are gated by SaaS/auth/team/sync state, git-only paths without replay/commit guarantees, and event families that are contracted in `spec-kitty-events` but have no machine-readable durable-source registry.
## Audit scope
Audited fresh clones from `spec-kitty-monorepo-prep` workspace:
- `Priivacy-ai/spec-kitty` @ `307993957f6d`
- `Priivacy-ai/spec-kitty-saas` @ `476a2ee1d3dd`
- `Priivacy-ai/spec-kitty-events` @ `2651f0673bd9` (`version = 6.1.0`)
- `Priivacy-ai/spec-kitty-tracker` @ `888bc635e46e`
Relevant source paths:
- CLI lifecycle: `src/specify_cli/status/lifecycle_events.py`
- CLI status events: `src/specify_cli/status/emit.py`
- CLI SQLite sync outbox: `src/specify_cli/sync/emitter.py`, `src/specify_cli/sync/queue.py`
- CLI decisions git log: `src/specify_cli/events/decision_log.py`
- CLI runtime bridge: `src/specify_cli/sync/runtime_event_emitter.py`
- CLI dossier/body uploads: `src/specify_cli/sync/dossier_pipeline.py`, `src/specify_cli/sync/body_upload.py`, `src/specify_cli/sync/body_queue.py`, `src/specify_cli/dossier/emitter_adapter.py`
- CLI local commit frames: `src/specify_cli/sync/local_commit.py`
- CLI legacy mission events: `src/specify_cli/mission_v1/events.py`
- SaaS durable intake/drain: `apps/sync/views.py`, `apps/sync/ingestion.py`, `apps/sync/models.py`, `apps/sync/drain.py`, `apps/sync/replay.py`, `apps/sync/projections.py`, `apps/sync/materialize.py`
- Event contracts: `src/spec_kitty_events/__init__.py`, `project_lifecycle.py`, `mission_next.py`, `dossier.py`, `analytics.py`, `retrospective.py`, `sync.py`, `connector.py`, `collaboration.py`, `profile_invocation.py`
- Tracker boundary: `spec-kitty-tracker/docs/ARCHITECTURE.md`, `TEAMSPACE_MIGRATION_BOUNDARY.md`, `wp04-contract-alignment.md`
## Current durability audit
| workspace-bound data family | Current local durable source | Current guarantee | Gap |
|---|---|---:|---|
| `BuildRegistered`, `BuildHeartbeat`, most runtime/analytics/proof events emitted through `EventEmitter` | SQLite offline queue (`~/.spec-kitty/queues/*.db`) | Mostly yes | `_emit()` validates/gates before queueing. If validation or missing required identity rejects, no SQLite row unless another git source exists. |
| `ProjectInitialized`, `MissionCreated`, `SpecifyStarted/Completed`, `PlanStarted/Completed`, `TasksStarted/Completed`, `WPCreated` | `.kittify/canonical-events.jsonl` or `kitty-specs//status.events.jsonl` | Locally yes, workspace replay no | SaaS fan-out is gated by `SPEC_KITTY_ENABLE_SAAS_SYNC`, auth/team/scope/project identity. If disabled at mission start, JSONL exists but there is no guaranteed git-to-workspace replay/import path. |
| `WPStatusChanged` | `kitty-specs//status.events.jsonl` plus SQLite queue | Strongest current path | Good model: canonical git append/readback happens before SaaS fan-out. Needs registry proof so it cannot regress. |
| `DecisionInputRequested`, `DecisionInputAnswered` | `kitty-specs//decisions.events.jsonl` | Partial | SQLite queue intentionally excludes these. `DecisionGitLog` appends to file, but commit failures are swallowed; tests confirm event line can survive uncommitted with HEAD unchanged. Need retry/pending-state guarantee. |
| `MissionRunStarted`, `NextStepIssued`, `NextStepAutoCompleted`, `MissionRunCompleted` | SQLite queue via `EventEmitter` | Mostly yes | OK if `_emit()` accepts. Needs registry + disabled-sync test proving rows stay queued with `drain_blocked_reason`. |
| `significance_evaluated`, `decision_timeout_expired` runtime callbacks | None | No | `runtime_event_emitter.py` drops payloads. Either classify as non-workspace/local-only or persist. |
| Dossier events: `MissionDossierArtifactIndexed/Missing/SnapshotComputed/ParityDriftDetected` | SQLite queue only when sync/dossier pipeline active | No | `trigger_feature_dossier_sync_if_enabled()` returns early when SaaS sync disabled; emitter adapter silently drops when no emitter is registered. |
| Artifact body uploads (`spec.md`, `plan.md`, `tasks.md`, research/contracts/checklists/WP docs, etc.) | Git artifact body exists; SQLite body queue only when enabled | No | `prepare_body_uploads()` returns `sync_disabled` without queueing intent. Queue-full has no git/overflow fallback. Need git scanner or durable upload manifest. |
| Post-mission lifecycle: `MissionReopened`, `FollowUpRecorded` | CLI currently comments/handles as local-only; SaaS now projects them | No/Drift | SaaS changelog and projections support these with `spec-kitty-events==6.1.0`, but CLI lifecycle code still treats them outside SaaS strict-validation path. |
| Retrospective events | `kitty-specs//status.events.jsonl` | Local yes | Needs explicit workspace importer/materializer mapping and tests. |
| Legacy `mission_v1` events | `kitty-specs//mission-events.jsonl` if `feature_dir` exists | Partial | If `feature_dir is None`, event is logged/debugged but not persisted. Classify local-only or route through durable sink. |
| Local commit frames | `.kittify/sync-state.json` | Yes | Good pattern: pending frames stored before send and replayed on connect. |
| Tracker events/state | Host-owned persistence only | Expected | `spec-kitty-tracker` docs correctly say tracker does not own durable workspace persistence. CLI/SaaS host boundary must enforce the invariant. |
## SaaS side finding
Once an event reaches SaaS batch ingress, the durability model is sounder:
- `apps/sync/views.py::batch_sync_events()` validates the batch then calls `persist_event_for_durable_drain()` inside a DB transaction.
- `apps/sync/ingestion.py::persist_event_for_durable_drain()` creates the canonical `Event` row and a `BatchIntakeItem` drain row.
- `apps/sync/drain.py` claims, materializes, retries, marks terminal failures, mirrors private events into shared workspaces, and dispatches projections.
- `apps/sync/replay.py` can rebuild projections from stored `Event` rows.
This does **not** solve runs that were not SaaS-enabled at the beginning. If the CLI path did not write SQLite and did not write an importable git record, SaaS has nothing to recover.
## Required architecture
### 1. Add a workspace durability registry
Create a machine-readable registry, preferably in `spec-kitty-events` or imported by both CLI and SaaS, with one row per event/data family:
- `event_type` or data family name
- `teamspace_bound: bool`
- `durable_source: sqlite_event_queue | sqlite_body_queue | git_status_events | git_canonical_events | git_decisions_events | git_artifact_body | kittify_sync_state | saas_postgres_only`
- `producer_module`
- `replay_adapter` / import command
- `saas_materializer`
- `local_only_reason` when `teamspace_bound = false`
- `pii_policy` for git-backed sources
Invariant: a producer may not emit/send/drop a workspace-bound fact until it first records that fact in one approved durable source.
### 2. Make feature flags affect drain eligibility only, never capture
For workspace-bound data, disabled sync/auth/team/project state must result in one of:
- SQLite event/body row with `drain_blocked_reason`, or
- git-backed append/manifest under `.kittify` or `kitty-specs`, with a registered replay path.
`SPEC_KITTY_ENABLE_SAAS_SYNC=0`, missing auth, or missing team must not mean “not written.”
### 3. Add git-to-workspace replay/backfill
Add `spec-kitty teamspace audit` and `spec-kitty teamspace replay --from-git` (names negotiable) that scan and import:
- `.kittify/canonical-events.jsonl`
- `kitty-specs/*/status.events.jsonl`
- `kitty-specs/*/decisions.events.jsonl`
- `kitty-specs/*/mission-events.jsonl` if still supported
- artifact bodies under `kitty-specs/*/{spec.md,plan.md,tasks.md,research.md,contracts/,checklists/,tasks/WP*.md,...}`
- `.kittify/sync-state.json` local commit frames where relevant
Replay must be idempotent by event id/content hash and use canonical SaaS validation/materializers.
### 4. Fix dossier/body durability
Dossier and artifact-body upload intent needs durable capture even when SaaS sync is disabled:
- record body-upload intent in SQLite regardless of sync enablement, or
- write a git-backed upload manifest under `.kittify`/`kitty-specs`, then replay from git later.
Also add a queue-full overflow strategy. Current `body_upload_queue` failure must not be the final durable state for workspace-bound artifact bodies.
### 5. Fix decision commit/retry semantics
`DecisionInput*` being git-authoritative is OK, but the guarantee must cover commit/replay:
- if `safe_commit()` fails, write a pending decision-log commit/replay marker under `.kittify` or surface audit failure;
- `teamspace audit` must report uncommitted or unreplayed `decisions.events.jsonl` entries;
- replay must import decisions from git/working-tree source or require successful commit before claiming durability.
### 6. Resolve contract drift
SaaS now projects `MissionReopened` and `FollowUpRecorded`; CLI still treats post-mission lifecycle as local-only/outside SaaS strict validation. Move these into the registry with explicit durable source + replay/materializer coverage.
Also classify currently dropped runtime callbacks (`significance_evaluated`, `decision_timeout_expired`) as either non-workspace/local-only or durable workspace-bound events.
## Acceptance tests
Add tests that fail when a workspace-bound fact has no durable source:
1. Static registry test: every non-local `spec-kitty-events` event type has a registry entry with durable source, producer, replay adapter, and SaaS materializer/consumer.
2. CLI disabled-start test: run specify → plan → tasks → next/decision flows with `SPEC_KITTY_ENABLE_SAAS_SYNC=0`; assert all workspace-bound facts are present in SQLite or git; then enable sync and replay; assert SaaS receives/materializes them.
3. Dossier/body disabled-start test: create/update `spec.md`, `plan.md`, `tasks.md`, contracts/checklists/WP docs while sync disabled; assert body upload intent or git replay manifest exists and replay imports bodies.
4. Decision commit-failure test: force `safe_commit()` refusal; assert `decisions.events.jsonl` survives and audit reports pending/uncommitted replay state.
5. Queue-full test: event queue and body queue full conditions must write approved overflow/git manifests or fail loudly before claiming workspace durability.
6. Contract drift test: CLI supported/published event types match SaaS materializer coverage for all workspace-bound types, including `MissionReopened` and `FollowUpRecorded`.
7. Tracker boundary test: tracker continues to own no durable workspace persistence; CLI/SaaS host adapters prove persistence before tracker egress/sync.
## Operator-visible outcome
After this lands, the answer to “if a run was not fully SaaS enabled at the beginning, what happens to workspace-bound events?” must be:
- They are still captured locally in SQLite or git immediately.
- Enabling SaaS later plus running replay/drain guarantees eventual workspace delivery unless audit reports a concrete unrecoverable violation.
- `spec-kitty teamspace audit` can prove coverage per mission/run instead of relying on Telescope observation.
Contributor guide
Assessment
This issue has not been assessed yet.