diff --git a/README.md b/README.md index ca1d5c6..4e35281 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Narratio is a stage-driven Go orchestrator for turning D&D session audio into polished transcripts and generated artifacts. -It runs a deterministic workflow across `prepare`, `transcribe`, `merge`, `polish`, `normalize`, `trim`, `analyze`, and `publish`, with manifest-driven resume and restore support. +It runs a deterministic workflow across `prepare`, `transcribe`, `merge`, `polish`, `normalize`, `trim`, `analyze`, and `publish`, with manifest-driven continuation and restore support. ```bash narratio run 2026-04-04 diff --git a/docs/cli.md b/docs/cli.md index 6583c86..700432f 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -13,7 +13,6 @@ This runs the canonical full pipeline for session `2026-04-04`. Top-level commands: - `run `: run full stage order. -- `resume `: continue from first non-succeeded stage. - `run-stage `: run one stage. - `analyze `: force-run analyze. - `publish `: force-run publish. @@ -77,19 +76,9 @@ Behavior: - evaluates full stage order; - skips already-succeeded stages unless `--force` is set; +- continues interrupted or partially completed sessions by running non-succeeded stages; - writes session and run manifests. -### `resume` - -```bash -narratio resume [--force] [--artifacts ] [...common config flags] -``` - -Behavior: - -- when not forced, starts at first non-succeeded stage in manifest order; -- with `--force`, reevaluates the selected stage list as runnable. - ### `run-stage` ```bash @@ -253,7 +242,7 @@ Behavior: ## `--artifacts` Selection Rules -- accepted on `run`, `resume`, `run-stage`, `analyze`, and `publish`; +- accepted on `run`, `run-stage`, `analyze`, and `publish`; - names must exist in `pipeline.scriptorium.artifacts`; - empty entries are invalid; - repeated names are deduplicated. diff --git a/docs/operations.md b/docs/operations.md index 454d5ac..f039099 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -63,7 +63,7 @@ narratio session init 2026-04-04 --remote --force If `campaign.yml` sets `session_template_file`, `session init` renders it. Template variables must resolve to concrete values. -## Stage Execution and Resume Behavior +## Stage Execution and Continuation Behavior Canonical stage order: @@ -80,7 +80,7 @@ Canonical stage order: Execution rules: - succeeded stages are skipped unless `--force` is set; -- `resume` starts at first non-succeeded stage; +- `run` continues interrupted or partially completed sessions by running non-succeeded stages; - force rerunning a succeeded upstream stage marks succeeded downstream stages as `stale`. Single-stage execution: @@ -91,7 +91,7 @@ narratio run-stage normalize 2026-04-04 --force ## Artifact Selection -`--artifacts` can be used on `run`, `resume`, `run-stage`, `analyze`, and `publish`. +`--artifacts` can be used on `run`, `run-stage`, `analyze`, and `publish`. Selection behavior: diff --git a/docs/policy/development.md b/docs/policy/development.md index e1c4991..a33d064 100644 --- a/docs/policy/development.md +++ b/docs/policy/development.md @@ -6,7 +6,7 @@ Canonical contributor workflow and engineering conventions for implemented Narra ## Repository layout - `cmd/narratio/`: CLI entrypoint. -- `internal/app/`: command handlers, plan/run/resume orchestration, cleanup gates, secrets loading. +- `internal/app/`: command handlers, run/stage orchestration, cleanup gates, secrets loading. - `internal/config/`: strict YAML loading, defaults, and validation. - `internal/stage/`: stage implementations and stage registry/order. - `internal/adapters/`: external boundary adapters (WhisperX, Seriatim, Audita, Scriptorium, storage, notify). diff --git a/docs/roadmap/audit.md b/docs/roadmap/audit.md deleted file mode 100644 index a6f2fa3..0000000 --- a/docs/roadmap/audit.md +++ /dev/null @@ -1,465 +0,0 @@ -# Roadmap: Code Quality and Deduplication Audit - -Status: Draft audit report - -This is a pre-1.0 implementation audit. It identifies remaining high-confidence opportunities to simplify, centralize, or clarify Narratio before release. It is report-only: no code refactors are included here. - -The current repository keeps architecture and development policy under `docs/policy/`, not at `docs/architecture.md` or `docs/development.md`. This audit used `docs/policy/architecture.md`, `docs/policy/development.md`, `docs/policy/documentation.md`, the current user/operator docs, the internal docs, code, examples, and tests. - -## 1. Executive Summary - -Overall code quality is strong. Narratio now has clear package boundaries in the important places: - -- configuration loading is strict and centralized enough for current commands; -- object-store construction is app-owned and secret-aware; -- storage adapters do not infer campaign/session/run semantics; -- S3 key construction and local workspace paths are mostly centralized in `internal/artifacts`; -- artifact source vocabulary has a dedicated `internal/artifactpolicy` package; -- remote current-state loading is centralized in `internal/artifacts`; -- cleanup path safety and temp object downloads have shared helpers; -- publish terminology has replaced the old archive/promote surface in live code. - -There is no major architectural risk and no reason to delay 1.0 for a broad rewrite. The remaining opportunities are targeted cleanup items where a future bug fix could otherwise need changes in several files. - -Top three refactoring targets before 1.0: - -1. Finish consolidating artifact source policy for Scriptorium inputs and previous-session candidates. -2. Centralize session-relative path conversion and atomic file install/copy mechanics. -3. Extract a small read-only inspection/preflight layer used by `session validate` and `session status`. - -The codebase is ready for a limited cleanup pass. Avoid speculative abstractions. - -## 2. High-Confidence Deduplication Opportunities - -### Complete Artifact Source Policy for Scriptorium Inputs - -Affected files/packages: - -- `internal/artifactpolicy` -- `internal/config/validate.go` -- `internal/stage/analyze.go` -- `internal/artifacts/artifact_resolver.go` -- `internal/previouscache/previouscache.go` -- tests under `internal/config`, `internal/stage`, `internal/artifacts`, and `internal/previouscache` - -Duplicated or near-duplicated behavior: - -- `artifactpolicy.ClassifySource` knows built-in, configured, and previous-session source families. -- `config.validateScriptoriumInputSource` still implements Scriptorium-input-specific validation, including previous-session source parsing, configured artifact lookups, and static built-in checks. -- `stage.resolveScriptoriumInput` classifies sources, then maps missing configured/previous/built-in sources to stage-specific required/optional behavior. -- `previouscache.artifactRelativePathCandidates` reconstructs previous-session artifact candidates from manifest outputs, published paths, and configured output paths. -- `artifacts.ResolveSessionArtifactWithCatalog` and `ResolvePreviousSessionArtifactWithCatalog` own runtime resolution, but caller-side policy still reaches into source details. - -Why it matters: - -Artifact source IDs are public configuration. They are used by Scriptorium inputs, previous-session inputs, publish outputs, locks, status, artifacts listing, restore, and validation. `internal/artifactpolicy` solved much of this, but Scriptorium input validation and previous-session candidate derivation still contain source-vocabulary logic outside the policy layer. - -Recommended refactor: - -Extend `internal/artifactpolicy` with narrow Scriptorium-input helpers, not a generic artifact engine: - -- `ValidateScriptoriumInputSource(source, configuredKeys)` returning a classified source plus any referenced configured artifact key. -- a shared helper for "is this source a built-in runtime input source?" -- a small previous-session source descriptor used by config validation, previous-cache planning, and analyze resolution. - -Keep missing/required behavior at call sites. For example, `analyze` should still decide whether a missing optional input is skipped or a missing required input fails with prepare guidance. - -Suggested tests: - -- `internal/artifactpolicy`: valid/invalid Scriptorium input sources, previous-session source format, unknown configured references. -- `internal/config`: Scriptorium input validation still reports field-specific errors. -- `internal/stage`: analyze required/optional source behavior unchanged. -- `internal/previouscache`: previous-session requirements and candidate ordering unchanged. - -Risk level: Medium. This touches public config validation, but the existing tests are good and the policy surface can remain small. - -### Centralize Session-Relative Path Conversion and Safe Local Install Mechanics - -Affected files/packages: - -- `internal/app/restore_plan.go` -- `internal/app/restore_execute.go` -- `internal/stage/run_local.go` -- `internal/stage/prepare.go` -- `internal/previouscache/previouscache.go` -- `internal/artifacts/local.go` -- `internal/audio/s3_audio.go` -- `internal/manifest/store.go` -- `internal/pathsafe` - -Duplicated or near-duplicated behavior: - -- `restore_plan.joinWithinSessionRoot` validates session-relative restore targets. -- `previouscache.relativeToSession`, `deriveManifestRelativePath`, and `manifestSessionRoot` convert local manifest paths back into session-relative paths. -- `stage.runLocalPathForCanonical` validates that canonical outputs stay within a session root and skips `previous/**`. -- `artifacts.resolveInRoot` validates relative paths under an artifact root. -- Atomic write/copy/install flows appear in `artifacts.LocalStore`, `audio.MaterializeS3Audio`, `restore_execute`, `manifest.LocalStore`, and `prepare` helper functions. - -Why it matters: - -These helpers are individually careful, but they all express the same safety policy: relative paths must not escape a scoped root, writes should use temp files plus rename, and cleanup should not leave partial outputs. This is exactly the kind of code where drift is costly. - -Recommended refactor: - -Add narrow helpers without moving application semantics: - -- in `internal/pathsafe`, add session/root helpers such as `JoinWithinRoot(root, rel)` and `RelativeWithinRoot(root, absoluteOrRelative)`; -- add a tiny file operation helper package, or extend `artifacts.LocalStore` carefully, for atomic install/copy/write with optional checksum; -- keep restore scope classification in `restore_plan.go`, because the include/exclude roots are command-specific. - -Suggested tests: - -- `internal/pathsafe`: root escape, absolute path rejection, Windows-style separators, empty paths, valid session-relative joins. -- `internal/app`: restore conflict/force behavior unchanged. -- `internal/stage`: run-local materialization paths unchanged. -- `internal/audio`: cache hit/miss behavior unchanged. -- `internal/manifest`: atomic manifest save behavior unchanged. - -Risk level: Low to Medium. The mechanics are well-contained, but file writes are sensitive and should be protected by focused tests. - -### Extract Shared Read-Only Session Inspection Checks - -Affected files/packages: - -- `internal/app/operator_findings.go` -- `internal/app/operator_session_validate.go` -- `internal/app/operator_status.go` -- `internal/stage/prepare.go` -- `internal/previouscache/previouscache.go` -- `internal/audio` -- `internal/artifacts/current_state.go` - -Duplicated or near-duplicated behavior: - -- `session validate` resolves stable input paths and checks files in a way that mirrors prepare. -- `session validate` checks local audio or remote audio by listing S3 audio objects, while prepare has the materialization path and restore has audio cache materialization. -- `session validate` checks previous-session current state, while previous-cache planning performs a deeper check and restore uses `previouscache.BuildPlan`. -- `status` independently loads local manifest state, remote current state, effective locks, and published output state. - -Why it matters: - -These commands are read-only, so divergence does not corrupt state. But operator trust depends on them matching runtime behavior. If validate says a session is ready while prepare would fail, or status reports a remote condition differently from restore, operators get confusing guidance. - -Recommended refactor: - -Create a small app-level inspection layer, not a workflow engine: - -- one helper for resolved stable-input file checks; -- one helper for audio presence checks that mirrors prepare's selection rules without downloading bodies; -- one helper that can run previous-cache planning in a "metadata only / no body download" mode where possible; -- one helper result type that status and validate can render differently. - -Do not move output formatting into this layer. Keep command-specific text in `internal/app`. - -Suggested tests: - -- `internal/app`: validate/status agree on local vs remote audio state. -- `internal/app`: previous-session missing current pointer/manifest is rendered consistently. -- `internal/previouscache`: optional and required previous inputs keep existing behavior. -- `internal/stage`: prepare behavior remains the authority for actual materialization. - -Risk level: Medium. The semantic differences between status, validate, prepare, and restore are intentional in places, so the helper should centralize checks, not command policy. - -### Atomic File Operation Helpers Are Repeated - -Affected files/packages: - -- `internal/artifacts/local.go` -- `internal/audio/s3_audio.go` -- `internal/app/restore_execute.go` -- `internal/manifest/store.go` -- `internal/stage/prepare.go` - -Duplicated or near-duplicated behavior: - -- create parent directory; -- create sibling temp file; -- copy/write bytes; -- sync/close; -- chmod; -- rename; -- remove temp file on failure; -- sometimes compute SHA-256 while copying. - -Why it matters: - -The duplication is not large, but these are correctness-sensitive operations. A future hardening change, such as more consistent `fsync`, permissions, or Windows behavior, would need repeated edits. - -Recommended refactor: - -Add a small dependency-light file helper, for example `internal/fileops`, with: - -- `WriteAtomic(path, data, perm, pattern)`; -- `CopyAtomic(src, dst, perm)`; -- `CopyAtomicWithSHA256(src, dst, perm)`; -- `InstallDownloadedTemp(tmp, dst, perm)`. - -Keep manifest JSON marshaling, audio cache policy, restore plan policy, and artifact store semantics in their existing packages. - -Suggested tests: - -- temp file removed on write/copy failure; -- destination parent creation; -- checksum from copy matches final file; -- restore and audio cache tests continue to pass. - -Risk level: Low. This is mechanical but should be done in a small commit. - -## 3. Medium-Confidence Opportunities - -### CLI Parsing Is Mostly Centralized, With Intentional Special Cases - -Affected files/packages: - -- `internal/app/session_args.go` -- `internal/app/operator_helpers.go` -- `internal/app/run.go` -- `internal/app/resume.go` -- `internal/app/run_stage.go` -- `internal/app/operator_locks.go` -- `internal/app/restore.go` - -Current state: - -Common config flags and session ID matching are centralized enough for 1.0. `run-stage`, `locks add`, and `locks remove` still do command-specific positional parsing because they have extra positional arguments. - -Recommended refactor: - -Do not introduce a CLI framework. If another session command is added, consider a small parser helper for "session id plus one additional positional argument" so `locks add/remove` and any future similar commands share the same mismatch behavior. - -Suggested tests: - -- existing session-oriented CLI tests; -- positional session ID plus `--session-id` mismatch; -- missing extra positional source for locks add/remove. - -Risk level: Low. - -### Restore Scope Mapping Should Probably Stay Local - -Affected files/packages: - -- `internal/app/restore_plan.go` -- `docs/internal/command-restore.md` - -Current state: - -Restore maps remote keys back to local paths and intentionally includes only `manifest.json`, `transcripts/**`, `artifacts/**`, and optionally `audio/**`. It explicitly excludes current pointers, run history, logs, reports, configs, inputs, and current-session `previous/**`. - -Why it is not a high-priority refactor: - -This policy is restore-specific. Generalizing it too much would obscure the command contract. - -Recommended refactor: - -Leave it local unless restore gains another caller. If it grows, extract only the classifier into a table-driven helper with tests. - -Risk level: Low. - -### Command Output Formatting Is Intentionally Text-Local - -Affected files/packages: - -- `internal/app/operator_findings.go` -- `internal/app/operator_artifact_rendering.go` -- `internal/app/operator_locks.go` -- `internal/app/restore_report.go` -- `internal/app/clean.go` -- `internal/app/plan.go` - -Current state: - -Each command renders plain text directly. Validation-style findings share a small renderer. Artifact list/status share artifact rendering. - -Recommended refactor: - -Do not add a generic renderer before 1.0. Add focused helper functions only if a user-facing inconsistency is found. - -Risk level: Low. - -### Manifest Transition Logic Is Dense but Correctly Centralized - -Affected files/packages: - -- `internal/app/runner.go` -- `internal/manifest` - -Current state: - -The runner owns manifest/run-manifest lifecycle, stage transitions, downstream stale marking, and post-publish cleanup gating. The function is long, but it is one explicit orchestration path. - -Recommended refactor: - -Avoid a broad manifest abstraction. If desired after 1.0, extract tiny helpers for repeated save/error wrapping inside the runner only. - -Risk level: Low. - -## 4. Boundary and Responsibility Concerns - -Healthy boundaries: - -- `internal/adapters/storage` exposes only `ObjectStore`; AWS SDK types stay inside the S3 backend. -- App code owns secret loading and object-store initialization. -- `internal/artifacts` owns local path and S3 key helpers. -- `internal/artifactpolicy` owns most public source vocabulary. -- `internal/audio` owns S3 audio cache materialization. -- Stage code remains explicit and adapter-facing. - -Boundary concerns: - -- Scriptorium input source validation still lives partly in `internal/config` instead of fully using source policy. -- Previous-cache candidate selection mixes manifest interpretation, publish metadata, and configured artifact output paths in `internal/previouscache`; this is defensible but should share source descriptors with artifact policy. -- Read-only operator checks mirror stage behavior in `internal/app`; if they grow, they should move into a small inspection layer rather than further expanding command handlers. -- File operation mechanics are repeated across packages; a narrow helper would clarify that write safety is shared mechanics, not stage policy. - -Recommended homes: - -- source vocabulary and validation: `internal/artifactpolicy`; -- concrete runtime artifact lookup: `internal/artifacts`; -- remote current-state mechanics: `internal/artifacts`; -- command orchestration and rendering: `internal/app`; -- file write mechanics: a small dependency-light helper such as `internal/fileops`, or carefully scoped methods on `artifacts.LocalStore`; -- stage execution policy: `internal/stage`. - -## 5. Path and Remote Key Construction Review - -Remote key construction is centralized enough for 1.0: - -- session, run, audio, session config, locks, current-state, published output, and run-relative keys are in `internal/artifacts/s3_keys.go`; -- storage adapters normalize object keys but do not infer Narratio semantics; -- publish identity helpers resolve bucket/session/run/current-state identities without moving layout policy into storage. - -Local path construction is mostly centralized: - -- session roots, run roots, previous cache paths, spool paths, and S3 audio cache paths are in `internal/artifacts/paths.go`; -- cleanup target validation is in `internal/app/cleanup_targets.go`; -- relative publish destinations use `internal/pathsafe`. - -Areas worth cleanup: - -- session-relative conversion helpers are repeated in restore, previous-cache, run-local stage code, and artifacts local-store helpers; -- `restore_plan.go` has local remote-key normalization, which is acceptable for restore but should not spread; -- tests still build some S3 keys through string concatenation when fixture readability would not suffer from using helpers. - -Recommended action: - -Add narrow `pathsafe` helpers for "join/rel within root" and use them where they reduce escape-check duplication. Do not move restore's include/exclude scope policy out of restore unless it gains another caller. - -## 6. Artifact/Catalog/Source Resolution Review - -Current state is good: - -- transcript source IDs live in `internal/artifactmodel` and are exposed through `internal/artifacts`; -- configured and previous-session source formats live in `internal/artifactpolicy`; -- publish output destination derivation is centralized in `artifactpolicy.ResolvePublishedDestination`; -- runtime catalog behavior is in `internal/artifacts`; -- previous-session requirements are collected by `artifacts.CollectPreviousArtifactRequirements`. - -Remaining gap: - -`artifactpolicy` does not yet cover the full Scriptorium-input validation contract. `internal/config/validate.go` still knows too much about source parsing and built-in source support. `stage/analyze.go` still has source-family-specific missing input messages, which is appropriate, but it should be consuming a richer policy classification rather than re-checking source strings. - -Recommendation: - -Extend the policy layer one step further, but keep runtime existence checks in `internal/artifacts` and command/stage missing behavior at the call sites. - -## 7. Config and Command-Loading Review - -Config loading is consistent: - -- `loadCommandConfig` handles pipeline, campaign, local session discovery, remote session fallback, and session identity checks; -- `loadPipelineCampaignConfig` correctly supports `session init`, which cannot load an existing session; -- `newCommandObjectStore` loads secret files before constructing storage; -- concrete-only session loading is enforced in `internal/config`. - -Intentional differences: - -- `session init` loads pipeline and campaign only. -- `clean --all` is pipeline-scoped, not session-scoped. -- `status`, `validate`, `artifacts`, and `locks` render command-specific output after shared loading. -- `restore` has richer help/output behavior and therefore does slightly more local parsing setup. - -Likely accidental drift remaining: - -- small positional parsing patterns for commands with extra arguments are repeated; -- `session validate` and `status` each decide which remote checks are warnings, errors, or status lines; -- `loadHelperContext` is useful but not used by every helper because some commands need partial failure reporting. - -Recommendation: - -No broad loader rewrite is needed. Future command work should reuse `commonConfigFlags`, `parseSessionAwareFlags`, `loadCommandConfig`, `loadPipelineCampaignConfig`, and `newCommandObjectStore`. - -## 8. Refactors to Avoid Before 1.0 - -Avoid: - -- a generic workflow engine or DAG abstraction; -- a generic CLI framework; -- broad manifest query or transition abstractions; -- moving secret loading into storage adapters; -- making storage adapters infer campaign/session/root-prefix semantics; -- centralizing all command text output into a generic renderer; -- merging audio cache materialization with generic restore downloads; -- compatibility aliases for retired archive/promote, legacy campaign, or legacy transcript names; -- moving restore's command-specific scope policy into storage or artifacts. - -These would add risk without solving current release problems. - -## 9. Recommended Implementation Sequence - -1. Add root-scoped path helpers and optional atomic file helpers. - - Scope: `internal/pathsafe` plus a small file helper if chosen. - - Tests: `internal/pathsafe`, `internal/audio`, `internal/app -run Restore`, `internal/stage -run Prepare`. - -2. Extend artifact source policy for Scriptorium inputs. - - Scope: `internal/artifactpolicy`, `internal/config/validate.go`, `internal/stage/analyze.go`, `internal/previouscache`. - - Tests: `internal/artifactpolicy`, `internal/config`, `internal/stage -run Analyze`, `internal/previouscache`. - -3. Extract read-only session inspection checks. - - Scope: stable inputs, audio presence, previous-session readiness, locks/current-state checks. - - Tests: `internal/app -run 'SessionValidate|Status'`, `internal/previouscache`, `internal/stage -run Prepare`. - -4. Trim command parsing edge duplication only if needed. - - Scope: commands with session ID plus one extra positional argument. - - Tests: `internal/app -run 'Session|Locks|RunStage'`. - -5. Final sweep. - - Run focused searches for retired terminology and old source names. - - Run the focused package tests listed below and then `go test ./...`. - -## 10. Test Strategy - -Focused checks for any cleanup work: - -- `go test ./internal/artifactpolicy -v` -- `go test ./internal/artifacts -v` -- `go test ./internal/config -v` -- `go test ./internal/stage -run 'Analyze|Prepare|Publish' -v` -- `go test ./internal/app -run 'SessionValidate|Status|Restore|Locks|RunStage' -v` -- `go test ./internal/previouscache -v` -- `go test ./internal/audio -v` -- `go test ./internal/pathsafe -v` -- `go test ./internal/adapters/storage -v` -- `go test ./internal/manifest -v` - -Full validation after each implementation prompt: - -- `go test ./...` - -Useful final searches: - -- `rg -n "archive|promote|promoted|promotion" internal docs examples cmd` -- `rg -n "narratio.transcript.merged|narratio.transcript.full|narratio.transcript.trimmed" internal docs examples` -- `rg -n "previous_session_artifact|promote_artifacts|pipeline.archive" internal docs examples` -- `rg -n "session_id is required|unexpected positional|--artifacts" internal/app` -- `rg -n "CreateTemp|Rename|copyFileAtomic|WriteFileAtomic|DownloadObjectToTemp" internal` - -## 11. Appendix: Findings Not Worth Acting On - -- Restore's include/exclude scope logic should remain restore-local. It is command policy, not general path policy. -- Direct text rendering in command handlers is acceptable. The output is text-only and command-specific by design. -- Stage-local path joins under run-local directories are acceptable when they use established session/run roots. -- `run-stage`, `locks add`, and `locks remove` deserve explicit positional parsing because their syntax is not identical to simple session commands. -- The runner is long, but it is the right place for explicit stage orchestration and manifest transitions. -- `session init` template rendering should remain separate from ordinary concrete session loading. -- S3 audio cache materialization is intentionally special and should not be folded into generic object download logic. diff --git a/docs/roadmap/cleanup.md b/docs/roadmap/cleanup.md deleted file mode 100644 index 10d1dac..0000000 --- a/docs/roadmap/cleanup.md +++ /dev/null @@ -1,206 +0,0 @@ -# Roadmap: Pre-1.0 Code Cleanup - -Status: Implemented - -This roadmap turns the remaining findings in `docs/roadmap/audit.md` into decision-complete implementation stages. It follows the policy documents under `docs/policy/` and preserves current public CLI, config, storage layout, manifest, and stage behavior unless a stage explicitly says otherwise. - -## Goals - -- Reduce repeated path, file, source-policy, and read-only inspection logic before 1.0. -- Keep Narratio explicit, stage-driven, and easy to review. -- Keep storage adapters free of campaign/session/run/root-prefix semantics. -- Keep command output text-only and command-specific. -- Keep implementation changes small enough for focused prompts and focused tests. - -## Non-Goals - -- Do not introduce a generic workflow engine or DAG abstraction. -- Do not introduce a generic CLI framework. -- Do not introduce a broad manifest abstraction. -- Do not move secret loading into storage adapters. -- Do not make storage adapters infer Narratio path or key semantics. -- Do not add compatibility aliases for retired archive/promote, campaign, transcript, or previous-session source names. -- Do not move restore's remote-key include/exclude policy out of restore unless another caller is added. - -## Stage 1: Path and File Mechanics (Implemented) - -Add shared mechanics for root-scoped path safety and atomic file operations. - -Implementation decisions: - -- Add root-scoped helpers in `internal/pathsafe`: - - join a slash-style relative path safely under a root; - - derive a safe slash-style relative path from a path under a root; - - reject empty paths, absolute paths, traversal, and paths outside the root. -- Add a small dependency-light `internal/fileops` package for shared file installation mechanics: - - atomic byte write; - - atomic file copy; - - atomic file copy with SHA-256 checksum; - - install an already-downloaded temp file with permissions. -- Prefer `internal/fileops` over extending `artifacts.LocalStore`, because the same mechanics are used by app, stage, audio, artifacts, and manifest code. -- Keep higher-level semantics local: - - restore decides which remote keys are in scope; - - audio decides cache hit/miss behavior; - - manifest decides JSON marshaling and validation; - - stages decide canonical output materialization. - -Implementation targets: - -- Replace duplicate root-escape checks in restore planning, run-local output handling, previous-cache path conversion, and artifact local path helpers where doing so keeps behavior identical. -- Replace duplicate temp-write/copy/rename mechanics in artifact local store, audio cache materialization, restore execution, and prepare helpers where the call site can keep its current error context. -- Leave manifest save behavior unchanged if sharing it would obscure manifest-specific validation or error text. - -Tests: - -- `go test ./internal/pathsafe -v` -- `go test ./internal/audio -v` -- `go test ./internal/app -run Restore -v` -- `go test ./internal/stage -run Prepare -v` -- `go test ./...` - -Acceptance criteria: - -- Root escape, absolute path, empty path, Windows separator, and valid relative path cases are covered by path-safe tests. -- Atomic helper tests prove temp files are cleaned up on failure and checksums match final file contents. -- Restore, prepare, and audio cache behavior remain unchanged. - -## Stage 2: Artifact Source Policy (Implemented) - -Finish centralizing artifact source vocabulary and validation in `internal/artifactpolicy`. - -Implementation decisions: - -- Extend `internal/artifactpolicy` with Scriptorium-input policy helpers: - - classify and validate built-in, configured, and previous-session source IDs; - - validate referenced configured artifact keys against the configured artifact set; - - expose a previous-session source descriptor for callers that need the configured artifact key. -- Keep runtime artifact lookup in `internal/artifacts`. -- Keep missing, required, optional, and operator-guidance behavior at call sites: - - config validation still produces field-specific errors; - - analyze still decides whether missing inputs fail or skip; - - previous-cache planning still decides required vs optional behavior. -- Do not make `artifactpolicy` inspect manifests, files, object storage, or runtime catalogs. - -Implementation targets: - -- Replace source parsing and static built-in checks in config validation with artifactpolicy helpers. -- Update analyze input resolution to consume the shared classification/descriptors while preserving current error messages and required/optional behavior. -- Update previous-cache planning to use shared previous-session source descriptors where source vocabulary is involved. -- Keep publish output destination derivation through `artifactpolicy.ResolvePublishedDestination`. - -Tests: - -- `go test ./internal/artifactpolicy -v` -- `go test ./internal/config -v` -- `go test ./internal/stage -run Analyze -v` -- `go test ./internal/previouscache -v` -- `go test ./...` - -Acceptance criteria: - -- Valid Scriptorium input source cases pass through one shared policy path. -- Invalid source format and unknown configured artifact references keep clear config-field errors. -- Analyze behavior for required/optional built-in, configured, and previous-session sources is unchanged. -- Previous-cache candidate ordering and required/optional behavior are unchanged. - -## Stage 3: Read-Only Inspection Layer (Implemented) - -Extract shared read-only session inspection checks for `session validate` and `session status`. - -Implementation decisions: - -- Keep the new inspection helpers in `internal/app`; they are command orchestration helpers, not stage or storage adapter behavior. -- Create small result types for checks, but do not create a generic reporting framework. -- Keep command-specific rendering local: - - `session validate` renders findings and fails on `ERROR`; - - `session status` renders state and does not fail for missing local/remote state unless config loading fails. -- Do not download artifact bodies for inspection unless current behavior already does so. - -Implementation targets: - -- Extract stable input checks from `operator_findings.go` into a reusable inspection helper. -- Extract local and remote audio presence checks that mirror prepare's selection rules without materializing audio. -- Extract previous-session readiness checks using existing current-state and previous-cache planning mechanics where possible. -- Extract effective lock and remote current-state inspection into reusable command helpers. -- Keep artifact catalog rendering separate from these checks. - -Tests: - -- `go test ./internal/app -run 'SessionValidate|Status' -v` -- `go test ./internal/previouscache -v` -- `go test ./internal/stage -run Prepare -v` -- `go test ./...` - -Acceptance criteria: - -- `session validate` and `session status` agree on local/remote audio and previous-session readiness facts. -- Missing current run pointer/manifest behavior remains command-appropriate: validation reports an error; status reports unavailable state. -- Existing lock and remote current-state behavior is unchanged. - -## Stage 4: Optional CLI Edge Cleanup (Implemented) - -Only implement this stage if Stage 1-3 leave meaningful repeated parser code. - -Implementation decisions: - -- Add at most one small parser helper for commands with `session_id` plus one additional positional argument. -- Use it for `session locks add` and `session locks remove` if it reduces duplication without obscuring syntax. -- Keep command handlers explicit. -- Do not change public syntax, flag names, help text meaning, or error semantics. - -Tests: - -- `go test ./internal/app -run 'Session|Locks|RunStage' -v` -- `go test ./...` - -Acceptance criteria: - -- Positional `session_id` and `--session-id` mismatch errors remain unchanged. -- Missing source arguments for lock add/remove remain clear. -- No new command aliases are introduced. - -## Stage 5: Final Sweep (Implemented) - -Run final validation after the implementation stages. - -Required searches: - -- `rg -n "archive|promote|promoted|promotion" internal docs examples cmd` -- `rg -n "narratio.transcript.merged|narratio.transcript.full|narratio.transcript.trimmed" internal docs examples` -- `rg -n "previous_session_artifact|promote_artifacts|pipeline.archive" internal docs examples` -- `rg -n "CreateTemp|Rename|copyFileAtomic|WriteFileAtomic|DownloadObjectToTemp" internal` - -Expected search results: - -- Retired terminology should remain only where intentionally historical or where fixture names make it unrelated to current behavior. -- Old transcript and old config/source names should not appear in runtime code, tests, examples, or current-behavior docs. -- File-operation searches should show centralized helpers plus acceptable direct uses where package-specific behavior remains intentional. - -Required tests: - -- `go test ./internal/artifactpolicy -v` -- `go test ./internal/artifacts -v` -- `go test ./internal/config -v` -- `go test ./internal/stage -run 'Analyze|Prepare|Publish' -v` -- `go test ./internal/app -run 'SessionValidate|Status|Restore|Locks|RunStage' -v` -- `go test ./internal/previouscache -v` -- `go test ./internal/audio -v` -- `go test ./internal/pathsafe -v` -- `go test ./internal/adapters/storage -v` -- `go test ./internal/manifest -v` -- `go test ./...` - -Roadmap cleanup: - -- Mark each completed stage as implemented only after its code, tests, and any internal docs are updated. -- Keep this file as planned work until the implementation stages land. -- Do not update canonical user/operator docs unless implementation changes visible behavior, which this roadmap does not intend. - -## Assumptions - -- Public CLI and config behavior remains unchanged throughout this cleanup. -- `internal/fileops` is the preferred home for shared atomic file mechanics. -- `internal/artifactpolicy` remains the source-policy home. -- `internal/artifacts` remains the path/key/current-state home. -- `internal/app` remains the command-loading, inspection, and rendering home. -- Stage packages keep stage execution policy and adapter interaction. diff --git a/internal/app/analyze_artifacts_commands_test.go b/internal/app/analyze_artifacts_commands_test.go index 6d060c2..7a8fc81 100644 --- a/internal/app/analyze_artifacts_commands_test.go +++ b/internal/app/analyze_artifacts_commands_test.go @@ -120,7 +120,7 @@ func TestRunStageArtifactsDoesNotImplyForce(t *testing.T) { } } -func TestResumeArtifactsWithSucceededAnalyzeSkipsUnlessForced(t *testing.T) { +func TestRunArtifactsWithSucceededAnalyzeSkipsUnlessForced(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot) manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json") @@ -135,16 +135,16 @@ func TestResumeArtifactsWithSucceededAnalyzeSkipsUnlessForced(t *testing.T) { } var out bytes.Buffer - err := Resume( + err := Run( context.Background(), []string{"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--artifacts", "session_recap"}, &out, ) if err != nil { - t.Fatalf("Resume() error = %v", err) + t.Fatalf("Run() error = %v", err) } - if !strings.Contains(out.String(), "has no remaining stages") { - t.Fatalf("output = %q, want no remaining stages", out.String()) + if !strings.Contains(out.String(), "executed=0 skipped=9") { + t.Fatalf("output = %q, want all stages skipped", out.String()) } } diff --git a/internal/app/commands.go b/internal/app/commands.go index 83a6ac3..f47759f 100644 --- a/internal/app/commands.go +++ b/internal/app/commands.go @@ -7,7 +7,7 @@ import ( "strings" ) -var supportedCommands = []string{"run", "run-stage", "resume", "analyze", "publish", "clean", "session"} +var supportedCommands = []string{"run", "run-stage", "analyze", "publish", "clean", "session"} // Execute dispatches CLI commands and returns a process exit code. func Execute(args []string, stdout, stderr io.Writer) int { @@ -24,8 +24,6 @@ func Execute(args []string, stdout, stderr io.Writer) int { switch cmd { case "run": err = Run(ctx, cmdArgs, stdout) - case "resume": - err = Resume(ctx, cmdArgs, stdout) case "run-stage": err = RunStage(ctx, cmdArgs, stdout) case "analyze": diff --git a/internal/app/commands_test.go b/internal/app/commands_test.go index f74c333..a8933ec 100644 --- a/internal/app/commands_test.go +++ b/internal/app/commands_test.go @@ -34,7 +34,6 @@ func TestExecuteValidCommands(t *testing.T) { {name: "run", args: []string{"run", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, wantOut: "narratio run: session 2026-05-03; executed=9 skipped=0; manifest="}, {name: "session plan", args: []string{"session", "plan", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, wantOut: "prepare: skip\ntranscribe: skip\nmerge: skip\npolish: skip\nnormalize: skip\ntrim: skip\nanalyze: skip\npublish: skip\nnotify: skip"}, {name: "session status", args: []string{"session", "status", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, wantOut: "Session: 2026-05-03"}, - {name: "resume", args: []string{"resume", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, wantOut: "narratio resume: session 2026-05-03 has no remaining stages"}, {name: "run-stage", args: []string{"run-stage", "polish", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, wantOut: "narratio run-stage: stage=polish executed=0 skipped=1 force=false; manifest="}, } @@ -66,7 +65,7 @@ func TestExecuteMissingRequiredFlags(t *testing.T) { {name: "run missing session", args: []string{"run"}, want: "run: session_id is required"}, {name: "plan old top-level removed", args: []string{"plan"}, want: `unknown command: "plan"`}, {name: "status old top-level removed", args: []string{"status"}, want: `unknown command: "status"`}, - {name: "resume missing session", args: []string{"resume"}, want: "resume: session_id is required"}, + {name: "resume removed", args: []string{"resume"}, want: `unknown command: "resume"`}, {name: "run-stage missing name", args: []string{"run-stage", "--config", "a", "--session", "b"}, want: "run-stage: expected stage name and session_id"}, {name: "run-stage missing session", args: []string{"run-stage", "polish"}, want: "run-stage: expected stage name and session_id"}, {name: "run missing config uses defaults", args: []string{"run", "2026-05-03", "--session", "session.yml"}, want: "run: no pipeline config path provided and no default pipeline config found; searched:"}, diff --git a/internal/app/resume.go b/internal/app/resume.go deleted file mode 100644 index d3f2418..0000000 --- a/internal/app/resume.go +++ /dev/null @@ -1,102 +0,0 @@ -package app - -import ( - "context" - "flag" - "fmt" - "io" - - "gitea.maximumdirect.net/eric/narratio/internal/artifacts" - "gitea.maximumdirect.net/eric/narratio/internal/config" - "gitea.maximumdirect.net/eric/narratio/internal/manifest" -) - -// Resume continues execution from the first non-succeeded stage in the manifest. -func Resume(ctx context.Context, args []string, out io.Writer) error { - fs := flag.NewFlagSet("resume", flag.ContinueOnError) - fs.SetOutput(io.Discard) - - var flags commonConfigFlags - var force bool - var selectedArtifacts artifactSelectionFlag - addCommonConfigFlags(fs, &flags) - fs.BoolVar(&force, "force", false, "force stage execution") - fs.Var(&selectedArtifacts, "artifacts", "configured artifact names to execute and publish (comma-separated or repeatable)") - - if err := parseSessionAwareFlags("resume", fs, args, &flags.sessionID); err != nil { - return err - } - if flags.sessionID == "" { - return fmt.Errorf("resume: session_id is required") - } - cfg, err := loadCommandConfig(ctx, flags.pipelinePath, flags.campaignPath, flags.campaignFilePath, flags.sessionPath, flags.sessionOptions()) - if err != nil { - return fmt.Errorf("resume: %w", err) - } - if err := config.Validate(cfg); err != nil { - return fmt.Errorf("resume: %w", err) - } - normalizedArtifacts, err := selectedArtifacts.Normalize() - if err != nil { - return fmt.Errorf("resume: invalid --artifacts: %w", err) - } - if err := validateSelectedArtifacts(cfg, normalizedArtifacts); err != nil { - return fmt.Errorf("resume: %w", err) - } - - full := BuildFullPlan() - selected := full - if !force { - m, err := loadManifestIfPresent(ctx, cfg) - if err != nil { - return fmt.Errorf("resume: %w", err) - } - if m != nil { - start := firstNonSucceededIndex(full, m) - if start >= len(full) { - _, err := fmt.Fprintf(out, "narratio resume: session %s has no remaining stages\n", cfg.Session.SessionID) - return err - } - selected = full[start:] - } - } - - summary, err := executeStagesFn(ctx, cfg, selected, RunOptions{ - Force: force, - SelectedArtifacts: normalizedArtifacts, - }) - if err != nil { - return fmt.Errorf("resume: %w", err) - } - - _, err = fmt.Fprintf( - out, - "narratio resume: session %s; executed=%d skipped=%d; manifest=%s\n", - summary.SessionID, - len(summary.Executed), - len(summary.Skipped), - summary.ManifestPath, - ) - return err -} - -func loadManifestIfPresent(ctx context.Context, cfg *config.Config) (*manifest.Manifest, error) { - path := artifacts.SessionManifestPathForCampaign( - cfg.Pipeline.Workspace.Root, - cfg.Session.Campaign, - cfg.Session.SessionID, - ) - exists, err := fileExists(path) - if err != nil { - return nil, fmt.Errorf("check manifest %q: %w", path, err) - } - if !exists { - return nil, nil - } - store := &manifest.LocalStore{} - m, err := store.Load(ctx, path) - if err != nil { - return nil, fmt.Errorf("load manifest %q: %w", path, err) - } - return m, nil -} diff --git a/internal/app/run_control.go b/internal/app/run_control.go index 6e23ac0..abeae11 100644 --- a/internal/app/run_control.go +++ b/internal/app/run_control.go @@ -1,8 +1,12 @@ package app import ( + "context" + "fmt" "time" + "gitea.maximumdirect.net/eric/narratio/internal/artifacts" + "gitea.maximumdirect.net/eric/narratio/internal/config" "gitea.maximumdirect.net/eric/narratio/internal/manifest" "gitea.maximumdirect.net/eric/narratio/internal/stage" ) @@ -43,13 +47,25 @@ func stageSucceeded(m *manifest.Manifest, name string) bool { return sr != nil && sr.Status == manifest.StatusSucceeded } -func firstNonSucceededIndex(stages []stage.Stage, m *manifest.Manifest) int { - for i, s := range stages { - if !stageSucceeded(m, s.Name()) { - return i - } +func loadManifestIfPresent(ctx context.Context, cfg *config.Config) (*manifest.Manifest, error) { + path := artifacts.SessionManifestPathForCampaign( + cfg.Pipeline.Workspace.Root, + cfg.Session.Campaign, + cfg.Session.SessionID, + ) + exists, err := fileExists(path) + if err != nil { + return nil, fmt.Errorf("check manifest %q: %w", path, err) } - return len(stages) + if !exists { + return nil, nil + } + store := &manifest.LocalStore{} + m, err := store.Load(ctx, path) + if err != nil { + return nil, fmt.Errorf("load manifest %q: %w", path, err) + } + return m, nil } func canonicalStageNames() []string { diff --git a/internal/app/run_control_test.go b/internal/app/run_control_test.go index 6a106d6..6953e01 100644 --- a/internal/app/run_control_test.go +++ b/internal/app/run_control_test.go @@ -8,18 +8,6 @@ import ( "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) -func TestFirstNonSucceededIndex(t *testing.T) { - stages := BuildFullPlan() - m := manifest.New("2026-05-03", time.Now().UTC()) - m.MarkStageSucceeded("prepare", time.Now().UTC(), nil) - m.MarkStageSucceeded("transcribe", time.Now().UTC(), nil) - - got := firstNonSucceededIndex(stages, m) - if got != 2 { - t.Fatalf("firstNonSucceededIndex() = %d, want 2", got) - } -} - func TestDecideStageActions(t *testing.T) { stages := BuildFullPlan()[:2] m := manifest.New("2026-05-03", time.Now().UTC()) diff --git a/internal/app/resume_run_stage_test.go b/internal/app/run_stage_test.go similarity index 87% rename from internal/app/resume_run_stage_test.go rename to internal/app/run_stage_test.go index 3b375e6..a8228bd 100644 --- a/internal/app/resume_run_stage_test.go +++ b/internal/app/run_stage_test.go @@ -13,7 +13,7 @@ import ( "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) -func TestResumeStartsAfterCompletedStages(t *testing.T) { +func TestRunContinuesAfterCompletedStages(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json") @@ -32,12 +32,12 @@ func TestResumeStartsAfterCompletedStages(t *testing.T) { mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "glossary.yml"), "terms: []\n") var out bytes.Buffer - err := Resume(context.Background(), []string{"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, &out) + err := Run(context.Background(), []string{"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, &out) if err != nil { - t.Fatalf("Resume() error = %v", err) + t.Fatalf("Run() error = %v", err) } - if !strings.Contains(out.String(), "executed=7 skipped=0") { - t.Fatalf("output = %q, want executed=7 skipped=0", out.String()) + if !strings.Contains(out.String(), "executed=7 skipped=2") { + t.Fatalf("output = %q, want executed=7 skipped=2", out.String()) } loaded, err := store.Load(context.Background(), manifestPath) @@ -49,7 +49,7 @@ func TestResumeStartsAfterCompletedStages(t *testing.T) { } } -func TestResumeNoRemainingStages(t *testing.T) { +func TestRunNoRemainingStagesRecordsSkippedStages(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json") @@ -64,20 +64,20 @@ func TestResumeNoRemainingStages(t *testing.T) { } var out bytes.Buffer - err := Resume(context.Background(), []string{"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, &out) + err := Run(context.Background(), []string{"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, &out) if err != nil { - t.Fatalf("Resume() error = %v", err) + t.Fatalf("Run() error = %v", err) } - if !strings.Contains(out.String(), "has no remaining stages") { - t.Fatalf("output = %q, want no remaining stages", out.String()) + if !strings.Contains(out.String(), "executed=0 skipped=9") { + t.Fatalf("output = %q, want executed=0 skipped=9", out.String()) } } -func TestResumeForceRerunsSucceeded(t *testing.T) { +func TestRunForceRerunsSucceeded(t *testing.T) { workspaceRoot := t.TempDir() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"source":"resume-force-test","segments":[{"speaker":"alice"}]}`)) + _, _ = w.Write([]byte(`{"source":"run-force-test","segments":[{"speaker":"alice"}]}`)) })) defer srv.Close() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot, srv.URL) @@ -93,9 +93,9 @@ func TestResumeForceRerunsSucceeded(t *testing.T) { } var out bytes.Buffer - err := Resume(context.Background(), []string{"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--force"}, &out) + err := Run(context.Background(), []string{"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--force"}, &out) if err != nil { - t.Fatalf("Resume() error = %v", err) + t.Fatalf("Run() error = %v", err) } if !strings.Contains(out.String(), "executed=9 skipped=0") { t.Fatalf("output = %q, want forced full rerun", out.String()) @@ -166,7 +166,7 @@ func TestRunStageSkipAndForce(t *testing.T) { } } -func TestRunStageForceMarksDownstreamStaleAndResumeContinuesFromStale(t *testing.T) { +func TestRunStageForceMarksDownstreamStaleAndRunContinuesFromStale(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json") @@ -203,12 +203,12 @@ func TestRunStageForceMarksDownstreamStaleAndResumeContinuesFromStale(t *testing } out.Reset() - err = Resume(context.Background(), []string{"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, &out) + err = Run(context.Background(), []string{"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, &out) if err != nil { - t.Fatalf("Resume() error = %v", err) + t.Fatalf("Run() error = %v", err) } - if !strings.Contains(out.String(), "executed=5 skipped=0") { - t.Fatalf("output = %q, want resume to execute normalize..notify", out.String()) + if !strings.Contains(out.String(), "executed=5 skipped=4") { + t.Fatalf("output = %q, want run to execute stale downstream stages", out.String()) } } diff --git a/internal/app/session_oriented_cli_test.go b/internal/app/session_oriented_cli_test.go index 869fa1e..3a847f9 100644 --- a/internal/app/session_oriented_cli_test.go +++ b/internal/app/session_oriented_cli_test.go @@ -162,8 +162,8 @@ func TestExecuteWorkflowCommandsAcceptPositionalSessionID(t *testing.T) { wantForce bool }{ { - name: "resume", - args: []string{"resume", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, + name: "run", + args: []string{"run", "2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath}, wantStage: "prepare", wantForce: false, },