Phase 1 Data Model: Unblock Sync Identity-Boundary Canary

Mission: unblock-sync-identity-boundary-canary-01KRZJ07 Date: 2026-05-19

Scope

This mission introduces no new persisted entities. The audit and sync fixes either read existing on-disk shapes or render existing in-memory state. The "data-model" surface consists of:

1. The shape contract for rows in status.events.jsonl (already exists; we are naming the lifecycle row family explicitly in the registry). 2. The DaemonOwnerRecord shape (already exists; consumed unchanged by doctor restart-daemon). 3. The sync status --check boundary-row classification (which rows are "path fields" vs "tabular identity fields").

E1 — status.events.jsonl row families (existing; named here)

Each line of status.events.jsonl is one JSON object. Two row families coexist; the audit must distinguish them.

Family A — Status-transition row

{
  "actor": "claude",
  "at": "2026-02-08T12:00:00+00:00",
  "event_id": "01HXYZ...",
  "feature_slug": "<mission-slug>",
  "from_lane": "planned",
  "to_lane": "claimed",
  "wp_id": "WP01",
  "execution_mode": "worktree",
  "force": false,
  "reason": null,
  "review_ref": null,
  "evidence": null
}

Discriminator: presence of from_lane AND to_lane. Owner module: src/specify_cli/status/store.py (write), src/specify_cli/status/reducer.py (read). FORBIDDEN_KEYS rule applies: event_type, event_name must not appear.

Family B — Mission-lifecycle row

{
  "aggregate_id": "01KRZJ079...",
  "aggregate_type": "Mission",
  "event_id": "01KRZJ09...",
  "event_type": "MissionCreated",
  "payload": {"...": "..."},
  "occurred_at": "2026-05-19T07:25:29+00:00"
}

Discriminator: aggregate_type == "Mission" AND presence of event_type. Owner modules: src/specify_cli/status/lifecycle_events.py, src/specify_cli/invocation/propagator.py, src/specify_cli/dossier/, src/specify_cli/next/_internal_runtime/engine.py, src/specify_cli/retrospective/events.py. FORBIDDEN_KEYS rule does NOT apply to event_type / event_name on this family.

Invariant

A row that is both a status-transition row AND a lifecycle row is malformed. The audit MUST still flag a row that carries from_lane / to_lane together with event_type (it does not match Family B because Family B does not carry from_lane / to_lane).

Validation

  • Family classifier is implemented as a function in src/specify_cli/audit/shape_registry.py named for clarity (e.g. is_mission_lifecycle_row(row: Mapping[str, Any]) -> bool).
  • Detector consults this classifier before applying FORBIDDEN_KEYS.

E2 — DaemonOwnerRecord (existing; consumed unchanged)

Fields relevant to doctor restart-daemon:

FieldTypeUsage
package_versionstrReported in boundary mismatch; identifies the foreground at write time.
executable_pathPathPath to the spec-kitty binary that launched the daemon; respawn target.
source_pathPathThe specify_cli source tree; respawn target.
server_urlstrURL of the SaaS endpoint the daemon is bound to.
queue_db_pathPathThe SQLite queue file the daemon owns.
(process metadata)pid, socket location, used for graceful stop.

doctor restart-daemon reads this record, drives a stop using the existing stop primitive, then relaunches using the existing daemon launcher with the same executable_path / source_path.

E3 — sync status --check boundary row classification

Each row rendered in the boundary view is one of:

Row kindExamplesRenderer in WP02
Identity scalarpackage version string, server URL, foreground/daemon parity flagRich Table (preserves tabular layout).
Canonical file pathqueue DB path, executable path, source pathPlain Console.print(f"{label}: {value}") outside the Table.

The split is determined by row kind at render time, not by string-length heuristics. Adding a new path-bearing row in the future picks "outside the Table" by virtue of being a path-kind row.

No schemas to migrate

There are no on-disk schema migrations. All changes are additive (registry entry, new subcommand, rendering refactor) and operate on pre-existing data shapes.