Audit workspace architecture implementation plan
All checks were successful
ci/woodpecker/tag/release Pipeline was successful

This commit is contained in:
2026-05-17 13:22:09 -05:00
parent 6ff54c5a0f
commit e58e545686

View File

@@ -0,0 +1,507 @@
# Workspace Architecture Implementation Plan (Audit)
## 1. Executive Summary
**Complexity assessment:** **heavy**.
This is not a single path-helper refactor. The current codebase has a hybrid session/run model that works for current behavior, but diverges from `docs/development/workspace.md` in foundational places (workspace root shape, manifest responsibilities, stage output placement, and archive symmetry).
Highest-risk areas:
1. Splitting the current single manifest model into durable **session manifest** vs per-invocation **run manifest** without regressing skip/force/resume UX.
2. Migrating path helpers and artifact-store interfaces from session-only roots (`work/{session}`) to campaign-aware roots (`work/{campaign}/{session}`) while preserving existing runs.
3. Introducing run-local stage outputs + immediate promotion while keeping stage tests and archive behavior stable.
4. Avoiding stale downstream skips after forced upstream reruns.
Surprising findings:
1. Code already has campaign/run-aware helpers (`SessionRunWorkDir`, `SessionSpoolAudioDir`) but core session helpers and manifest pathing remain campaign-unaware.
2. Archive recently gained run/session fallback behavior for manifest/promotion sources, which confirms an existing hybrid-layout pressure point.
3. Analyze input resolution is functional but ad hoc and stage-local; there is no centralized artifact registry/resolver.
---
## 2. Current-State Map
## 2.1 Workspace Path Construction
Primary path model:
- [`internal/artifacts/paths.go`](../../internal/artifacts/paths.go)
- `SessionWorkDir(rootDir, sessionID)` => `{root}/work/{session_id}`
- `buildSessionPaths(workspaceRoot, sessionID)` roots all canonical paths under `{root}/work/{session_id}`
- `SessionRunWorkDir(rootDir, campaign, sessionID, runID)` exists, but is not the default session root helper.
Artifact store abstraction:
- [`internal/artifacts/store.go`](../../internal/artifacts/store.go)
- `SessionPaths(sessionID string)` / `EnsureLayout(sessionID string)` are session-id-only (no campaign argument).
- [`internal/artifacts/local.go`](../../internal/artifacts/local.go)
- `EnsureLayout` creates session-level folders under `SessionWorkDir`.
Path normalization helper:
- [`internal/artifacts/resolve.go`](../../internal/artifacts/resolve.go)
- `ResolveSessionLocalPathForRead` accepts absolute/workspace/session-relative values and probes filesystem.
Where campaign-aware/run-aware support exists:
- Local run path helpers: `SessionRunWorkDir`, `SessionSpoolAudioDir`.
- S3 key helpers: [`internal/artifacts/s3_keys.go`](../../internal/artifacts/s3_keys.go) (`campaigns/sessions/runs/current`).
Where session-root assumptions remain `{workspace}/work/{session}`:
- Manifest path computation: [`internal/app/runner.go`](../../internal/app/runner.go) `manifestPathFor`.
- Artifact store layout and most stage `paths := env.ArtifactStore.SessionPaths(sessionID)` calls.
- Many tests hardcode `workspace/work/<session>/...` (examples below).
Manual/ad hoc path construction (not through a single resolver API):
- Common stage patterns: `filepath.Join(paths.TranscriptsDir, "...")`, `filepath.Join(paths.ArtifactsDir, "...")`, etc.
- `prepare` run/work selection: [`internal/stage/prepare.go`](../../internal/stage/prepare.go) `pathsWorkDirForManifest`.
- Analyze input fallback path resolution: `resolveInputPathForRead` in [`internal/stage/analyze.go`](../../internal/stage/analyze.go).
## 2.2 Manifest and Run Identity
Current manifest model:
- [`internal/manifest/manifest.go`](../../internal/manifest/manifest.go) `Manifest` includes both session and run fields:
- `SessionID`, `Campaign`
- `RunID`, `LocalWorkDir`, `LocalSpoolDir`
- `S3Bucket`, `S3SessionPrefix`, `S3RunPrefix`
- `Stages`, `Inputs`, stage outputs/logs/generated configs/metadata.
Current persistence:
- [`internal/manifest/store.go`](../../internal/manifest/store.go) `LocalStore` reads/writes one JSON manifest path.
- Runner always loads/saves one manifest path via `manifestPathFor(cfg)` (session-root path under current layout).
Identity initialization:
- [`internal/app/runner.go`](../../internal/app/runner.go) `ensureManifestIdentity` populates run fields if absent.
- `RunID` is generated once for an empty manifest and reused thereafter (hybrid semantics).
Interpretation today:
- Best described as a **hybrid session manifest** with run identity fields, not as distinct session + run manifests.
What this means for redesign:
- Session-vs-run split is not just file relocation; it requires new responsibilities and write flows.
- A backward-compatible evolution path is possible by:
- preserving current fields in session manifest for migration/read-compat,
- adding explicit run-manifest type + path,
- gradually moving invocation-specific details to run manifests.
## 2.3 Stage Output Paths (Current)
All implemented stages currently write canonical artifacts directly into session-level `paths.*` roots (under current session root), with logs/configs typically also session-level.
1. `prepare` ([`internal/stage/prepare.go`](../../internal/stage/prepare.go))
- Inputs copied to `inputs/` (`session.yml`, `pipeline.resolved.yml`, `speakers.yml`, `autocorrect.yml`, `glossary.yml`)
- Audio copied to `audio/`
- Manifest input provenance recorded in `m.Inputs`
- S3 audio uses run-scoped spool/work helpers when `RunID` is present.
2. `transcribe` ([`internal/stage/transcribe.go`](../../internal/stage/transcribe.go))
- Outputs: `transcripts/raw/{speaker}.json`
- Stage metadata only (no stage logs/config files produced here).
3. `merge` ([`internal/stage/merge.go`](../../internal/stage/merge.go))
- Pre-normalize intermediates: `transcripts/raw/normalized/{basename}.normalized.json`
- Merge output: `transcripts/merged.json`
- Report: `artifacts/seriatim.report.json` (if enabled)
- Logs/configs:
- per-input normalize logs/configs in session-level `logs/` + `config/`
- merge logs/config in session-level `logs/` + `config/`
4. `polish` ([`internal/stage/polish.go`](../../internal/stage/polish.go))
- Output: `transcripts/processed.json`
- Report: `artifacts/audita.report.json` (if enabled)
- Work dir: `artifacts/audita-work`
- Logs/config: `logs/audita.*`, `config/audita.generated.yml`
5. `normalize` ([`internal/stage/normalize.go`](../../internal/stage/normalize.go))
- Output: `transcripts/normalized.json` (configurable)
- Report: `artifacts/seriatim.normalize.report.json` (if enabled)
- Logs/config: `logs/seriatim.normalize.*`, `config/seriatim.normalize.generated.yml`
6. `trim` ([`internal/stage/trim.go`](../../internal/stage/trim.go))
- Output: `transcripts/trimmed.json` (configurable)
- Bounds output path from config (default examples use `artifacts/session_bounds.json`)
- Logs/configs in session-level `logs/` and `config/` for scriptorium + seriatim invocations
7. `analyze` ([`internal/stage/analyze.go`](../../internal/stage/analyze.go))
- Current implemented artifact: `artifacts/session_recap.md`
- Logs/config in session-level `logs/` + `config/`
- Optional render diagnostics under session-level artifacts/logs/config.
8. `archive` ([`internal/stage/archive.go`](../../internal/stage/archive.go))
- Reads from a "workDir" derived by `archiveWorkDir`:
- prefers `m.LocalWorkDir` if it exists,
- else tries run-scoped campaign/session/run path,
- else falls back to legacy session path.
- Uploads run record and promotions.
- Current code now resolves manifest and promotion sources via run/session fallback.
9. `notify`
- Placeholder only in [`internal/stage/placeholders.go`](../../internal/stage/placeholders.go); no durable outputs.
Durable-vs-diagnostic split today:
- Durable artifacts and diagnostics are mixed at session level.
- No run-local stage directories exist yet.
## 2.4 Idempotency / Force / Resume / Sparse Runs
Run control implementation:
- [`internal/app/run_control.go`](../../internal/app/run_control.go)
- skip rule: `!force && stageSucceeded(manifest, stage)`
- stale detection TODO only; no invalidation logic.
Command behavior:
- `run`: full plan through `executeStages`.
- `run-stage`: single selected stage through `executeStages`.
- `resume`: starts at first non-succeeded stage unless `--force`.
Current assumptions:
- Command invocation mutates a single durable workspace + single manifest for the session path model.
- There is no per-invocation run manifest write path.
Smallest safe UX-preserving invariant to keep during migration:
- Session manifest remains source-of-truth for skip decisions across invocations.
## 2.5 Archive and S3 Alignment
S3 semantics are relatively mature:
- [`internal/artifacts/s3_keys.go`](../../internal/artifacts/s3_keys.go)
- session prefix + run prefix + `current/manifest.json` + `current/run_id.txt`.
Archive stage behavior:
- uploads run records under run prefix,
- uploads promoted session outputs,
- uploads current manifest then current run pointer,
- current pointer is commit marker,
- required promotions fail; optional promotions skipped.
Local-vs-remote mismatch still present:
- Local canonical session root currently defaults to `work/{session}` (artifact store),
- while archive/run helpers expect campaign-aware run locations (`work/{campaign}/{session}/{run}`),
- causing fallback logic and hybrid handling in `archive`.
## 2.6 Artifact Source Resolution in Analyze
Current implementation is stage-local and ad hoc:
- Transcript discoverers:
- `discoverProcessedTranscript`
- `discoverNormalizedTranscript`
- `discoverTrimmedTranscript`
- Input source switch in `resolveScriptoriumInput` supports:
- `processed_transcript`
- `normalized_transcript`
- `trimmed_transcript`
- `previous_session_artifact`
No first-class artifact registry exists yet. Aliases and canonical IDs are not modeled.
## 2.7 Stale/Invalidation
- `manifest.StageStatus` already defines `stale` (`internal/manifest/status.go`), but no stage uses it.
- Skip logic ignores stale state and only checks `succeeded`.
- Forced upstream rerun does not invalidate downstream stage success markers.
## 2.8 Test Coverage Relevant to Redesign
High-value existing coverage:
- Workspace helpers:
- [`internal/artifacts/paths_model_test.go`](../../internal/artifacts/paths_model_test.go)
- [`internal/artifacts/resolve_test.go`](../../internal/artifacts/resolve_test.go)
- Runner semantics:
- [`internal/app/runner_test.go`](../../internal/app/runner_test.go)
- [`internal/app/resume_run_stage_test.go`](../../internal/app/resume_run_stage_test.go)
- Stage path/output behavior:
- `internal/stage/*_test.go` for prepare/transcribe/merge/polish/normalize/trim/analyze/archive
- Archive behavior:
- [`internal/stage/archive_test.go`](../../internal/stage/archive_test.go)
- [`internal/app/post_archive_cleanup_test.go`](../../internal/app/post_archive_cleanup_test.go)
Tests likely to fail during workspace redesign:
- Any tests hardcoding `work/{session}` manifest and transcript paths (many in `internal/app/*test.go`, `internal/stage/*test.go`).
- Archive tests assuming current hybrid fallback behavior.
---
## 3. Gap Analysis Against `docs/development/workspace.md`
| Intended concept | Current status | Notes |
|---|---|---|
| Session root at `work/{campaign}/{session}` | **Partial / mostly absent** | `SessionWorkDir` and artifact store still use `work/{session}`. Campaign-aware run helper exists separately. |
| Distinct session manifest vs run manifest | **Absent** | One hybrid manifest model/file is used. |
| Run-local stage dirs under `runs/{run_id}/{stage}` | **Absent** | Stages write canonical outputs/logs/config directly at session level. |
| Immediate promotion run-local -> session canonical | **Absent** | No run-local staging area to promote from today. |
| Session manifest skip source of truth | **Present** | Skip/resume use single manifest stage statuses. |
| Sparse runs represented under `runs/{run_id}` | **Absent** | No run-manifest/per-run stage records yet. |
| Artifact resolver with canonical IDs + aliases | **Absent** | Analyze resolves via stage-local source-name switch and fallback helpers. |
| Downstream invalidation for forced upstream reruns | **Absent** | TODO only; no stale propagation or status clearing. |
| Local semantics mirror archive semantics | **Partial** | Archive/S3 side models runs/current, local workspace core still session-layout-centric. |
| Safe path helpers centralization | **Partial** | Good helper base exists, but many stage-level manual joins still encode conventions. |
---
## 4. Recommended Implementation Sequence
## Step 1: Introduce campaign-aware session path model without behavior break
Purpose:
- Add first-class helpers for `work/{campaign}/{session}` and make them available everywhere.
Expected changes:
- `internal/artifacts`: add/extend path helpers and `SessionPaths` constructor variants that accept campaign.
- `internal/app`: pass campaign into path-model entrypoints where available.
Behavior change:
- None initially (can keep legacy fallback reads).
Tests:
- Add campaign-aware path-model tests.
- Keep legacy-path compatibility tests.
Risks:
- Wide compile-time touch due `Store` interface signatures.
Rollback:
- Keep legacy helper wrappers until full migration lands.
## Step 2: Split manifest responsibilities (session manifest + run manifest scaffolding)
Purpose:
- Preserve current UX while introducing explicit run execution records.
Expected changes:
- `internal/manifest`: add run manifest type/store helpers.
- `internal/app/runner.go`: create/load session manifest and initialize per-invocation run manifest path.
Behavior change:
- Session manifest remains skip truth source.
- Run manifest begins recording invocation metadata/stage actions.
Tests:
- New tests for both manifest files existing and being updated correctly.
Risks:
- Incorrect ordering of saves can regress crash consistency.
Rollback:
- Keep session-manifest-only decision logic until run manifest proves stable.
## Step 3: Move stage execution products to run-local directories with promotion
Purpose:
- Align with workspace architecture (`runs/{run_id}/{stage}/...`) while preserving canonical outputs.
Expected changes:
- `internal/stage`: each implemented stage writes outputs/logs/config/reports to run-local paths.
- Introduce shared promotion helpers (atomic copy/rename + output validation + manifest provenance).
Behavior change:
- Canonical outputs remain session-level; run-local diagnostics now preserved per run.
Tests:
- Stage tests updated to assert run-local outputs + promoted canonical outputs.
- New tests for immediate promotion and producer run metadata.
Risks:
- Highest regression risk (all implemented stages touched).
Rollback:
- Stage-by-stage migration flag or phased rollout by stage order.
## Step 4: Archive alignment pass
Purpose:
- Remove hybrid fallback complexity once local layout is canonical.
Expected changes:
- `internal/stage/archive.go`: resolve sources from canonical session outputs and run manifests deterministically.
- Keep `current/manifest.json` + `current/run_id.txt` publication semantics.
Behavior change:
- Simpler source selection; fewer cross-layout heuristics.
Tests:
- Archive tests for run uploads, promotions, current pointers, and fallback removal/compat gates.
Risks:
- Breaking current mixed-layout compatibility too early.
Rollback:
- Retain fallback compatibility for one migration window.
## Step 5: Artifact registry/resolver (analyze first consumer)
Purpose:
- Replace ad hoc analyze source resolution with canonical artifact IDs and aliases.
Expected changes:
- New resolver package (for example `internal/artifacts/registry` or `internal/stage/artifactresolve`) with IDs:
- `narratio.transcript.merged`
- `narratio.transcript.polished`
- `narratio.transcript.full`
- `narratio.transcript.trimmed`
- `narratio.bounds.session`
- `narratio.artifact.session_recap`
- Backward-compatible alias map for current source names.
Behavior change:
- Analyze input resolution becomes centralized and consistent.
Tests:
- Resolver unit tests for canonical IDs + aliases + missing-input error clarity.
- Analyze tests updated to assert resolver usage.
Risks:
- Input resolution edge cases for `previous_session_artifact` and required/optional handling.
Rollback:
- Keep old resolver path behind a temporary compatibility function.
## Step 6: Minimal downstream invalidation after forced upstream reruns
Purpose:
- Prevent stale downstream skips before full checksum stale detection exists.
Expected changes:
- `internal/app/run_control.go` + manifest transition helpers.
- On forced rerun success of stage `X`, clear/mark downstream success states.
Behavior change:
- Subsequent runs no longer skip stale downstream stages.
Tests:
- New run-control tests for force-induced downstream invalidation.
- Resume tests with forced sparse runs.
Risks:
- Over-invalidating too broadly and degrading UX.
Rollback:
- Start with deterministic downstream stage list based on pipeline order only.
## Step 7: Legacy layout migration strategy
Purpose:
- Handle existing `work/{session}` data safely.
Expected changes:
- Startup detection/migration path in app layer.
- Clear failure messages for ambiguous legacy states.
Behavior change:
- Explicit migration semantics instead of implicit fallback drift.
Tests:
- Migration detection tests for legacy-only, new-only, and ambiguous layouts.
Risks:
- Silent duplication if both layouts are partially populated.
Rollback:
- Prefer fail-fast ambiguity policy over auto-merge.
---
## 5. Minimal Viable v1.0 Scope
Must-have for v1.0:
1. Campaign-aware canonical session roots.
2. Session manifest remains skip/resume authority.
3. Introduce run manifests + run-local stage records.
4. Immediate promotion from run-local outputs to canonical session outputs.
5. Minimal downstream invalidation on forced upstream rerun.
6. Archive/local semantic alignment with `current/*` behavior preserved.
7. Backward-compatible analyze aliases (`processed_transcript`, `normalized_transcript`, `trimmed_transcript`).
Nice-to-have / defer if risky:
1. Full checksum-based stale detection graph.
2. Broad manifest schema-version migration framework.
3. Full artifact-registry rollout beyond analyzes initial needs.
4. Aggressive cleanup of all legacy fallback branches in one release.
---
## 6. Open Questions (with Recommendations)
1. **Should campaign be mandatory for all local path derivation immediately?**
- Recommendation: yes for new layout writes; keep controlled read compatibility for legacy session-only roots during migration window.
2. **Session manifest location transition policy:** auto-migrate vs explicit migrate command?
- Recommendation: if user base is small, prefer explicit fail-fast with actionable migration instructions to avoid silent split-brain state.
3. **Run manifest granularity:** per-stage detailed records vs summary + references?
- Recommendation: start with summary + stage status/paths; avoid duplicating full session artifact state to keep write path simple.
4. **Invalidation marking:** use `stale` status now or clear `succeeded` markers?
- Recommendation: if invasive to propagate new status semantics quickly, clear/overwrite downstream success states first; add formal `stale` usage in follow-up.
5. **Promotion timing:** promote every stage immediately vs delayed at end of run?
- Recommendation: immediate per-stage promotion after validation (matches current idempotent skip expectations and simplifies resume behavior).
6. **Docs consistency order after implementation starts:**
- Recommendation: update `README.md` and `architecture.md` in lockstep with each migration step. Current conflict is explicit:
- code/README/architecture still primarily describe session roots at `work/{session}`
- `docs/development/workspace.md` defines `work/{campaign}/{session}` plus run manifests.