Skip to content

0027 — Shoot safety resets (guarded re-cull · reset · reset exports)

  • Status: IMPLEMENTED (2026-07-17). Studio surface (re-cull-fresh button, soft/hard reset + reset-exports endpoints + pkg/shoot methods + Review-screen Danger zone) + CLI parity (reset frame/shoot/exports subcommands, the reset <frame>reset frame <name> restructure, godog scenarios). Gates resolved against existing code + 0028 (no data change). The one deferred extra is the §8.2 "silent faces-off loupe empty-state" (a cheap related UX nicety, not a reset action).
  • Why now: a face-enabled re-cull failed over the LAN bridge with an opaque "network error", leaving lyndsey-denis in a stuck state with no in-studio way to recover short of hand-deleting sidecar files. A photographer needs a guarded, discreet failsafe to return a shoot (or its exports) to a known state when a bug or a mistake corrupts it — without ever risking the originals.
  • Realises: 0019 §7 ("incremental/resumable ingest" robustness family) and the operational-trust gap; extends the existing per-frame reset command to a shoot scope. Surfaces the CLI cull --reanalyze capability in the studio.
  • Depends on: 0001 §3/§4.6 (non-destructive guarantee), 0003 (studio contract), 0005 (verdicts/analysis sidecar), 0011 (studio auth), 0024 (settings rail).
  • Anchors: new R-RESET-* requirements (to fold into 0002).

1. Why

The workspace records every decision in the .krites/ sidecar (0001 §3): verdicts.yaml (durable keep/maybe/reject + ratings), analysis.yaml (the regenerable analysis cache), develop.yaml + edits/ + removal patches (edit records), and export/ (rendered output). Caches are regenerable; verdicts and edits are durable decisions (0001 §3 "Caches are regenerable").

Today there is no in-studio way to clear any of these. The only reset is the CLI command scoped to a single frame (pkg/cmd/reset, clears one frame's verdict override + edits). When a whole shoot goes wrong — a failed cull, a bad profile, exports rendered from stale verdicts — the photographer's only recourse is to hand-delete sidecar files, which is both unsafe (easy to delete the wrong thing) and undiscoverable.

This spec adds scoped, guarded reset actions to the studio (with CLI parity), and surfaces the missing force-reanalyze control. The guard exists precisely because these clear durable human decisions: they must be hard to hit by accident and must spell out what they clear vs preserve.

2. The actions

Four operations, three of them destructive-of-decisions (danger-zone gated), one a normal control:

  1. Re-cull (fresh analysis)normal control. Forces a full recompute, ignoring the analysis cache (the studio equivalent of CLI cull --reanalyze, pipeline.WithReanalyze). Re-judges every frame from fresh signals. Not destructive of originals or exports; it does regenerate verdicts, so it is confirmed but lives with the cull controls, not the danger zone. This is also the direct workaround for the class of bug that motivated the spec (a cache stuck in a bad shape).

  2. Reset decisions — keep my pickssoft reset (guarded). Clears the analysis cache and auto-generated verdicts, but preserves every frame the photographer touched by hand — explicit verdict overrides, star ratings, and develop/removal edits. Leaves the shoot ready for a clean re-cull that won't clobber the keepers.

  3. Reset shoot — back to post-ingesthard reset (guarded). Returns the shoot to its just-imported state: clears the analysis cache and all verdicts, ratings, develop edits, and removal patches. Equivalent to the per-frame reset applied across the whole shoot, plus the analysis cache.

  4. Reset exportsguarded. Deletes everything under export/. Independent of the cull state; for when exports were rendered from stale or wrong verdicts.

The soft/hard split is deliberate (decision 2026-07-17): a soft "recompute but keep my picks" covers the common "the auto-cull went weird" case; the hard "everything back to zero" is the true failsafe. Both are offered rather than one compromise semantics.

3. What each action clears vs preserves

Originals are never touched by any action (0001 §3/§4.6, R-ND-*). Only the sidecar and export/ are affected.

Target (.krites/ unless noted) Re-cull fresh Soft reset (keep picks) Hard reset (post-ingest) Reset exports
originals/ (the RAWs) preserve preserve preserve preserve
analysis.yaml (cache) regenerate clear clear preserve
verdicts.yaml — auto verdicts regenerate clear clear preserve
verdicts.yaml — manual overrides + ratings regenerate¹ preserve clear preserve
develop.yaml / edits/ / removal patches preserve preserve clear preserve
previews/ (regenerable) preserve preserve preserve preserve
export/ preserve preserve preserve clear

¹ A re-cull re-resolves all verdicts from fresh signals; §7-Q1 asks whether it should protect manual overrides the way the soft reset does.

Provenance of "manual". The soft reset needs to distinguish an auto verdict from a hand-set one. verdicts.yaml must therefore mark human-touched frames (an override/source flag on the record) — see §7-Q2; if that provenance isn't already persisted it is the one load-bearing data change here.

4. Guard & UX

  • Not on the main chrome — Review-screen danger zone (decided 2026-07-17). The three destructive actions live in a reject-toned, collapsed-by-default "Danger zone" reachable from the Review chrome (an overflow/advanced surface off the shoot being reviewed) — NOT the global settings rail, which applies to all shoots. Re-cull (fresh) sits with the cull controls in the Review toolbar, not here.
  • Explicit confirm. Each destructive action opens a confirm dialog that names the shoot and lists exactly what will be cleared and what is preserved (the §3 row for that action, originals-are-safe stated first). Hard reset and reset-exports — the two that destroy the most — require a typed confirmation (the shoot name) (R-RESET-4); soft reset a single explicit click-through.
  • Honest result. On success, a toast + a notification-bell entry (0025) stating what was cleared; the grid/loupe refresh to the new state.

5. API surface (studio, 0003 §5)

New endpoints, all POST, all behind the studio's existing auth (0011):

  • POST /api/v1/shoots/{id}/recull — force a fresh-analysis cull (streams like the existing POST …/cull, but sets WithReanalyze). (Alternatively: a ?reanalyze=true query on the existing cull endpoint — see §7-Q3.)
  • POST /api/v1/shoots/{id}/reset — body { "scope": "soft" | "hard" }. Clears per the §3 table. Returns { cleared: {...}, preserved: {...} }.
  • POST /api/v1/shoots/{id}/exports/reset — clears export/. Returns { removed: N }.

All are excluded from MCP (R-MCP-2, destructive-feeling — like export/remove).

6. CLI parity

Extend the existing reset command from per-frame to shoot-scoped, via a subcommand or a scope flag (scaffolded with gtb generate command, never hand-edited — cmd.go is generated):

  • krites reset frame <name> — the current behaviour (unchanged).
  • krites reset shoot [--keep-picks] — hard reset, or soft with --keep-picks.
  • krites reset exports — clear export/.

Reset logic lives in pkg/shoot (new Workspace methods: ResetShoot(keepPicks bool), ResetExports()) so the studio and CLI share one tested implementation; the existing Verdicts.Reset, ResetEdits, PruneRemovals are the primitives.

7. Gate decisions (resolved 2026-07-17)

Most gates were already answered by existing code + 0028 — verified before slicing:

  • Q1 — re-cull protects manual overrides → ALREADY TRUE. pipeline.preserveOverrides copies every existing[frame] with Override == true over the fresh verdicts on every re-cull. So "Re-cull (fresh)" already keeps hand-set picks; the soft reset is the one that additionally drops them. No work.
  • Q2 — verdict provenance → ALREADY PERSISTED. shoot.FrameVerdict has Override bool (+ Proposed and DecidedAt), set by Verdicts.Override(...). The soft reset preserves frames where Override is true. No data change — the "one load-bearing data change" the draft anticipated is unnecessary.
  • Q3 — recull endpoint vs flag → USE THE 0028 JOB. POST /cull { reanalyze } and jobTracker.startCull(shoot, { reanalyze: true }) already exist (shipped in 0028). "Re-cull (fresh)" is a button wired to that path — no new …/recull route. The reset endpoints (…/reset, …/exports/reset) stay synchronous (deleting sidecar files is fast, like review/removal — not a background job).
  • Q4 — undo → DEFERRED. No .krites/trash/ stash in v1; the typed shoot-name confirm plus the explicit cleared-vs-preserved list are the safeguard. Originals are never touched, so the worst case is re-running a cull.
  • Danger-zone placement → REVIEW SCREEN (not the settings rail). The settings rail is global ("applies to all shoots") but a reset targets one shoot, so it lives in a reject-toned, collapsed-by-default Danger zone reachable from the Review chrome (§4).

These are the reason the reset failsafe is needed, and are cheap to fix with it:

  1. The cull SSE doesn't survive a long CPU cull. Confirmed 2026-07-17 by reproducing under the studio's sandbox home: provisioning is not the problem (the sandbox .cache/krites is a symlink to the fully-provisioned real cache), and the cull does not crash — the same code path runs cleanly from the CLI, ORT loads, inference churns. The failure is that a face-enabled cull over the LAN is minutes of CPU work, and the studio streams it with no heartbeat: the handler commits the SSE 200, then does the slow ORT session-init + per-frame inference, and the first progress byte can be many seconds out. A long/gappy stream drops over the LAN and the client (api.js runCull) reports a bare "network error", while the server keeps grinding. Fix (several, cheap): (a) emit an immediate first SSE event (e.g. progress 0/total) before the model load so headers flush with a body byte; (b) send periodic heartbeat comments during long stretches to keep the connection warm; © cache/reuse the ORT session across culls instead of re-initialising each run; (d) recover in-handler panics into a structured SSE error event, and surface a real HTTP error for any pre-stream failure. → Now owned by spec 0028 (long operations as reconnectable background jobs); this item is retained here only as the incident record that motivated both specs.
  2. Silent "faces off" loupe. When Analysis.Faces is empty (model-free cull), the loupe shows nothing — no "enable Eye detection to see per-face analysis" hint (0020 gap). Add an honest empty-state.

9. Non-goals

  • No touching originals/ — ever, by any action.
  • No cross-shoot / library-wide reset (a shoot is the unit; 0001 §2 not-a-catalogue).
  • No partial "reset just these N frames" from the studio in v1 (the per-frame CLI reset already covers single frames; grid-selection reset is a possible follow-up).
  • Not a general undo/history system (Q4 deferred).