14 KiB
Implementation Plan: Shared Stage Checkpoint Mechanics
Status: Not started.
Scope and Authority
Implement the agreed follow-up to v2.2.0: consolidate the thin checkpoint mechanism shared by transcribe and analyze without combining their progress models or changing operator behavior. There is no separate consolidation roadmap in this checkout. This plan uses the accompanying discussion as its scope: persistence is already centralized; candidate isolation and save-before-promotion are the duplication to remove. The completed transcription roadmap provides background, not authorization to reimplement transcription or HTTP replay. The preceding completed implementation plan remains in Git history.
Complete the numbered stages in order, one bounded implementation prompt per stage for gpt-5.6-terra. Record completion and actual verification results here. This planning pass changes documentation only; the steps below are for the subsequent implementation sprint.
Before each stage, read AGENTS.md, docs/development.md, and all three
documents in docs/policy/: architecture, documentation, and testing. Use
maintained Go 1.26. Prefer graph tools for code discovery; inspect current
files if their index is stale. Read the stage-specific preparation below,
rather than unrelated historical roadmaps.
Target and Scope Boundaries
The existing internal/manifest.Store and LocalStore remain the session
manifest persistence interface and implementation. Encoding, schemas,
validation, normalization, and atomic file replacement remain unchanged.
There is no new store, journal, file format, dependency, command, configuration,
or migration.
One private helper in internal/stage owns candidate preparation, calling the
existing store, and promoting the selected stage record only after successful
save. The existing typed analyze/transcribe checkpoint methods become thin
callers. This is execution plumbing: keep it beside the callers rather than
moving orchestration into the storage package or exporting a reusable API.
Keep these responsibilities stage-owned:
- Transcribe's completed-only speaker map, evidence checks, bounded concurrency, mutex, checkpoint-failure latch, cancellation, pre-dispatch force reset, output installation, and aggregate repair.
- Analyze's dispositions, selection, dependencies, catalog updates, working and committed collections, failure reconciliation, invalidation, and outputs.
- Each stage's decision whether/when to checkpoint, typed validation, contextual errors, and post-save working-state updates.
- Runner aggregate transitions, session locking, terminal failure saves, and fixed dependent-stage invalidation.
Do not introduce a common subtask model, scheduler, transaction manager, global mutex, automatic retry, additional checkpoint, or batching policy. Do not migrate publish/restore, planning clones, cleanup, or ordinary runner saves: their operations are not the demonstrated duplication. Leave HTTP replay and all adapters unchanged.
Selected Design
Shared Save-and-Promote Operation
Add internal/stage/checkpoint.go with a private helper shaped as:
func checkpointStageCollection(
ctx context.Context,
store manifest.Store,
path string,
live *manifest.Manifest,
stageName string,
update func(*manifest.StageRecord) error,
) error
Names are implementation choices, not public contracts. Use a function and small copying helpers, not a factory/options/interface framework. The two existing typed checkpoint methods are the only production callers. They pass the already-resolved manifest path; the helper does not discover paths or inspect configuration.
The helper performs this sequence:
- Check required live/store inputs without mutation. Copy a checkpoint candidate and select a copied target record. If absent, provide an empty candidate record to the caller, without inferring its name, status, or time.
- Apply the supplied update to that candidate record only. On update error, return without saving or promoting. The typed caller validates and fills its version, collection, and stage-owned initialization/timestamps.
- Invoke
Store.Saveonce with the supplied context. Preserve error identity through%wand useful path context. On error, do not promote, retry, reload, or synthesize aggregate failure. - After success, install the saved target record and candidate manifest
UpdatedAtinto live state. Preserve an existing target record's pointer identity by assigning through it. Allocate missing live maps/records only after success. Do not replace the entire live manifest or overwrite other stage records. Copy mutable checkpoint collections on promotion so saved candidates and caller working collections cannot mutate live progress.
Callers serialize the whole operation. Transcribe's current mutex does so; analyze executes artifacts sequentially. Do not add a lock to the helper or promise support for concurrent independent manifest writers. Atomic replacement visibility does not become power-loss durability or a transaction covering transcript files.
Bounded Candidate Isolation
Factor cloneManifestForTranscribeCheckpoint into a shared checkpoint-scoped
copy helper in the new file. Use ordinary typed copies and existing
manifest.CloneAnalyzeArtifactCollection and
manifest.CloneTranscribeSpeakerCollection. Do not introduce reflection
cloning, JSON round trips, or a public general-purpose clone API.
Copy the manifest value, stages map, stage records, and mutable collections
touched by these updates or LocalStore.Save. In particular, Save normalizes
analyze collections throughout the candidate, including on a transcribe
checkpoint. Clone those collections for every stage so normalization cannot
leak into live state on failure. Preserve the existing bounded copies of
input/artifact and output/log/config slices. Retain nil entries for existing
validation to reject, rather than silently repairing invalid manifests.
This is not arbitrary deep isolation of metadata. Neither the two update
callbacks nor the current Store mutates map[string]any metadata, so it may
remain shared read-only. Document that boundary and inspect every current
normalization write. Use existing typed clones for nested checkpoint evidence;
do not restrict legal metadata values or alter their Go/JSON representations
to simplify copying.
Differences to Preserve
- Analyze still requires an existing stage record. Its callback changes only analyze version/collection, not transcribe-style initialization or timestamp policy. It refreshes working and committed collections only after success.
- Transcribe retains missing-record initialization for direct stage calls:
its typed caller supplies name, running status, and initial timestamps when
needed. Its callback retains the existing stage
UpdatedAtupdate. Copying an existing record must not clear aggregate fields or invoke lifecycle transitions. - Existing transcribe live-record initialization is incomplete on the unusual missing-record path; promoting the actual successfully saved candidate makes its identity agree with disk. This does not add a new missing-record rejection or change ordinary runner status transitions.
- Analyze rollback/failure handling and transcribe failure latching remain in their execution owners. The shared operation never invents failed items.
- Preserve starting-checkpoint elision, force/reset semantics, empty versioned collections, evidence and producer identity, legacy transcription handling, supported zero-sized audio, and aggregate repair without HTTP calls.
- Do not freeze exact error wording, wall-clock values, copy choreography, or iteration order into new contracts. No new schema, limits, filename rules, statuses, or compatibility guarantees are authorized.
Stage 1: Add the Shared Checkpoint Operation
Status: Pending.
Preparation: internal/manifest/store.go, manifest.go, analyze_state.go,
transcribe_state.go; internal/stage/analyze_execution_state.go,
transcribe_execution_state.go; docs/internal/manifest.md.
- Add the private helper and bounded candidate copying described above. Leave existing production checkpoint paths unchanged until their migration stages. Do not duplicate manifest record-validation rules.
- Add focused
internal/stage/checkpoint_test.gocoverage with realmanifest.LocalStoreand temporary directories for successful save/reload. Use a small Store wrapper only to observe/fail the persistence boundary. - Prove live progress remains old during Save, matches saved progress after success, and does not change on update, validation, or save failure. Include both typed collections and a transcribe checkpoint alongside analyze records subject to save-time normalization. A failure test must exercise candidate timestamp/normalization mutation before returning an error, not merely fail before any work.
- Prove unrelated stage/aggregate data survives, existing target references observe updates, missing-target creation is invisible on failure, and later caller collection edits do not mutate committed records. Preserve context and error identity. Test the supported copy/write boundary, not arbitrary malicious Store mutation.
Verify: go test ./internal/manifest ./internal/stage and
go test -race ./internal/stage.
Done when the shared integrity boundary is tested without changing existing stage behavior. Temporary copying overlap is removed in Stage 3.
Stage 2: Migrate Analyze
Status: Pending.
Preparation: internal/stage/analyze_execution_state.go,
analyze_checkpoint_test.go, analyze_failure_test.go,
internal/app/analyze_checkpoint_recovery_test.go, and
docs/internal/stage-analyze.md.
- Replace manual candidate copying, saving, and live promotion in analyze with the shared helper. Retain typed validation, required-record checking, the typed update callback, and committed/working collection refresh.
- Preserve starting-checkpoint elision, selected-artifact behavior, catalog sequencing, dependent invalidation on changed output, and rollback after checkpoint failure. Do not change the runner's terminal failure save or joined errors.
- Run existing tests before adding coverage. Retain tests proving earlier artifacts survive failure/cancellation, unsaved output bytes gain no authority, selected/forced work preserves appropriate unrelated artifacts, and interrupted runs recover across store reloads. Add only missing integration coverage; do not duplicate Stage 1's isolation matrix.
Verify: go test ./internal/stage ./internal/app ./internal/manifest and
go test -race ./internal/stage ./internal/app.
Done when analyze delegates persistence choreography while progress, artifact selection, and failure behavior remain unchanged.
Stage 3: Migrate Transcribe and Remove Duplicate Machinery
Status: Pending.
Preparation: internal/stage/transcribe_execution_state.go, transcribe.go,
transcribe_resume.go, transcribe_checkpoint_test.go,
transcribe_reconciliation_test.go,
internal/app/transcribe_checkpoint_recovery_test.go, and
docs/internal/stage-transcribe.md.
- Delegate
checkpointCollectionLockedto the shared helper, retaining typed initialization/update and successful working-collection refresh. Keep the existing mutex across candidate creation, save, and promotion. Do not move HTTP/hashing work into that critical section or add a lock. - Preserve failure-latch checks and worker joining, the parent versus worker context distinction, force's durable empty starting collection, and ordinary retry's reuse of committed forced replacements.
- Remove
cloneManifestForTranscribeCheckpoint. Its current use by a read-only resume test is not a reason to retain duplicate production code. Use an independent test snapshot or the shared bounded copy where sufficient, without exporting a helper for tests. - Retain existing real-store and concurrency coverage: no lost completions; pre-dispatch save failure prevents requests/canonical writes; later failure retains prior authority; interrupted and aggregate-save recovery; failed force followed by retry; run-directory independence; zero-sized audio; removed/mismatched aggregate repair without additional WhisperX requests or removed-speaker merge input. Do not weaken these regression assertions.
Verify: go test ./internal/stage ./internal/app ./internal/manifest and
go test -race ./internal/stage ./internal/app.
Done when both stages use one save/promote implementation and one bounded copy implementation, with their different models and synchronization intact.
Stage 4: Document, Review Scope, and Validate
Status: Pending.
Preparation: docs/internal/manifest.md, docs/internal/stage-transcribe.md,
docs/internal/stage-analyze.md, documentation/testing policies, and the
implementation diff against this plan's starting commit.
- Update manifest internals to identify the shared checkpoint owner and copy/ save/promote boundary. Stage guides retain stage-specific policy and link to that explanation instead of duplicating shared implementation details. Do not rewrite immutable release notes or announce new operator behavior.
- Assess tests collectively. Consolidate genuinely duplicated persistence mechanics coverage, but retain stage/application recovery tests proving correct integration. Avoid test-count/LOC quotas. Record a brief summary here of duplicated mechanisms removed and thin wrappers retained.
- Confirm no schema, validation policy, scheduling, force, concurrency, output selection, stage order, adapter call, or configuration change slipped into the refactor. Additional restrictions require a separate decision, not tests that retrospectively declare new policy.
- Run all gates below and record actual results. Mark this plan complete only when both migrations and verification pass. Do not release, tag, push, delete other roadmaps, or expand into a repository-wide cloning refactor.
go test ./...
go test -race ./...
go vet ./...
go build ./...
go test ./internal/doccheck
go test ./internal/config -run '^TestExamplesLoadAndValidate$'
git diff --check
Done when duplication is removed, recovery remains covered, documentation matches the implementation, and all gates pass.
Open Questions
None requiring a product decision. A private stage-level helper is selected because persistence already has an owner and the duplication is execution plumbing. Model unification, library extraction, generalized transactions, and broader cloning cleanup remain out of scope.