From cee52aa09232c01b16e7dc03389eda4da6c42cce Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Fri, 22 May 2026 19:05:23 -0500 Subject: [PATCH] Updated transcript artifact names and canonical paths to use a consistent, role-based nomenclature --- docs/cli.md | 4 +- docs/config.md | 22 +- docs/integrations/audita.md | 12 +- docs/integrations/seriatim.md | 8 +- docs/internal/artifacts.md | 8 +- docs/internal/stage-merge.md | 12 +- docs/internal/stage-normalize.md | 18 +- docs/internal/stage-polish.md | 20 +- docs/internal/stage-trim.md | 20 +- docs/roadmap/transcripts.md | 210 ++++++++++++++++++ examples/pipeline.full.annotated.yml | 12 +- examples/pipeline.production.yml | 10 +- internal/adapters/audita/fake_test.go | 2 +- internal/adapters/audita/subprocess_test.go | 8 +- .../adapters/scriptorium/subprocess_test.go | 6 +- internal/adapters/seriatim/fake_test.go | 10 +- internal/adapters/seriatim/subprocess_test.go | 12 +- internal/app/commands_test.go | 4 +- internal/app/operator_helpers.go | 6 +- internal/app/operator_helpers_test.go | 70 +++--- internal/app/post_archive_cleanup_test.go | 8 +- internal/app/restore_workflow_test.go | 8 +- internal/app/resume_run_stage_test.go | 10 +- internal/app/runner_test.go | 14 +- internal/artifactmodel/transcripts.go | 84 +++++++ internal/artifacts/artifact_resolver.go | 72 +++--- internal/artifacts/artifact_resolver_test.go | 22 +- internal/artifacts/catalog.go | 6 +- internal/artifacts/catalog_test.go | 8 +- .../artifacts/previous_requirements_test.go | 2 +- internal/artifacts/s3_keys_test.go | 4 +- internal/artifacts/transcripts.go | 25 +++ internal/artifacts/transcripts_test.go | 123 ++++++++++ internal/config/defaults.go | 44 ++-- internal/config/load_validate_test.go | 4 +- internal/config/normalize_test.go | 8 +- internal/config/scriptorium_test.go | 40 +++- internal/config/storage_archive_test.go | 117 +++++++--- internal/config/trim_test.go | 16 +- internal/config/validate.go | 33 +-- internal/contracts/bounds_test.go | 8 +- internal/manifest/manifest_test.go | 4 +- internal/manifest/store_test.go | 2 +- internal/stage/analyze.go | 18 +- internal/stage/analyze_test.go | 102 ++++----- internal/stage/archive_test.go | 44 ++-- internal/stage/merge.go | 6 +- internal/stage/merge_test.go | 4 +- internal/stage/normalize.go | 12 +- internal/stage/normalize_test.go | 34 +-- internal/stage/placeholders_test.go | 8 +- internal/stage/polish.go | 12 +- internal/stage/polish_test.go | 22 +- internal/stage/run_local_test.go | 4 +- internal/stage/trim.go | 10 +- internal/stage/trim_test.go | 42 ++-- 56 files changed, 991 insertions(+), 463 deletions(-) create mode 100644 docs/roadmap/transcripts.md create mode 100644 internal/artifactmodel/transcripts.go create mode 100644 internal/artifacts/transcripts.go create mode 100644 internal/artifacts/transcripts_test.go diff --git a/docs/cli.md b/docs/cli.md index 5e4be6e..461caf0 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -361,8 +361,8 @@ Examples: ```bash narratio locks --session-id 2026-04-04 -narratio locks add --session-id 2026-04-04 --reason "manual transcript review" narratio.transcript.trimmed -narratio locks remove --session-id 2026-04-04 narratio.transcript.trimmed +narratio locks add --session-id 2026-04-04 --reason "manual transcript review" narratio.transcript.final_trimmed +narratio locks remove --session-id 2026-04-04 narratio.transcript.final_trimmed ``` ### `run-stage` diff --git a/docs/config.md b/docs/config.md index 931046b..a50d390 100644 --- a/docs/config.md +++ b/docs/config.md @@ -185,8 +185,8 @@ archive: enabled: true upload_run: true promote_artifacts: - - source: narratio.transcript.trimmed - dest: transcripts/trimmed.json + - source: narratio.transcript.final_trimmed + dest: transcripts/final.trimmed.json required: true - source: narratio.artifact.session_recap dest: artifacts/session_recap.md @@ -206,7 +206,7 @@ scriptorium: output_path: artifacts/session_recap.md inputs: transcript: - source: narratio.transcript.trimmed + source: narratio.transcript.final_trimmed required: true previous_recap: source: narratio.previous_session.artifact.session_recap @@ -280,7 +280,7 @@ Operational notes: | `pipeline.cache.s3_audio` | bool | No | `true` | | `pipeline.archive.enabled` | bool | No | `true` | | `pipeline.archive.upload_run` | bool | No | `true` | -| `pipeline.archive.promote_artifacts[]` | list | No | trimmed transcript rule | +| `pipeline.archive.promote_artifacts[]` | list | No | final-trimmed transcript rule | | `pipeline.archive.promote_artifacts[].source` | string | Yes (per rule) | none | | `pipeline.archive.promote_artifacts[].dest` | string | No | derived from source | | `pipeline.archive.promote_artifacts[].required` | bool | No | `true` | @@ -317,7 +317,7 @@ Operational notes: | `pipeline.audita.output_schema` | string | No | empty | | `pipeline.audita.work_dir_retention` | string | No | empty | | `pipeline.audita.report` | bool | No | `true` | -| `pipeline.normalize.output_path` | string | No | `transcripts/normalized.json` | +| `pipeline.normalize.output_path` | string | No | `transcripts/final.json` | | `pipeline.normalize.output_schema` | string | No | `seriatim-intermediate` | | `pipeline.normalize.report` | bool | No | `true` | | `pipeline.trim.enabled` | bool | No | `false` | @@ -366,19 +366,19 @@ Scriptorium artifact-key and dependency rules: Allowed `pipeline.scriptorium.artifacts..inputs..source` values: - `narratio.previous_session.artifact.` -- `narratio.transcript.merged` +- `narratio.transcript.base` - `narratio.transcript.polished` -- `narratio.transcript.full` -- `narratio.transcript.trimmed` +- `narratio.transcript.final` +- `narratio.transcript.final_trimmed` - `narratio.bounds.session` - `narratio.artifact.` `pipeline.archive.promote_artifacts[].source` values: -- `narratio.transcript.merged` +- `narratio.transcript.base` - `narratio.transcript.polished` -- `narratio.transcript.full` -- `narratio.transcript.trimmed` +- `narratio.transcript.final` +- `narratio.transcript.final_trimmed` - `narratio.bounds.session` - `narratio.artifact.` diff --git a/docs/integrations/audita.md b/docs/integrations/audita.md index 849dc9f..cbc4e13 100644 --- a/docs/integrations/audita.md +++ b/docs/integrations/audita.md @@ -5,9 +5,9 @@ Define Narratio's adapter contract for transcript polishing via Audita CLI subpr ## Inputs and Outputs Inputs (`audita.PolishRequest`): -- merged transcript path +- base transcript path - glossary path -- output processed transcript path +- output polished transcript path - optional report path (required when report enabled) - work dir - generated config path @@ -15,7 +15,7 @@ Inputs (`audita.PolishRequest`): - optional module/model/base URL and concurrency knobs Outputs (`audita.PolishResult`): -- processed transcript path +- polished transcript path - optional report path - generated config path - stdout/stderr log paths @@ -27,7 +27,7 @@ Owns: - Deterministic CLI argument construction for `audita process` - Environment bridging for API credentials - Invocation config emission -- Output validation for processed transcript and report +- Output validation for polished transcript and report Does not own: - Upstream/downstream stage orchestration @@ -52,7 +52,7 @@ Via `pipeline.audita.*` mapped in app/stage wiring: ## Failure Behavior - Constructor validation fails on invalid binary/timeout/schema/concurrency/URL values. -- Run fails on missing required paths, missing required credential env var, subprocess errors, invalid processed JSON shape, or invalid report JSON. +- Run fails on missing required paths, missing required credential env var, subprocess errors, invalid polished JSON shape, or invalid report JSON. - Failures preserve stdout/stderr paths in returned result metadata. ## Tests to Inspect Before Changing @@ -61,6 +61,6 @@ Via `pipeline.audita.*` mapped in app/stage wiring: - `internal/stage/polish_test.go` ## Architectural Invariants -- Processed output must be valid JSON with top-level `segments` array. +- Polished output must be valid JSON with top-level `segments` array. - When report is enabled, report output must be valid JSON. - If `llm_api_key_env` is configured, credential must be present in environment. diff --git a/docs/integrations/seriatim.md b/docs/integrations/seriatim.md index 250ea1e..16faddf 100644 --- a/docs/integrations/seriatim.md +++ b/docs/integrations/seriatim.md @@ -5,7 +5,7 @@ Define Narratio's adapter contract for merge, normalize, and trim subprocess inv ## Inputs and Outputs Inputs: -- `MergeRequest`: raw/normalized transcript inputs, output path, optional report, speaker/autocorrect paths, logs/config +- `MergeRequest`: raw/per-speaker normalized transcript inputs, base output path, optional report, speaker/autocorrect paths, logs/config - `NormalizeRequest`: input transcript, output path, schema, optional report, timeout/log/config - `TrimRequest`: input transcript, output path, keep selector, timeout/log/config @@ -44,8 +44,8 @@ Via `pipeline.seriatim.*` mapped in app/stage wiring: ## Failure Behavior - Constructor fails for invalid binary/timeout/output-schema/coalesce-gap. - Merge fails on missing output path/inputs/report path (if enabled), subprocess errors, invalid merged output JSON, invalid report JSON. -- Normalize fails on missing input/output, invalid schema, subprocess errors, invalid normalized output JSON shape, invalid report JSON. -- Trim fails on missing input/output/keep selector, subprocess errors, invalid trimmed output JSON shape. +- Normalize fails on missing input/output, invalid schema, subprocess errors, invalid final output JSON shape, invalid report JSON. +- Trim fails on missing input/output/keep selector, subprocess errors, invalid final-trimmed output JSON shape. ## Tests to Inspect Before Changing - `internal/adapters/seriatim/subprocess_test.go` @@ -56,5 +56,5 @@ Via `pipeline.seriatim.*` mapped in app/stage wiring: ## Architectural Invariants - Supported output schemas are limited to `seriatim-minimal`, `seriatim-intermediate`, `seriatim-full`. -- Normalize/trim outputs must include `segments` arrays. +- Final and final-trimmed outputs must include `segments` arrays. - Merge/normalize/trim all route through deterministic subprocess invocation. diff --git a/docs/internal/artifacts.md b/docs/internal/artifacts.md index e241c14..4dcc78d 100644 --- a/docs/internal/artifacts.md +++ b/docs/internal/artifacts.md @@ -33,10 +33,10 @@ Does not own: ## Built-in IDs | Artifact ID | Canonical file | Producer stage | Output kind | | --- | --- | --- | --- | -| `narratio.transcript.merged` | `transcripts/merged.json` | `merge` | `transcript_merged` | -| `narratio.transcript.polished` | `transcripts/processed.json` | `polish` | `transcript_processed` | -| `narratio.transcript.full` | `transcripts/normalized.json` | `normalize` | `transcript_normalized` | -| `narratio.transcript.trimmed` | `transcripts/trimmed.json` | `trim` | `transcript_trimmed` | +| `narratio.transcript.base` | `transcripts/base.json` | `merge` | `transcript_base` | +| `narratio.transcript.polished` | `transcripts/polished.json` | `polish` | `transcript_polished` | +| `narratio.transcript.final` | `transcripts/final.json` | `normalize` | `transcript_final` | +| `narratio.transcript.final_trimmed` | `transcripts/final.trimmed.json` | `trim` | `transcript_final_trimmed` | | `narratio.bounds.session` | `artifacts/session_bounds.json` | `trim` | `session_bounds` | ## Source families diff --git a/docs/internal/stage-merge.md b/docs/internal/stage-merge.md index 4839a81..1cac61b 100644 --- a/docs/internal/stage-merge.md +++ b/docs/internal/stage-merge.md @@ -1,7 +1,7 @@ # Stage: merge ## Purpose -Normalize per-speaker raw transcripts and merge them into one merged transcript via Seriatim. +Normalize per-speaker raw transcripts and merge them into the base transcript via Seriatim. ## Inputs and Outputs Inputs: @@ -10,7 +10,7 @@ Inputs: - `inputs/autocorrect.yml` Outputs: -- `transcripts/merged.json` +- `transcripts/base.json` - optional `artifacts/seriatim.report.json` (when report enabled) ## Boundaries @@ -19,7 +19,7 @@ Owns: - Per-input normalize calls to Seriatim - Final merge call to Seriatim - Run-local log/config/report path wiring -- Promotion of merged/report outputs to canonical paths +- Promotion of base/report outputs to canonical paths Does not own: - Transcript polishing or downstream artifact generation @@ -43,7 +43,7 @@ Does not own: ## State and Manifest Behavior - Reads transcript inputs from transcribe stage outputs in manifest when present; falls back to canonical raw directory. - Writes run-local outputs/logs/config under `runs/{run_id}/merge/...` when enabled. -- Promotes canonical merged transcript and optional report. +- Promotes canonical base transcript and optional report. - Records normalized-input provenance and adapter metadata in stage metadata. ## Skip and Resume Behavior @@ -51,7 +51,7 @@ Does not own: - Forced rerun of this or upstream stages can stale downstream succeeded stages via runner invalidation. ## Failure Behavior -- Fails on missing/invalid raw transcripts, missing speakers/autocorrect files, normalize failure, merge failure, invalid merged output JSON, or invalid report JSON when enabled. +- Fails on missing/invalid raw transcripts, missing speakers/autocorrect files, normalize failure, merge failure, invalid base output JSON, or invalid report JSON when enabled. ## Tests to Inspect Before Changing - `internal/stage/merge_test.go` @@ -59,5 +59,5 @@ Does not own: ## Architectural Invariants - Merge consumes normalized forms of each raw transcript. -- Merged transcript must validate before promotion. +- Base transcript must validate before promotion. - Report output is optional and gated by config. diff --git a/docs/internal/stage-normalize.md b/docs/internal/stage-normalize.md index 8b4e08d..325e469 100644 --- a/docs/internal/stage-normalize.md +++ b/docs/internal/stage-normalize.md @@ -1,22 +1,22 @@ # Stage: normalize ## Purpose -Normalize the processed transcript into a deterministic intermediate schema for trim and optionally emit a normalize report. +Normalize the polished transcript into the full final transcript and optionally emit a normalize report. ## Inputs and Outputs Inputs: -- `transcripts/processed.json` +- `transcripts/polished.json` Outputs: -- `transcripts/normalized.json` (or configured normalize output path) +- `transcripts/final.json` (or configured normalize output path) - optional `artifacts/seriatim.normalize.report.json` ## Boundaries Owns: -- Processed transcript discovery/validation +- Polished transcript discovery/validation - Normalize request construction and invocation - Optional normalize report wiring -- Promotion of normalized transcript and optional report +- Promotion of final transcript and optional report Does not own: - Bounds detection or segment trimming @@ -35,9 +35,9 @@ Does not own: - Seriatim adapter (`Normalize`). ## State and Manifest Behavior -- Reads processed transcript from polish outputs in manifest when present; falls back to canonical path. +- Reads polished transcript from polish outputs in manifest when present; falls back to canonical path. - Uses run-local output/report/log/config paths when run layout is enabled. -- Promotes canonical normalized transcript and optional normalize report. +- Promotes canonical final transcript and optional normalize report. - Records adapter/result metadata including source path selection. ## Skip and Resume Behavior @@ -45,12 +45,12 @@ Does not own: - Forced reruns can stale downstream succeeded stages. ## Failure Behavior -- Fails on missing/invalid processed transcript, adapter error, invalid normalized output, or invalid report output when report enabled. +- Fails on missing/invalid polished transcript, adapter error, invalid final output, or invalid report output when report enabled. ## Tests to Inspect Before Changing - `internal/stage/normalize_test.go` - `internal/adapters/seriatim/subprocess_test.go` ## Architectural Invariants -- Normalized output must validate as processed-transcript-compatible JSON (`segments` array required). +- Final output must validate as transcript-compatible JSON (`segments` array required). - Default normalize config is applied when `pipeline.normalize` is unset. diff --git a/docs/internal/stage-polish.md b/docs/internal/stage-polish.md index dff7b44..a9f54bc 100644 --- a/docs/internal/stage-polish.md +++ b/docs/internal/stage-polish.md @@ -1,23 +1,23 @@ # Stage: polish ## Purpose -Polish merged transcript with Audita and produce a processed transcript for downstream normalization/analyze. +Polish the base transcript with Audita and produce a polished transcript for downstream normalization/analyze. ## Inputs and Outputs Inputs: -- `transcripts/merged.json` +- `transcripts/base.json` - `inputs/glossary.yml` Outputs: -- `transcripts/processed.json` +- `transcripts/polished.json` - optional `artifacts/audita.report.json` (when report enabled) ## Boundaries Owns: -- Merged transcript discovery/validation +- Base transcript discovery/validation - Audita invocation request construction - Run-local logs/config/work-dir/report wiring -- Promotion of processed transcript and optional report +- Promotion of polished transcript and optional report Does not own: - Upstream merge normalization @@ -47,9 +47,9 @@ Does not own: - Audita adapter (`env.Audita.Run`). ## State and Manifest Behavior -- Reads merged transcript from merge manifest outputs when available; falls back to canonical merged path. +- Reads base transcript from merge manifest outputs when available; falls back to canonical base path. - Uses run-local output/report/log/config/scratch paths when run layout is enabled. -- Promotes canonical `transcripts/processed.json` and optional report. +- Promotes canonical `transcripts/polished.json` and optional report. - Records adapter invocation metadata, credential presence signal, and output provenance in stage metadata. ## Skip and Resume Behavior @@ -57,13 +57,13 @@ Does not own: - Forced rerun can stale downstream succeeded stages via runner invalidation. ## Failure Behavior -- Fails on missing/invalid merged transcript, missing glossary, adapter error, invalid processed output shape (`segments` array required), or invalid report JSON when enabled. +- Fails on missing/invalid base transcript, missing glossary, adapter error, invalid polished output shape (`segments` array required), or invalid report JSON when enabled. ## Tests to Inspect Before Changing - `internal/stage/polish_test.go` - `internal/adapters/audita/subprocess_test.go` ## Architectural Invariants -- Processed transcript must contain a top-level `segments` array. +- Polished transcript must contain a top-level `segments` array. - Report behavior is strictly config-gated. -- Stage output canonicalization always ends at `transcripts/processed.json`. +- Stage output canonicalization always ends at `transcripts/polished.json`. diff --git a/docs/internal/stage-trim.md b/docs/internal/stage-trim.md index 378aeef..292dfd8 100644 --- a/docs/internal/stage-trim.md +++ b/docs/internal/stage-trim.md @@ -1,21 +1,21 @@ # Stage: trim ## Purpose -Optionally trim the normalized transcript to session bounds; always produce a durable trimmed transcript. +Optionally trim the final transcript to session bounds; always produce a durable final-trimmed transcript. ## Inputs and Outputs Inputs: -- `transcripts/normalized.json` +- `transcripts/final.json` Outputs: -- `transcripts/trimmed.json` (or configured trim output path) +- `transcripts/final.trimmed.json` (or configured trim output path) - when trim enabled: `artifacts/session_bounds.json` ## Boundaries Owns: - Trim-enabled switch behavior - Bounds generation via Scriptorium artifact run -- Bounds validation against normalized transcript +- Bounds validation against final transcript - Keep-selector derivation and Seriatim trim invocation - Copy-through behavior when disabled or bounds indicate unchanged transcript @@ -50,19 +50,19 @@ Does not own: - `Trim` when bounds indicate trimming is required ## State and Manifest Behavior -- Reads normalized transcript from normalize manifest outputs when available; falls back to canonical path. +- Reads final transcript from normalize manifest outputs when available; falls back to canonical path. - Uses run-local outputs/logs/reports/config/scratch paths when run layout is enabled. -- Promotes canonical trimmed transcript; promotes session bounds when trim enabled. +- Promotes canonical final-trimmed transcript; promotes session bounds when trim enabled. - Records bounds diagnostics, trim action, keep selector, and adapter metadata. ## Skip and Resume Behavior - Runner-level skip applies when already succeeded and not forced. - Forced reruns can stale downstream succeeded stages. -- When `trim.enabled=false`, stage still succeeds by copying normalized to trimmed output. +- When `trim.enabled=false`, stage still succeeds by copying final to final-trimmed output. ## Failure Behavior -- Fails on missing/invalid normalized transcript. -- With trim enabled, fails on missing adapters/config, bounds generation/validation errors, invalid bounds JSON, invalid range/segment ids, trim adapter failures, or invalid trimmed output. +- Fails on missing/invalid final transcript. +- With trim enabled, fails on missing adapters/config, bounds generation/validation errors, invalid bounds JSON, invalid range/segment ids, trim adapter failures, or invalid final-trimmed output. ## Tests to Inspect Before Changing - `internal/stage/trim_test.go` @@ -70,6 +70,6 @@ Does not own: - `internal/adapters/seriatim/subprocess_test.go` ## Architectural Invariants -- Trim never falls back to processed transcript; normalized transcript is required input. +- Trim never falls back to polished transcript; final transcript is required input. - `session_bounds` output exists only for enabled trim path. - Render-debug artifacts are diagnostics and not declared stage outputs. diff --git a/docs/roadmap/transcripts.md b/docs/roadmap/transcripts.md new file mode 100644 index 0000000..4f0e6a1 --- /dev/null +++ b/docs/roadmap/transcripts.md @@ -0,0 +1,210 @@ +# Roadmap: Transcript Artifact Naming + +Status: Implemented + +## Problem + +Narratio's built-in transcript artifact names and canonical paths currently mix +operator-facing artifact meaning with historical stage and tool terminology: + +- `narratio.transcript.merged` maps to `transcripts/merged.json`. +- `narratio.transcript.polished` maps to `transcripts/processed.json`. +- `narratio.transcript.full` maps to `transcripts/normalized.json`. +- `narratio.transcript.trimmed` maps to `transcripts/trimmed.json`. + +This makes the public artifact surface harder to reason about. Operators see +`full`, `normalized`, `processed`, `polished`, `merged`, and `trimmed` used in +different places for the same transcript lineage. + +The transcript source IDs, canonical paths, and manifest output kinds should +use one vocabulary based on each transcript's role in the session artifact +model. + +## Target Model + +Built-in transcript artifacts should use these public source IDs, canonical +paths, and manifest output kinds: + +| Source ID | Canonical path | Output kind | Meaning | +| --- | --- | --- | --- | +| `narratio.transcript.base` | `transcripts/base.json` | `transcript_base` | First unified transcript produced by merging per-speaker raw transcripts. | +| `narratio.transcript.polished` | `transcripts/polished.json` | `transcript_polished` | Audita-polished transcript. | +| `narratio.transcript.final` | `transcripts/final.json` | `transcript_final` | Full final transcript after normalization. | +| `narratio.transcript.final_trimmed` | `transcripts/final.trimmed.json` | `transcript_final_trimmed` | Trimmed version of the final transcript. | + +Stage names remain process-oriented and unchanged: + +- `merge` +- `polish` +- `normalize` +- `trim` + +Downstream adapter contracts also remain process-oriented. The rename changes +Narratio's artifact model, canonical paths, config examples, archive promotion +sources, lock sources, status output, and documentation. It should not rename +the stages themselves or move external integration details into stage logic. + +## Compatibility Policy + +This is a hard cutover. + +After implementation, these old source IDs should be rejected: + +- `narratio.transcript.merged` +- `narratio.transcript.full` +- `narratio.transcript.trimmed` + +These old canonical paths should not be compatibility fallbacks: + +- `transcripts/merged.json` +- `transcripts/processed.json` +- `transcripts/normalized.json` +- `transcripts/trimmed.json` + +Existing remote archives are not migrated automatically. Operators who want +new promoted keys for old sessions should republish those sessions after +updating configuration. + +## Implementation Stages + +### Stage 1: Centralize Transcript Artifact Naming + +Status: Implemented + +Consolidate transcript artifact source IDs, canonical paths, and output kinds +in the artifact/path layer before changing runtime behavior. + +Implementation requirements: + +- Add or consolidate constants/helpers for built-in transcript source IDs. +- Add or consolidate constants/helpers for canonical transcript paths. +- Add or consolidate constants/helpers for transcript manifest output kinds. +- Keep source ID, path, and output-kind mappings in one registry or one + obviously shared artifact model. +- Update artifact registry tests to prove the target mapping. +- Avoid changing stage output behavior in this stage unless the implementation + is simpler and still reviewable. + +Acceptance criteria: + +- There is one clear source of truth for built-in transcript artifact names, + paths, and output kinds. +- Tests prove the new target mapping in the artifact layer. +- No generic workflow abstraction is introduced. + +### Stage 2: Rename Runtime Outputs and Defaults + +Status: Implemented + +Switch runtime behavior to the new transcript artifact model. + +Implementation requirements: + +- Update `merge` to write and record `transcripts/base.json` with + `transcript_base`. +- Update `polish` to write and record `transcripts/polished.json` with + `transcript_polished`. +- Update `normalize` to write and record `transcripts/final.json` with + `transcript_final`. +- Update `trim` to write and record `transcripts/final.trimmed.json` with + `transcript_final_trimmed`. +- Update normalize and trim defaults to: + - `pipeline.normalize.output_path: transcripts/final.json` + - `pipeline.trim.output_path: transcripts/final.trimmed.json` +- Update built-in artifact resolution, archive promotion destination + derivation, archive locks, status output, artifact catalog output, + previous-cache resolution, restore planning, and restore execution to use + the new registry values. +- Ensure old source IDs fail config validation. + +Acceptance criteria: + +- New runs produce the target canonical transcript files. +- Manifest outputs use the target output kinds. +- Archive promotion and lock validation accept new source IDs and reject old + source IDs. +- Status and artifact listing display new source IDs. +- Restore uses the new canonical paths and does not restore old transcript + paths as canonical outputs. + +### Stage 3: Update Tests, Examples, and Current Documentation + +Status: Implemented + +Update all implemented-behavior references after the runtime cutover lands. + +Implementation requirements: + +- Update examples to use `narratio.transcript.final_trimmed` and + `transcripts/final.trimmed.json` where trimmed final transcript is intended. +- Update examples that refer to full final transcripts to use + `narratio.transcript.final` and `transcripts/final.json`. +- Update `docs/config.md`, `docs/internal/artifacts.md`, stage docs, + CLI examples, operations examples, archive examples, lock examples, and + status/artifact-list examples. +- Add strict validation tests proving old source IDs are rejected. +- Mark roadmap stages implemented only after code, tests, examples, and + current-behavior docs agree. + +Acceptance criteria: + +- Maintained examples load and validate. +- Current-behavior docs describe only implemented new names. +- Old names remain only in this roadmap as historical/planning context until + this roadmap is retired or archived. + +## Test Guidance + +Run focused tests while implementing: + +- `go test ./internal/artifacts -v` +- `go test ./internal/config -v` +- `go test ./internal/stage -v` +- `go test ./internal/app -v` + +Run full validation before finishing: + +- `go test ./...` + +Run final searches: + +- Old source IDs: + - `narratio.transcript.merged` + - `narratio.transcript.full` + - `narratio.transcript.trimmed` +- Old paths: + - `transcripts/merged.json` + - `transcripts/processed.json` + - `transcripts/normalized.json` + - `transcripts/trimmed.json` +- Old output kinds: + - `transcript_merged` + - `transcript_processed` + - `transcript_normalized` + - `transcript_trimmed` + +Expected remaining matches should be limited to this roadmap's +historical/planning references until the roadmap is fully completed. + +## Architecture Guardrails + +- Keep Narratio explicit and stage-driven; do not introduce a generic workflow + or DAG abstraction. +- Keep path and artifact naming in centralized helpers rather than scattered + string concatenation. +- Preserve manifest-driven resume behavior. +- Keep storage details behind storage adapters. +- Do not move Seriatim, Audita, or Scriptorium command details out of their + adapter boundaries. +- Keep current-behavior documentation in sync only after implementation lands; + planned behavior belongs in this roadmap until then. + +## Assumptions + +- The cutover is intentionally not backward-compatible. +- Existing remote archive objects are not renamed or migrated automatically. +- Stage names and downstream adapter request field names remain unchanged. +- The term `base` is preferred over `merged` for the first unified transcript. +- The term `final` is preferred over `full` or `normalized` for the full final + transcript. +- The trimmed final path is `transcripts/final.trimmed.json`. diff --git a/examples/pipeline.full.annotated.yml b/examples/pipeline.full.annotated.yml index 9dd616c..e3f2827 100644 --- a/examples/pipeline.full.annotated.yml +++ b/examples/pipeline.full.annotated.yml @@ -39,8 +39,8 @@ archive: upload_run: true # Optional promotion rules; sources use Narratio artifact source IDs. promote_artifacts: - - source: narratio.transcript.trimmed - dest: transcripts/trimmed.json + - source: narratio.transcript.final_trimmed + dest: transcripts/final.trimmed.json required: true - source: narratio.artifact.session_recap dest: artifacts/session_recap.md @@ -93,14 +93,14 @@ audita: normalize: # Optional; defaults shown explicitly. - output_path: transcripts/normalized.json + output_path: transcripts/final.json output_schema: seriatim-intermediate report: true trim: # Keep disabled unless bounds prompt integration is configured. enabled: false - output_path: transcripts/trimmed.json + output_path: transcripts/final.trimmed.json bounds: prompt_id: dnd.session_bounds profile_id: local-fast @@ -127,7 +127,7 @@ scriptorium: timeout: 10m inputs: transcript: - source: narratio.transcript.trimmed + source: narratio.transcript.final_trimmed required: true previous_recap: source: narratio.previous_session.artifact.session_recap @@ -155,7 +155,7 @@ scriptorium: source: narratio.artifact.session_recap required: true transcript: - source: narratio.transcript.trimmed + source: narratio.transcript.final_trimmed required: true vars: session_id: true diff --git a/examples/pipeline.production.yml b/examples/pipeline.production.yml index d3a22ce..b6bc4f4 100644 --- a/examples/pipeline.production.yml +++ b/examples/pipeline.production.yml @@ -19,8 +19,8 @@ archive: enabled: true upload_run: true promote_artifacts: - - source: narratio.transcript.trimmed - dest: transcripts/trimmed.json + - source: narratio.transcript.final_trimmed + dest: transcripts/final.trimmed.json required: true - source: narratio.artifact.session_recap dest: artifacts/session_recap.md @@ -57,7 +57,7 @@ audita: report: true normalize: - output_path: transcripts/normalized.json + output_path: transcripts/final.json output_schema: seriatim-intermediate report: true @@ -78,7 +78,7 @@ scriptorium: timeout: 10m inputs: transcript: - source: narratio.transcript.trimmed + source: narratio.transcript.final_trimmed required: true previous_recap: source: narratio.previous_session.artifact.session_recap @@ -102,7 +102,7 @@ scriptorium: source: narratio.artifact.session_recap required: true transcript: - source: narratio.transcript.trimmed + source: narratio.transcript.final_trimmed required: true vars: session_id: true diff --git a/internal/adapters/audita/fake_test.go b/internal/adapters/audita/fake_test.go index 69dcb50..225c448 100644 --- a/internal/adapters/audita/fake_test.go +++ b/internal/adapters/audita/fake_test.go @@ -14,7 +14,7 @@ func TestFakeRunnerCapturesRequestAndReturnsPath(t *testing.T) { dir := t.TempDir() req := PolishRequest{ GeneratedConfigPath: filepath.Join(dir, "config", "audita.yml"), - OutputProcessedPath: filepath.Join(dir, "transcripts", "processed.json"), + OutputProcessedPath: filepath.Join(dir, "transcripts", "polished.json"), StdoutLogPath: filepath.Join(dir, "logs", "audita.stdout.log"), StderrLogPath: filepath.Join(dir, "logs", "audita.stderr.log"), } diff --git a/internal/adapters/audita/subprocess_test.go b/internal/adapters/audita/subprocess_test.go index 2b12ea0..5b8fe77 100644 --- a/internal/adapters/audita/subprocess_test.go +++ b/internal/adapters/audita/subprocess_test.go @@ -52,9 +52,9 @@ func TestSubprocessRunnerSuccessArgsEnvAndValidation(t *testing.T) { dir := t.TempDir() req := PolishRequest{ GeneratedConfigPath: filepath.Join(dir, "audita.generated.yml"), - MergedTranscriptPath: filepath.Join(dir, "merged.json"), + MergedTranscriptPath: filepath.Join(dir, "base.json"), GlossaryPath: filepath.Join(dir, "glossary.yml"), - OutputProcessedPath: filepath.Join(dir, "processed.json"), + OutputProcessedPath: filepath.Join(dir, "polished.json"), ReportPath: filepath.Join(dir, "audita.report.json"), WorkDir: filepath.Join(dir, "artifacts", "audita-work"), StdoutLogPath: filepath.Join(dir, "audita.stdout.log"), @@ -571,7 +571,7 @@ func mustAuditaRunner(t *testing.T, cfg SubprocessRunnerConfig) *SubprocessRunne func auditaReqForTest(t *testing.T, withReport bool) PolishRequest { t.Helper() dir := t.TempDir() - merged := filepath.Join(dir, "merged.json") + merged := filepath.Join(dir, "base.json") glossary := filepath.Join(dir, "glossary.yml") writeAuditaTestFile(t, merged, `{"segments":[]}`) writeAuditaTestFile(t, glossary, "terms: []\n") @@ -579,7 +579,7 @@ func auditaReqForTest(t *testing.T, withReport bool) PolishRequest { GeneratedConfigPath: filepath.Join(dir, "audita.generated.yml"), MergedTranscriptPath: merged, GlossaryPath: glossary, - OutputProcessedPath: filepath.Join(dir, "processed.json"), + OutputProcessedPath: filepath.Join(dir, "polished.json"), WorkDir: filepath.Join(dir, "artifacts", "audita-work"), StdoutLogPath: filepath.Join(dir, "audita.stdout.log"), StderrLogPath: filepath.Join(dir, "audita.stderr.log"), diff --git a/internal/adapters/scriptorium/subprocess_test.go b/internal/adapters/scriptorium/subprocess_test.go index 4bc80d2..862309f 100644 --- a/internal/adapters/scriptorium/subprocess_test.go +++ b/internal/adapters/scriptorium/subprocess_test.go @@ -31,7 +31,7 @@ func TestSubprocessRunnerRunSuccessBuildsDeterministicArgsAndCapturesLogs(t *tes ConfigPath: "/etc/scriptorium/config.yml", PromptID: "dnd.session_recap", ProfileID: "local-quality", - InputPaths: map[string]string{"transcript": filepath.Join(dir, "processed.json"), "other": filepath.Join(dir, "other.md")}, + InputPaths: map[string]string{"transcript": filepath.Join(dir, "polished.json"), "other": filepath.Join(dir, "other.md")}, Vars: map[string]string{"session_id": "2026-05-03", "campaign_name": "Icewind Dale"}, OutputPath: filepath.Join(dir, "artifacts", "session_recap.md"), StdoutLogPath: filepath.Join(dir, "logs", "scriptorium.run.stdout.log"), @@ -180,7 +180,7 @@ func TestSubprocessRunnerRenderSuccess(t *testing.T) { req := RenderArtifactRequest{ Binary: wrapper, PromptID: "dnd.session_recap", - InputPaths: map[string]string{"transcript": filepath.Join(dir, "processed.json")}, + InputPaths: map[string]string{"transcript": filepath.Join(dir, "polished.json")}, OutputPath: filepath.Join(dir, "artifacts", "session_recap.render.json"), StdoutLogPath: filepath.Join(dir, "logs", "scriptorium.render.stdout.log"), StderrLogPath: filepath.Join(dir, "logs", "scriptorium.render.stderr.log"), @@ -285,7 +285,7 @@ type scriptoriumHelperRecord struct { func runReqForTest(t *testing.T, binary string) RunArtifactRequest { t.Helper() dir := t.TempDir() - transcriptPath := filepath.Join(dir, "processed.json") + transcriptPath := filepath.Join(dir, "polished.json") writeScriptoriumFile(t, transcriptPath, `{"segments":[]}`) return RunArtifactRequest{ Binary: binary, diff --git a/internal/adapters/seriatim/fake_test.go b/internal/adapters/seriatim/fake_test.go index b6bb023..c479569 100644 --- a/internal/adapters/seriatim/fake_test.go +++ b/internal/adapters/seriatim/fake_test.go @@ -14,7 +14,7 @@ func TestFakeRunnerCapturesRequestAndReturnsPath(t *testing.T) { dir := t.TempDir() req := MergeRequest{ GeneratedConfigPath: filepath.Join(dir, "config", "seriatim.yml"), - OutputMergedTranscriptPath: filepath.Join(dir, "transcripts", "merged.json"), + OutputMergedTranscriptPath: filepath.Join(dir, "transcripts", "base.json"), StdoutLogPath: filepath.Join(dir, "logs", "seriatim.stdout.log"), StderrLogPath: filepath.Join(dir, "logs", "seriatim.stderr.log"), } @@ -57,8 +57,8 @@ func TestFakeRunnerTrimCapturesRequestAndReturnsPath(t *testing.T) { dir := t.TempDir() req := TrimRequest{ GeneratedConfigPath: filepath.Join(dir, "config", "seriatim.trim.yml"), - InputTranscriptPath: filepath.Join(dir, "transcripts", "processed.json"), - OutputTrimmedPath: filepath.Join(dir, "transcripts", "trimmed.json"), + InputTranscriptPath: filepath.Join(dir, "transcripts", "polished.json"), + OutputTrimmedPath: filepath.Join(dir, "transcripts", "final.trimmed.json"), KeepSelector: "1-10", StdoutLogPath: filepath.Join(dir, "logs", "seriatim.trim.stdout.log"), StderrLogPath: filepath.Join(dir, "logs", "seriatim.trim.stderr.log"), @@ -105,8 +105,8 @@ func TestFakeRunnerNormalizeCapturesRequestAndReturnsPath(t *testing.T) { dir := t.TempDir() req := NormalizeRequest{ GeneratedConfigPath: filepath.Join(dir, "config", "seriatim.normalize.yml"), - InputTranscriptPath: filepath.Join(dir, "transcripts", "processed.json"), - OutputNormalizedPath: filepath.Join(dir, "transcripts", "normalized.json"), + InputTranscriptPath: filepath.Join(dir, "transcripts", "polished.json"), + OutputNormalizedPath: filepath.Join(dir, "transcripts", "final.json"), OutputSchema: "seriatim-intermediate", ReportPath: filepath.Join(dir, "artifacts", "seriatim.normalize.report.json"), StdoutLogPath: filepath.Join(dir, "logs", "seriatim.normalize.stdout.log"), diff --git a/internal/adapters/seriatim/subprocess_test.go b/internal/adapters/seriatim/subprocess_test.go index 789a2b7..b67a836 100644 --- a/internal/adapters/seriatim/subprocess_test.go +++ b/internal/adapters/seriatim/subprocess_test.go @@ -50,7 +50,7 @@ func TestSubprocessRunnerSuccessWithReportArgsAndEnv(t *testing.T) { req := MergeRequest{ GeneratedConfigPath: filepath.Join(dir, "seriatim.generated.yml"), InputTranscriptPaths: []string{filepath.Join(dir, "a.json"), filepath.Join(dir, "b.json")}, - OutputMergedTranscriptPath: filepath.Join(dir, "merged.json"), + OutputMergedTranscriptPath: filepath.Join(dir, "base.json"), ReportPath: filepath.Join(dir, "seriatim.report.json"), SpeakersPath: filepath.Join(dir, "speakers.yml"), AutocorrectPath: filepath.Join(dir, "autocorrect.yml"), @@ -732,7 +732,7 @@ func mergeReqForTest(t *testing.T, withReport bool) MergeRequest { req := MergeRequest{ GeneratedConfigPath: filepath.Join(dir, "seriatim.generated.yml"), InputTranscriptPaths: []string{in1, in2}, - OutputMergedTranscriptPath: filepath.Join(dir, "merged.json"), + OutputMergedTranscriptPath: filepath.Join(dir, "base.json"), StdoutLogPath: filepath.Join(dir, "seriatim.stdout.log"), StderrLogPath: filepath.Join(dir, "seriatim.stderr.log"), } @@ -745,11 +745,11 @@ func mergeReqForTest(t *testing.T, withReport bool) MergeRequest { func trimReqForTest(t *testing.T) TrimRequest { t.Helper() dir := t.TempDir() - input := filepath.Join(dir, "processed.json") + input := filepath.Join(dir, "polished.json") writeSeriatimFile(t, input, `{"schema":"seriatim.intermediate.v1","segments":[]}`) return TrimRequest{ InputTranscriptPath: input, - OutputTrimmedPath: filepath.Join(dir, "trimmed.json"), + OutputTrimmedPath: filepath.Join(dir, "final.trimmed.json"), KeepSelector: "5-12", GeneratedConfigPath: filepath.Join(dir, "seriatim.trim.generated.yml"), StdoutLogPath: filepath.Join(dir, "seriatim.trim.stdout.log"), @@ -760,12 +760,12 @@ func trimReqForTest(t *testing.T) TrimRequest { func normalizeReqForTest(t *testing.T, withReport bool) NormalizeRequest { t.Helper() dir := t.TempDir() - input := filepath.Join(dir, "processed.json") + input := filepath.Join(dir, "polished.json") writeSeriatimFile(t, input, `{"schema":"audita.processed.v1","segments":[]}`) req := NormalizeRequest{ InputTranscriptPath: input, - OutputNormalizedPath: filepath.Join(dir, "normalized.json"), + OutputNormalizedPath: filepath.Join(dir, "final.json"), OutputSchema: "seriatim-intermediate", GeneratedConfigPath: filepath.Join(dir, "seriatim.normalize.generated.yml"), StdoutLogPath: filepath.Join(dir, "seriatim.normalize.stdout.log"), diff --git a/internal/app/commands_test.go b/internal/app/commands_test.go index 06594f7..572f30e 100644 --- a/internal/app/commands_test.go +++ b/internal/app/commands_test.go @@ -112,7 +112,7 @@ func TestExecuteRunStageNormalizeIsAccepted(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot, "https://example.com/transcribe") workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03") - mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "processed.json"), `{"segments":[{"id":1}]}`) + mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "polished.json"), `{"segments":[{"id":1}]}`) var stdout bytes.Buffer var stderr bytes.Buffer @@ -233,7 +233,7 @@ inputs: }) workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", sessionID) - mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "merged.json"), `{"schema":"seriatim-intermediate","segments":[]}`) + mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "base.json"), `{"schema":"seriatim-intermediate","segments":[]}`) mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "glossary.yml"), "[]\n") var stdout bytes.Buffer diff --git a/internal/app/operator_helpers.go b/internal/app/operator_helpers.go index 0cc5687..79017bd 100644 --- a/internal/app/operator_helpers.go +++ b/internal/app/operator_helpers.go @@ -894,10 +894,10 @@ func writeArtifactList(out io.Writer, cfg *config.Config, catalog *artifacts.Art lockSet := lockSourceSet(locks.All) fmt.Fprintln(out, "Built-in:") for _, id := range []string{ - artifacts.ArtifactTranscriptMerged, + artifacts.ArtifactTranscriptBase, artifacts.ArtifactTranscriptPolished, - artifacts.ArtifactTranscriptFull, - artifacts.ArtifactTranscriptTrimmed, + artifacts.ArtifactTranscriptFinal, + artifacts.ArtifactTranscriptFinalTrimmed, artifacts.ArtifactBoundsSession, } { writeArtifactLine(out, id, lockSet) diff --git a/internal/app/operator_helpers_test.go b/internal/app/operator_helpers_test.go index 61f0cb0..c83f3a0 100644 --- a/internal/app/operator_helpers_test.go +++ b/internal/app/operator_helpers_test.go @@ -476,7 +476,7 @@ func TestExecuteLocksAddListAndRemoveUseRemoteLockStore(t *testing.T) { "--session", sessionPath, "--session-id", "2026-05-03", "--reason", "manual edit", - "narratio.transcript.trimmed", + "narratio.transcript.final_trimmed", }, &stdout, &stderr) if code != 0 { t.Fatalf("locks add exit code = %d, want 0; stderr=%q", code, stderr.String()) @@ -486,7 +486,7 @@ func TestExecuteLocksAddListAndRemoveUseRemoteLockStore(t *testing.T) { if !ok { t.Fatalf("remote locks key %q not uploaded", key) } - if !strings.Contains(string(obj.Data), "source: narratio.transcript.trimmed") || !strings.Contains(string(obj.Data), "reason: manual edit") { + if !strings.Contains(string(obj.Data), "source: narratio.transcript.final_trimmed") || !strings.Contains(string(obj.Data), "reason: manual edit") { t.Fatalf("lock store data = %q", string(obj.Data)) } @@ -502,7 +502,7 @@ func TestExecuteLocksAddListAndRemoveUseRemoteLockStore(t *testing.T) { if code != 0 { t.Fatalf("locks list exit code = %d, want 0; stderr=%q", code, stderr.String()) } - if !strings.Contains(stdout.String(), "- narratio.transcript.trimmed origin=remote") { + if !strings.Contains(stdout.String(), "- narratio.transcript.final_trimmed origin=remote") { t.Fatalf("stdout = %q, want remote lock", stdout.String()) } @@ -514,7 +514,7 @@ func TestExecuteLocksAddListAndRemoveUseRemoteLockStore(t *testing.T) { "--campaign", campaignPath, "--session", sessionPath, "--session-id", "2026-05-03", - "narratio.transcript.trimmed", + "narratio.transcript.final_trimmed", }, &stdout, &stderr) if code != 0 { t.Fatalf("locks remove exit code = %d, want 0; stderr=%q", code, stderr.String()) @@ -547,7 +547,7 @@ func TestExecuteLocksAddDuplicateRequiresForce(t *testing.T) { "--session", sessionPath, "--session-id", "2026-05-03", "--reason", "first", - "narratio.transcript.trimmed", + "narratio.transcript.final_trimmed", }, &stdout, &stderr) if code != 0 { t.Fatalf("initial locks add exit code = %d, want 0; stderr=%q", code, stderr.String()) @@ -562,7 +562,7 @@ func TestExecuteLocksAddDuplicateRequiresForce(t *testing.T) { "--session", sessionPath, "--session-id", "2026-05-03", "--reason", "second", - "narratio.transcript.trimmed", + "narratio.transcript.final_trimmed", }, &stdout, &stderr) if code == 0 { t.Fatal("duplicate locks add exit code = 0, want non-zero") @@ -581,7 +581,7 @@ func TestExecuteLocksAddDuplicateRequiresForce(t *testing.T) { "--session-id", "2026-05-03", "--reason", "second", "--force", - "narratio.transcript.trimmed", + "narratio.transcript.final_trimmed", }, &stdout, &stderr) if code != 0 { t.Fatalf("forced locks add exit code = %d, want 0; stderr=%q", code, stderr.String()) @@ -599,8 +599,8 @@ func TestExecuteLocksRequireSessionID(t *testing.T) { want string }{ {"list", []string{"locks"}, "locks: --session-id is required"}, - {"add", []string{"locks", "add", "narratio.transcript.trimmed"}, "locks add: --session-id is required"}, - {"remove", []string{"locks", "remove", "narratio.transcript.trimmed"}, "locks remove: --session-id is required"}, + {"add", []string{"locks", "add", "narratio.transcript.final_trimmed"}, "locks add: --session-id is required"}, + {"remove", []string{"locks", "remove", "narratio.transcript.final_trimmed"}, "locks remove: --session-id is required"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -620,7 +620,7 @@ func TestExecuteLocksRequireSessionID(t *testing.T) { func TestExecuteLocksCannotModifyStaticLocks(t *testing.T) { workspaceRoot := t.TempDir() pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) - addStaticArchiveLockToPipelineConfig(t, pipelinePath, "narratio.transcript.trimmed") + addStaticArchiveLockToPipelineConfig(t, pipelinePath, "narratio.transcript.final_trimmed") fake := &storage.FakeBackend{} var storeInitCalls int restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath}) @@ -633,7 +633,7 @@ func TestExecuteLocksCannotModifyStaticLocks(t *testing.T) { "--campaign", campaignPath, "--session", sessionPath, "--session-id", "2026-05-03", - "narratio.transcript.trimmed", + "narratio.transcript.final_trimmed", }, &stdout, &stderr) if code == 0 { t.Fatal("locks add static lock exit code = 0, want non-zero") @@ -650,7 +650,7 @@ func TestExecuteLocksCannotModifyStaticLocks(t *testing.T) { "--campaign", campaignPath, "--session", sessionPath, "--session-id", "2026-05-03", - "narratio.transcript.trimmed", + "narratio.transcript.final_trimmed", }, &stdout, &stderr) if code == 0 { t.Fatal("locks remove static lock exit code = 0, want non-zero") @@ -666,7 +666,7 @@ func TestExecuteTopLevelLockAndUnlockAreRemoved(t *testing.T) { t.Run(cmd, func(t *testing.T) { var stdout bytes.Buffer var stderr bytes.Buffer - code := Execute([]string{cmd, "narratio.transcript.trimmed"}, &stdout, &stderr) + code := Execute([]string{cmd, "narratio.transcript.final_trimmed"}, &stdout, &stderr) if code == 0 { t.Fatal("exit code = 0, want non-zero") } @@ -718,14 +718,14 @@ func TestExecuteArtifactsListRemoteReportsPromotedAvailability(t *testing.T) { pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) addArchivePromotionsToPipeline(t, pipelinePath, ` promote_artifacts: - - source: narratio.transcript.trimmed - dest: transcripts/trimmed.json + - source: narratio.transcript.final_trimmed + dest: transcripts/final.trimmed.json required: true `) fake := &storage.FakeBackend{} trimmedKey := artifacts.S3PromotedArtifactKey( artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"), - "transcripts/trimmed.json", + "transcripts/final.trimmed.json", ) fake.SeedObject(storage.FakeObject{Key: trimmedKey, Data: []byte(`{"segments":[]}`)}) var storeInitCalls int @@ -743,7 +743,7 @@ func TestExecuteArtifactsListRemoteReportsPromotedAvailability(t *testing.T) { if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } - if !strings.Contains(stdout.String(), "narratio.transcript.trimmed remote=promoted") { + if !strings.Contains(stdout.String(), "narratio.transcript.final_trimmed remote=promoted") { t.Fatalf("stdout = %q, want promoted remote availability", stdout.String()) } } @@ -753,7 +753,7 @@ func TestExecuteArtifactsListRemoteUsesPromotionDestinations(t *testing.T) { pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) addArchivePromotionsToPipeline(t, pipelinePath, ` promote_artifacts: - - source: narratio.transcript.full + - source: narratio.transcript.final dest: transcripts/full.json required: true - source: narratio.bounds.session @@ -781,7 +781,7 @@ func TestExecuteArtifactsListRemoteUsesPromotionDestinations(t *testing.T) { } out := stdout.String() for _, unwanted := range []string{ - "narratio.transcript.full remote=missing", + "narratio.transcript.final remote=missing", "narratio.bounds.session remote=missing", } { if strings.Contains(out, unwanted) { @@ -789,7 +789,7 @@ func TestExecuteArtifactsListRemoteUsesPromotionDestinations(t *testing.T) { } } for _, want := range []string{ - "narratio.transcript.full dest=transcripts/full.json remote=promoted", + "narratio.transcript.final dest=transcripts/full.json remote=promoted", "narratio.bounds.session dest=transcripts/bounds.json remote=promoted", } { if !strings.Contains(out, want) { @@ -803,24 +803,24 @@ func TestExecuteStatusReportsRemoteArtifactCatalog(t *testing.T) { pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) addArchivePromotionsToPipeline(t, pipelinePath, ` promote_artifacts: - - source: narratio.transcript.trimmed - dest: transcripts/trimmed.json + - source: narratio.transcript.final_trimmed + dest: transcripts/final.trimmed.json required: true - - source: narratio.transcript.full + - source: narratio.transcript.final dest: transcripts/full.json required: true `) fake := &storage.FakeBackend{} sessionPrefix := artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03") manifestKey, runIDKey := artifacts.ResolveArchiveCurrentStateKeys(sessionPrefix) - trimmedKey := artifacts.S3PromotedArtifactKey(sessionPrefix, "transcripts/trimmed.json") + trimmedKey := artifacts.S3PromotedArtifactKey(sessionPrefix, "transcripts/final.trimmed.json") fullKey := artifacts.S3PromotedArtifactKey(sessionPrefix, "transcripts/full.json") lockKey := artifacts.S3SessionLocksKey(sessionPrefix) fake.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")}) fake.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, "2026-05-03", "sample-campaign")}) fake.SeedObject(storage.FakeObject{Key: trimmedKey, Data: []byte(`{"segments":[]}`)}) fake.SeedObject(storage.FakeObject{Key: fullKey, Data: []byte(`{"segments":[]}`)}) - fake.SeedObject(storage.FakeObject{Key: lockKey, Data: []byte("locks:\n - source: narratio.transcript.trimmed\n reason: remote review\n")}) + fake.SeedObject(storage.FakeObject{Key: lockKey, Data: []byte("locks:\n - source: narratio.transcript.final_trimmed\n reason: remote review\n")}) var storeInitCalls int restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath}) @@ -843,15 +843,15 @@ func TestExecuteStatusReportsRemoteArtifactCatalog(t *testing.T) { "Configured:", "Previous-session:", "Promoted:", - "narratio.transcript.trimmed locked", - "narratio.transcript.trimmed locked remote=promoted", - "narratio.transcript.full dest=transcripts/full.json remote=promoted", + "narratio.transcript.final_trimmed locked", + "narratio.transcript.final_trimmed locked remote=promoted", + "narratio.transcript.final dest=transcripts/full.json remote=promoted", } { if !strings.Contains(out, want) { t.Fatalf("stdout = %q, want %q", out, want) } } - if strings.Contains(out, "narratio.transcript.merged remote=missing") { + if strings.Contains(out, "narratio.transcript.base remote=missing") { t.Fatalf("stdout = %q, did not want catalog remote marker", out) } } @@ -861,8 +861,8 @@ func TestExecuteStatusReportsRemoteArtifactCatalogErrorsWithoutFailing(t *testin pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) addArchivePromotionsToPipeline(t, pipelinePath, ` promote_artifacts: - - source: narratio.transcript.trimmed - dest: transcripts/trimmed.json + - source: narratio.transcript.final_trimmed + dest: transcripts/final.trimmed.json required: true `) fake := &storage.FakeBackend{ExistsErr: fmt.Errorf("exists failed")} @@ -885,7 +885,7 @@ func TestExecuteStatusReportsRemoteArtifactCatalogErrorsWithoutFailing(t *testin if !strings.Contains(out, "Remote archive: missing or unavailable:") { t.Fatalf("stdout = %q, want remote archive unavailable state", out) } - if !strings.Contains(out, "Remote outputs:") || !strings.Contains(out, "narratio.transcript.trimmed remote=error") { + if !strings.Contains(out, "Remote outputs:") || !strings.Contains(out, "narratio.transcript.final_trimmed remote=error") { t.Fatalf("stdout = %q, want remote output error state", out) } if !strings.Contains(out, "Archive locks: error:") { @@ -898,7 +898,7 @@ func TestExecuteArchiveLoadsRemoteLocks(t *testing.T) { pipelinePath, campaignPath, sessionPath := writeValidArchiveConfigFiles(t, workspaceRoot) fake := &storage.FakeBackend{} lockKey := artifacts.S3SessionLocksKey(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03")) - fake.SeedObject(storage.FakeObject{Key: lockKey, Data: []byte("locks:\n - source: narratio.transcript.trimmed\n reason: remote review\n")}) + fake.SeedObject(storage.FakeObject{Key: lockKey, Data: []byte("locks:\n - source: narratio.transcript.final_trimmed\n reason: remote review\n")}) var storeInitCalls int restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath}) @@ -907,7 +907,7 @@ func TestExecuteArchiveLoadsRemoteLocks(t *testing.T) { // The archive stage only checks the manifest statuses and source files. _ = stageName } - mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "trimmed.json"), `{"segments":[]}`) + mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "final.trimmed.json"), `{"segments":[]}`) var stdout bytes.Buffer var stderr bytes.Buffer @@ -915,7 +915,7 @@ func TestExecuteArchiveLoadsRemoteLocks(t *testing.T) { if code != 0 { t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) } - promotedKey := artifacts.S3PromotedArtifactKey(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"), "transcripts/trimmed.json") + promotedKey := artifacts.S3PromotedArtifactKey(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"), "transcripts/final.trimmed.json") if _, ok := fake.Objects[promotedKey]; ok { t.Fatalf("locked promoted key %q was uploaded", promotedKey) } diff --git a/internal/app/post_archive_cleanup_test.go b/internal/app/post_archive_cleanup_test.go index e85e618..15bdda9 100644 --- a/internal/app/post_archive_cleanup_test.go +++ b/internal/app/post_archive_cleanup_test.go @@ -197,7 +197,7 @@ func TestPostArchiveCleanupNotRunWhenPromotionIsMissing(t *testing.T) { cfg.Pipeline.Spool.DeleteAudioAfterArchive = true cfg.Pipeline.Workspace.CleanupAfterArchive = true cfg.Pipeline.Archive.PromoteArtifacts = []config.ArchivePromotionRule{ - {Source: "narratio.transcript.merged", Dest: "transcripts/merged.json", Required: boolPtr(true)}, + {Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(true)}, } archiveStageImpl, err := stage.Select("archive") @@ -333,7 +333,7 @@ func archiveStageCleanupFixture(t *testing.T) (*config.Config, cleanupSeed, stri Enabled: boolPtr(true), UploadRun: boolPtr(true), PromoteArtifacts: []config.ArchivePromotionRule{ - {Source: "narratio.transcript.trimmed", Dest: "transcripts/trimmed.json", Required: boolPtr(true)}, + {Source: "narratio.transcript.final_trimmed", Dest: "transcripts/final.trimmed.json", Required: boolPtr(true)}, {Source: "narratio.artifact.session_recap", Dest: "artifacts/session_recap.md", Required: boolPtr(true)}, }, } @@ -371,14 +371,14 @@ func writeArchiveFixtureRunFiles(t *testing.T, runWorkDir, sessionRoot string) { t.Helper() mustWriteFile(t, filepath.Join(runWorkDir, "prepare", "inputs", "session.yml"), "session_id: 2026-05-03\n") mustWriteFile(t, filepath.Join(runWorkDir, "transcribe", "outputs", "transcripts", "raw", "speaker.json"), "{}\n") - mustWriteFile(t, filepath.Join(runWorkDir, "trim", "outputs", "transcripts", "trimmed.json"), "{\"segments\":[]}\n") + mustWriteFile(t, filepath.Join(runWorkDir, "trim", "outputs", "transcripts", "final.trimmed.json"), "{\"segments\":[]}\n") mustWriteFile(t, filepath.Join(runWorkDir, "analyze", "outputs", "artifacts", "session_recap.md"), "# recap\n") mustWriteFile(t, filepath.Join(runWorkDir, "polish", "reports", "audita.report.json"), "{}\n") mustWriteFile(t, filepath.Join(runWorkDir, "merge", "config", "seriatim.generated.yml"), "key: value\n") mustWriteFile(t, filepath.Join(runWorkDir, "logs", "audita.stderr.log"), "stderr\n") mustWriteFile(t, filepath.Join(runWorkDir, "manifest.json"), "{}\n") - mustWriteFile(t, filepath.Join(sessionRoot, "transcripts", "trimmed.json"), "{\"segments\":[]}\n") + mustWriteFile(t, filepath.Join(sessionRoot, "transcripts", "final.trimmed.json"), "{\"segments\":[]}\n") mustWriteFile(t, filepath.Join(sessionRoot, "artifacts", "session_recap.md"), "# recap\n") } diff --git a/internal/app/restore_workflow_test.go b/internal/app/restore_workflow_test.go index 6749b8e..8a0afcb 100644 --- a/internal/app/restore_workflow_test.go +++ b/internal/app/restore_workflow_test.go @@ -170,7 +170,7 @@ scriptorium: output_path: artifacts/session_recap.md inputs: transcript: - source: narratio.transcript.trimmed + source: narratio.transcript.final_trimmed required: true previous_recap: source: narratio.previous_session.artifact.session_recap @@ -184,7 +184,7 @@ previous_session_id: 2026-04-26 cfg, sessionPrefix, manifestKey, runIDKey := seedRestoreCommittedState(t, fakeStore, pipelinePath, campaignPath, sessionPath) seedRestoreObject(fakeStore, runIDKey, []byte("20260519T010203Z-a1b2c3d4\n")) seedRestoreObject(fakeStore, manifestKey, restoreWorkflowManifestJSON(t, cfg.Session.SessionID, cfg.Session.Campaign)) - seedRestoreObject(fakeStore, sessionPrefix+"transcripts/trimmed.json", []byte(`{"segments":[]}`+"\n")) + seedRestoreObject(fakeStore, sessionPrefix+"transcripts/final.trimmed.json", []byte(`{"segments":[]}`+"\n")) seedRestorePreviousCurrent(t, fakeStore, cfg, "# previous recap\n") restoreWithStoreAndRealPhases(t, fakeStore) @@ -210,7 +210,7 @@ previous_session_id: 2026-04-26 } sessionRoot := artifacts.SessionWorkDirForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID) - mustReadEquals(t, filepath.Join(sessionRoot, "transcripts", "trimmed.json"), `{"segments":[]}`+"\n") + mustReadEquals(t, filepath.Join(sessionRoot, "transcripts", "final.trimmed.json"), `{"segments":[]}`+"\n") previousManifestBytes, err := os.ReadFile(filepath.Join(sessionRoot, "previous", "manifest.json")) if err != nil { t.Fatalf("read restored previous manifest: %v", err) @@ -269,7 +269,7 @@ previous_session_id: 2026-04-26 t.Fatalf("scriptorium run requests = %d, want 1", len(scriptoriumFake.RunRequests)) } req := scriptoriumFake.RunRequests[0] - if got := req.InputPaths["transcript"]; got != filepath.Join(sessionRoot, "transcripts", "trimmed.json") { + if got := req.InputPaths["transcript"]; got != filepath.Join(sessionRoot, "transcripts", "final.trimmed.json") { t.Fatalf("transcript input = %q, want trimmed transcript path", got) } if got := req.InputPaths["previous_recap"]; got != filepath.Join(sessionRoot, "previous", "artifacts", "session_recap.md") { diff --git a/internal/app/resume_run_stage_test.go b/internal/app/resume_run_stage_test.go index 336599c..9e9f7dc 100644 --- a/internal/app/resume_run_stage_test.go +++ b/internal/app/resume_run_stage_test.go @@ -107,7 +107,7 @@ func TestRunStageExecutesOnlySelectedStage(t *testing.T) { pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json") workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03") - mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "merged.json"), `{"segments":[]}`) + mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "base.json"), `{"segments":[]}`) mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "glossary.yml"), "terms: []\n") var out bytes.Buffer @@ -137,7 +137,7 @@ func TestRunStageSkipAndForce(t *testing.T) { pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json") workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03") - mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "merged.json"), `{"segments":[]}`) + mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "base.json"), `{"segments":[]}`) mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "glossary.yml"), "terms: []\n") store := &manifest.LocalStore{} @@ -171,7 +171,7 @@ func TestRunStageForceMarksDownstreamStaleAndResumeContinuesFromStale(t *testing pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json") workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03") - mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "merged.json"), `{"segments":[]}`) + mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "base.json"), `{"segments":[]}`) mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "glossary.yml"), "terms: []\n") store := &manifest.LocalStore{} @@ -217,7 +217,7 @@ func TestRunStageTrimExecutes(t *testing.T) { pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json") workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03") - mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "normalized.json"), `{"segments":[{"id":1},{"id":2}]}`) + mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "final.json"), `{"segments":[{"id":1},{"id":2}]}`) var out bytes.Buffer err := RunStage(context.Background(), []string{"--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "trim"}, &out) @@ -246,7 +246,7 @@ func TestRunStageNormalizeExecutes(t *testing.T) { pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json") workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03") - mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "processed.json"), `{"segments":[{"id":1},{"id":2}]}`) + mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "polished.json"), `{"segments":[{"id":1},{"id":2}]}`) var out bytes.Buffer err := RunStage(context.Background(), []string{"--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "normalize"}, &out) diff --git a/internal/app/runner_test.go b/internal/app/runner_test.go index 41d907e..c8fdf9e 100644 --- a/internal/app/runner_test.go +++ b/internal/app/runner_test.go @@ -761,7 +761,7 @@ func TestExecuteStagesRunLocalArtifactsAndCanonicalPromotion(t *testing.T) { filepath.Join(runRoot, "merge", "logs", "seriatim.stdout.log"), filepath.Join(runRoot, "polish", "config", "audita.generated.yml"), filepath.Join(runRoot, "normalize", "logs", "seriatim.normalize.stdout.log"), - filepath.Join(runRoot, "trim", "outputs", "transcripts", "trimmed.json"), + filepath.Join(runRoot, "trim", "outputs", "transcripts", "final.trimmed.json"), } for _, p := range runLocalChecks { if _, statErr := os.Stat(p); statErr != nil { @@ -771,10 +771,10 @@ func TestExecuteStagesRunLocalArtifactsAndCanonicalPromotion(t *testing.T) { canonicalChecks := []string{ filepath.Join(paths.TranscriptsRawDir, "alice.json"), - filepath.Join(paths.TranscriptsDir, "merged.json"), - filepath.Join(paths.TranscriptsDir, "processed.json"), - filepath.Join(paths.TranscriptsDir, "normalized.json"), - filepath.Join(paths.TranscriptsDir, "trimmed.json"), + filepath.Join(paths.TranscriptsDir, "base.json"), + filepath.Join(paths.TranscriptsDir, "polished.json"), + filepath.Join(paths.TranscriptsDir, "final.json"), + filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), } for _, p := range canonicalChecks { if _, statErr := os.Stat(p); statErr != nil { @@ -919,7 +919,7 @@ func TestAdapterBackedStageFailureMarksManifestFailed(t *testing.T) { if ensureErr != nil { t.Fatalf("EnsureLayout() error = %v", ensureErr) } - if err := os.WriteFile(filepath.Join(paths.TranscriptsDir, "merged.json"), []byte(`{"segments":[]}`), 0o644); err != nil { + if err := os.WriteFile(filepath.Join(paths.TranscriptsDir, "base.json"), []byte(`{"segments":[]}`), 0o644); err != nil { t.Fatalf("write merged transcript: %v", err) } if err := os.WriteFile(filepath.Join(paths.InputsDir, "glossary.yml"), []byte("terms: []\n"), 0o644); err != nil { @@ -931,7 +931,7 @@ func TestAdapterBackedStageFailureMarksManifestFailed(t *testing.T) { if ensureErr != nil { t.Fatalf("EnsureLayout() error = %v", ensureErr) } - if err := os.WriteFile(filepath.Join(paths.TranscriptsDir, "processed.json"), []byte(`{"segments":[]}`), 0o644); err != nil { + if err := os.WriteFile(filepath.Join(paths.TranscriptsDir, "polished.json"), []byte(`{"segments":[]}`), 0o644); err != nil { t.Fatalf("write processed transcript: %v", err) } cfg.Pipeline.Scriptorium = &config.ScriptoriumConfig{ diff --git a/internal/artifactmodel/transcripts.go b/internal/artifactmodel/transcripts.go new file mode 100644 index 0000000..c084ce1 --- /dev/null +++ b/internal/artifactmodel/transcripts.go @@ -0,0 +1,84 @@ +package artifactmodel + +import "strings" + +const ( + SourceTranscriptBase = "narratio.transcript.base" + SourceTranscriptPolished = "narratio.transcript.polished" + SourceTranscriptFinal = "narratio.transcript.final" + SourceTranscriptFinalTrimmed = "narratio.transcript.final_trimmed" +) + +const ( + TranscriptPathBase = "transcripts/base.json" + TranscriptPathPolished = "transcripts/polished.json" + TranscriptPathFinal = "transcripts/final.json" + TranscriptPathFinalTrimmed = "transcripts/final.trimmed.json" +) + +const ( + TranscriptOutputKindBase = "transcript_base" + TranscriptOutputKindPolished = "transcript_polished" + TranscriptOutputKindFinal = "transcript_final" + TranscriptOutputKindFinalTrimmed = "transcript_final_trimmed" +) + +// TranscriptArtifactSpec describes one built-in transcript artifact mapping. +type TranscriptArtifactSpec struct { + SourceID string + CanonicalRelPath string + ProducerStage string + OutputKind string +} + +var runtimeTranscriptArtifacts = []TranscriptArtifactSpec{ + { + SourceID: SourceTranscriptBase, + CanonicalRelPath: TranscriptPathBase, + ProducerStage: "merge", + OutputKind: TranscriptOutputKindBase, + }, + { + SourceID: SourceTranscriptPolished, + CanonicalRelPath: TranscriptPathPolished, + ProducerStage: "polish", + OutputKind: TranscriptOutputKindPolished, + }, + { + SourceID: SourceTranscriptFinal, + CanonicalRelPath: TranscriptPathFinal, + ProducerStage: "normalize", + OutputKind: TranscriptOutputKindFinal, + }, + { + SourceID: SourceTranscriptFinalTrimmed, + CanonicalRelPath: TranscriptPathFinalTrimmed, + ProducerStage: "trim", + OutputKind: TranscriptOutputKindFinalTrimmed, + }, +} + +// RuntimeTranscriptArtifacts returns transcript mappings in pipeline order. +func RuntimeTranscriptArtifacts() []TranscriptArtifactSpec { + return cloneTranscriptSpecs(runtimeTranscriptArtifacts) +} + +// LookupRuntimeTranscriptArtifact returns runtime transcript metadata by source ID. +func LookupRuntimeTranscriptArtifact(sourceID string) (TranscriptArtifactSpec, bool) { + trimmed := strings.TrimSpace(sourceID) + for _, spec := range runtimeTranscriptArtifacts { + if spec.SourceID == trimmed { + return spec, true + } + } + return TranscriptArtifactSpec{}, false +} + +func cloneTranscriptSpecs(specs []TranscriptArtifactSpec) []TranscriptArtifactSpec { + if len(specs) == 0 { + return nil + } + out := make([]TranscriptArtifactSpec, len(specs)) + copy(out, specs) + return out +} diff --git a/internal/artifacts/artifact_resolver.go b/internal/artifacts/artifact_resolver.go index b98e589..96e49e9 100644 --- a/internal/artifacts/artifact_resolver.go +++ b/internal/artifacts/artifact_resolver.go @@ -9,20 +9,35 @@ import ( "regexp" "strings" + "gitea.maximumdirect.net/eric/narratio/internal/artifactmodel" "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) const ( - ArtifactTranscriptMerged = "narratio.transcript.merged" - ArtifactTranscriptPolished = "narratio.transcript.polished" - ArtifactTranscriptFull = "narratio.transcript.full" - ArtifactTranscriptTrimmed = "narratio.transcript.trimmed" - ArtifactBoundsSession = "narratio.bounds.session" + ArtifactTranscriptBase = artifactmodel.SourceTranscriptBase + ArtifactTranscriptPolished = artifactmodel.SourceTranscriptPolished + ArtifactTranscriptFinal = artifactmodel.SourceTranscriptFinal + ArtifactTranscriptFinalTrimmed = artifactmodel.SourceTranscriptFinalTrimmed + ArtifactBoundsSession = "narratio.bounds.session" ArtifactProvenancePreviousCacheManifestInput = "manifest.inputs.previous_cache" ArtifactProvenancePreviousCacheFilesystem = "current_session.previous_cache" ) +const ( + TranscriptPathBase = artifactmodel.TranscriptPathBase + TranscriptPathPolished = artifactmodel.TranscriptPathPolished + TranscriptPathFinal = artifactmodel.TranscriptPathFinal + TranscriptPathFinalTrimmed = artifactmodel.TranscriptPathFinalTrimmed +) + +const ( + TranscriptOutputKindBase = artifactmodel.TranscriptOutputKindBase + TranscriptOutputKindPolished = artifactmodel.TranscriptOutputKindPolished + TranscriptOutputKindFinal = artifactmodel.TranscriptOutputKindFinal + TranscriptOutputKindFinalTrimmed = artifactmodel.TranscriptOutputKindFinalTrimmed +) + // ErrSessionArtifactNotFound is returned when no readable artifact exists for a known ID. var ErrSessionArtifactNotFound = errors.New("session artifact not found") var configuredArtifactSourceRE = regexp.MustCompile(`^narratio\.artifact\.([a-z][a-z0-9_]*)$`) @@ -44,42 +59,27 @@ type artifactSpec struct { ContentKind artifactContentKind } -var artifactRegistry = map[string]artifactSpec{ - ArtifactTranscriptMerged: { - ID: ArtifactTranscriptMerged, - CanonicalRelPath: "transcripts/merged.json", - ProducerStage: "merge", - OutputKind: "transcript_merged", - ContentKind: contentTranscriptJSON, - }, - ArtifactTranscriptPolished: { - ID: ArtifactTranscriptPolished, - CanonicalRelPath: "transcripts/processed.json", - ProducerStage: "polish", - OutputKind: "transcript_processed", - ContentKind: contentTranscriptJSON, - }, - ArtifactTranscriptFull: { - ID: ArtifactTranscriptFull, - CanonicalRelPath: "transcripts/normalized.json", - ProducerStage: "normalize", - OutputKind: "transcript_normalized", - ContentKind: contentTranscriptJSON, - }, - ArtifactTranscriptTrimmed: { - ID: ArtifactTranscriptTrimmed, - CanonicalRelPath: "transcripts/trimmed.json", - ProducerStage: "trim", - OutputKind: "transcript_trimmed", - ContentKind: contentTranscriptJSON, - }, - ArtifactBoundsSession: { +var artifactRegistry = buildArtifactRegistry() + +func buildArtifactRegistry() map[string]artifactSpec { + registry := map[string]artifactSpec{} + for _, transcript := range RuntimeTranscriptArtifacts() { + registry[transcript.SourceID] = artifactSpec{ + ID: transcript.SourceID, + CanonicalRelPath: transcript.CanonicalRelPath, + ProducerStage: transcript.ProducerStage, + OutputKind: transcript.OutputKind, + ContentKind: contentTranscriptJSON, + } + } + registry[ArtifactBoundsSession] = artifactSpec{ ID: ArtifactBoundsSession, CanonicalRelPath: "artifacts/session_bounds.json", ProducerStage: "trim", OutputKind: "session_bounds", ContentKind: contentJSON, - }, + } + return registry } // ResolvedSessionArtifact describes one session-level artifact lookup result. diff --git a/internal/artifacts/artifact_resolver_test.go b/internal/artifacts/artifact_resolver_test.go index e939e6a..677c99d 100644 --- a/internal/artifacts/artifact_resolver_test.go +++ b/internal/artifacts/artifact_resolver_test.go @@ -23,7 +23,7 @@ func TestNormalizeSessionArtifactSource(t *testing.T) { {name: "legacy alias normalized unsupported", source: "normalized_transcript", wantErr: "unsupported artifact source"}, {name: "legacy alias trimmed unsupported", source: "trimmed_transcript", wantErr: "unsupported artifact source"}, {name: "configured source unsupported in built-in normalization", source: "narratio.artifact.session_recap", wantErr: "unsupported artifact source"}, - {name: "canonical", source: ArtifactTranscriptTrimmed, wantID: ArtifactTranscriptTrimmed}, + {name: "canonical", source: ArtifactTranscriptFinalTrimmed, wantID: ArtifactTranscriptFinalTrimmed}, {name: "unsupported", source: "narratio.unknown", wantErr: "unsupported artifact source"}, } @@ -77,7 +77,7 @@ func TestConfiguredArtifactSourceHelpers(t *testing.T) { }, { name: "built-in", - source: ArtifactTranscriptMerged, + source: ArtifactTranscriptBase, wantMatch: false, }, } @@ -160,7 +160,7 @@ func TestResolveSessionArtifactPrefersManifestOutput(t *testing.T) { if err := os.WriteFile(manifestPath, []byte(`{"segments":[]}`), 0o644); err != nil { t.Fatalf("WriteFile() error = %v", err) } - canonicalPath := filepath.Join(paths.TranscriptsDir, "normalized.json") + canonicalPath := filepath.Join(paths.TranscriptsDir, "final.json") if err := os.MkdirAll(filepath.Dir(canonicalPath), 0o755); err != nil { t.Fatalf("MkdirAll() error = %v", err) } @@ -170,10 +170,10 @@ func TestResolveSessionArtifactPrefersManifestOutput(t *testing.T) { m := manifest.New("session", time.Now().UTC()) m.MarkStageSucceeded("normalize", time.Now().UTC(), []manifest.ArtifactRecord{ - {Kind: "transcript_normalized", LocalPath: manifestPath, ProducerRunID: "run-123"}, + {Kind: "transcript_final", LocalPath: manifestPath, ProducerRunID: "run-123"}, }) - resolved, err := ResolveSessionArtifact(paths, m, ArtifactTranscriptFull) + resolved, err := ResolveSessionArtifact(paths, m, ArtifactTranscriptFinal) if err != nil { t.Fatalf("ResolveSessionArtifact() error = %v", err) } @@ -191,7 +191,7 @@ func TestResolveSessionArtifactPrefersManifestOutput(t *testing.T) { func TestResolveSessionArtifactFallsBackToCanonicalPath(t *testing.T) { workspace := t.TempDir() paths := buildSessionPaths(workspace, "campaign", "session") - canonicalPath := filepath.Join(paths.TranscriptsDir, "trimmed.json") + canonicalPath := filepath.Join(paths.TranscriptsDir, "final.trimmed.json") if err := os.MkdirAll(filepath.Dir(canonicalPath), 0o755); err != nil { t.Fatalf("MkdirAll() error = %v", err) } @@ -199,7 +199,7 @@ func TestResolveSessionArtifactFallsBackToCanonicalPath(t *testing.T) { t.Fatalf("WriteFile() error = %v", err) } - resolved, err := ResolveSessionArtifact(paths, nil, ArtifactTranscriptTrimmed) + resolved, err := ResolveSessionArtifact(paths, nil, ArtifactTranscriptFinalTrimmed) if err != nil { t.Fatalf("ResolveSessionArtifact() error = %v", err) } @@ -215,7 +215,7 @@ func TestResolveSessionArtifactMissingReturnsTypedError(t *testing.T) { workspace := t.TempDir() paths := buildSessionPaths(workspace, "campaign", "session") - _, err := ResolveSessionArtifact(paths, nil, ArtifactTranscriptTrimmed) + _, err := ResolveSessionArtifact(paths, nil, ArtifactTranscriptFinalTrimmed) if err == nil { t.Fatal("expected error, got nil") } @@ -227,7 +227,7 @@ func TestResolveSessionArtifactMissingReturnsTypedError(t *testing.T) { func TestResolveSessionArtifactValidatesTranscriptShape(t *testing.T) { workspace := t.TempDir() paths := buildSessionPaths(workspace, "campaign", "session") - canonicalPath := filepath.Join(paths.TranscriptsDir, "processed.json") + canonicalPath := filepath.Join(paths.TranscriptsDir, "polished.json") if err := os.MkdirAll(filepath.Dir(canonicalPath), 0o755); err != nil { t.Fatalf("MkdirAll() error = %v", err) } @@ -247,7 +247,7 @@ func TestResolveSessionArtifactValidatesTranscriptShape(t *testing.T) { func TestResolveSessionArtifactWithCatalogBuiltInBehaviorUnchanged(t *testing.T) { workspace := t.TempDir() paths := buildSessionPaths(workspace, "campaign", "session") - canonicalPath := filepath.Join(paths.TranscriptsDir, "trimmed.json") + canonicalPath := filepath.Join(paths.TranscriptsDir, "final.trimmed.json") if err := os.MkdirAll(filepath.Dir(canonicalPath), 0o755); err != nil { t.Fatalf("MkdirAll() error = %v", err) } @@ -255,7 +255,7 @@ func TestResolveSessionArtifactWithCatalogBuiltInBehaviorUnchanged(t *testing.T) t.Fatalf("WriteFile() error = %v", err) } - resolved, err := ResolveSessionArtifactWithCatalog(paths, nil, ArtifactTranscriptTrimmed, NewArtifactCatalog()) + resolved, err := ResolveSessionArtifactWithCatalog(paths, nil, ArtifactTranscriptFinalTrimmed, NewArtifactCatalog()) if err != nil { t.Fatalf("ResolveSessionArtifactWithCatalog() error = %v", err) } diff --git a/internal/artifacts/catalog.go b/internal/artifacts/catalog.go index e096afd..6f06538 100644 --- a/internal/artifacts/catalog.go +++ b/internal/artifacts/catalog.go @@ -215,10 +215,10 @@ func (c *ArtifactCatalog) addEntry(entry CatalogEntry) error { func runtimeBuiltInArtifactIDs() []string { return []string{ - ArtifactTranscriptMerged, + ArtifactTranscriptBase, ArtifactTranscriptPolished, - ArtifactTranscriptFull, - ArtifactTranscriptTrimmed, + ArtifactTranscriptFinal, + ArtifactTranscriptFinalTrimmed, ArtifactBoundsSession, } } diff --git a/internal/artifacts/catalog_test.go b/internal/artifacts/catalog_test.go index ed31a83..890d430 100644 --- a/internal/artifacts/catalog_test.go +++ b/internal/artifacts/catalog_test.go @@ -8,9 +8,9 @@ func TestArtifactCatalogRegisterBuiltInsAndLookup(t *testing.T) { t.Fatalf("RegisterBuiltIns() error = %v", err) } - entry, ok := catalog.Lookup(ArtifactTranscriptFull) + entry, ok := catalog.Lookup(ArtifactTranscriptFinal) if !ok { - t.Fatalf("Lookup(%q) ok = false, want true", ArtifactTranscriptFull) + t.Fatalf("Lookup(%q) ok = false, want true", ArtifactTranscriptFinal) } if !entry.Planned { t.Fatalf("entry.Planned = false, want true") @@ -18,8 +18,8 @@ func TestArtifactCatalogRegisterBuiltInsAndLookup(t *testing.T) { if entry.Executable { t.Fatalf("entry.Executable = true, want false") } - if entry.CanonicalRelPath != "transcripts/normalized.json" { - t.Fatalf("entry.CanonicalRelPath = %q, want transcripts/normalized.json", entry.CanonicalRelPath) + if entry.CanonicalRelPath != "transcripts/final.json" { + t.Fatalf("entry.CanonicalRelPath = %q, want transcripts/final.json", entry.CanonicalRelPath) } } diff --git a/internal/artifacts/previous_requirements_test.go b/internal/artifacts/previous_requirements_test.go index d6b8acf..5187404 100644 --- a/internal/artifacts/previous_requirements_test.go +++ b/internal/artifacts/previous_requirements_test.go @@ -24,7 +24,7 @@ func TestCollectPreviousArtifactRequirements(t *testing.T) { "session_recap": { Enabled: true, Inputs: map[string]config.ScriptoriumInputConfig{ - "transcript": {Source: "narratio.transcript.trimmed", Required: true}, + "transcript": {Source: "narratio.transcript.final_trimmed", Required: true}, }, }, }, diff --git a/internal/artifacts/s3_keys_test.go b/internal/artifacts/s3_keys_test.go index 7000275..e5874c4 100644 --- a/internal/artifacts/s3_keys_test.go +++ b/internal/artifacts/s3_keys_test.go @@ -43,8 +43,8 @@ func TestS3KeyConstruction(t *testing.T) { t.Fatalf("manifest key = %q", manifestKey) } - promoted := S3PromotedArtifactKey(sessionPrefix, "transcripts/trimmed.json") - if promoted != "dnd/campaigns/forsaken/sessions/2026-04-19/transcripts/trimmed.json" { + promoted := S3PromotedArtifactKey(sessionPrefix, "transcripts/final.trimmed.json") + if promoted != "dnd/campaigns/forsaken/sessions/2026-04-19/transcripts/final.trimmed.json" { t.Fatalf("promoted key = %q", promoted) } diff --git a/internal/artifacts/transcripts.go b/internal/artifacts/transcripts.go new file mode 100644 index 0000000..c7aa1b3 --- /dev/null +++ b/internal/artifacts/transcripts.go @@ -0,0 +1,25 @@ +package artifacts + +import "gitea.maximumdirect.net/eric/narratio/internal/artifactmodel" + +type TranscriptArtifactSpec = artifactmodel.TranscriptArtifactSpec + +// RuntimeTranscriptArtifacts returns the current runtime transcript mappings in pipeline order. +func RuntimeTranscriptArtifacts() []TranscriptArtifactSpec { + return artifactmodel.RuntimeTranscriptArtifacts() +} + +// PlannedTranscriptArtifacts returns the target transcript mappings for the transcript naming roadmap. +func PlannedTranscriptArtifacts() []TranscriptArtifactSpec { + return artifactmodel.RuntimeTranscriptArtifacts() +} + +// LookupRuntimeTranscriptArtifact returns current runtime transcript metadata by source ID. +func LookupRuntimeTranscriptArtifact(sourceID string) (TranscriptArtifactSpec, bool) { + return artifactmodel.LookupRuntimeTranscriptArtifact(sourceID) +} + +// LookupPlannedTranscriptArtifact returns target transcript metadata by source ID. +func LookupPlannedTranscriptArtifact(sourceID string) (TranscriptArtifactSpec, bool) { + return artifactmodel.LookupRuntimeTranscriptArtifact(sourceID) +} diff --git a/internal/artifacts/transcripts_test.go b/internal/artifacts/transcripts_test.go new file mode 100644 index 0000000..1bc842f --- /dev/null +++ b/internal/artifacts/transcripts_test.go @@ -0,0 +1,123 @@ +package artifacts + +import ( + "reflect" + "testing" +) + +func TestRuntimeTranscriptArtifacts(t *testing.T) { + want := []TranscriptArtifactSpec{ + { + SourceID: ArtifactTranscriptBase, + CanonicalRelPath: TranscriptPathBase, + ProducerStage: "merge", + OutputKind: TranscriptOutputKindBase, + }, + { + SourceID: ArtifactTranscriptPolished, + CanonicalRelPath: TranscriptPathPolished, + ProducerStage: "polish", + OutputKind: TranscriptOutputKindPolished, + }, + { + SourceID: ArtifactTranscriptFinal, + CanonicalRelPath: TranscriptPathFinal, + ProducerStage: "normalize", + OutputKind: TranscriptOutputKindFinal, + }, + { + SourceID: ArtifactTranscriptFinalTrimmed, + CanonicalRelPath: TranscriptPathFinalTrimmed, + ProducerStage: "trim", + OutputKind: TranscriptOutputKindFinalTrimmed, + }, + } + + got := RuntimeTranscriptArtifacts() + if !reflect.DeepEqual(got, want) { + t.Fatalf("RuntimeTranscriptArtifacts() = %#v, want %#v", got, want) + } + + for _, spec := range want { + gotSpec, ok := LookupRuntimeTranscriptArtifact(spec.SourceID) + if !ok { + t.Fatalf("LookupRuntimeTranscriptArtifact(%q) ok = false, want true", spec.SourceID) + } + if gotSpec != spec { + t.Fatalf("LookupRuntimeTranscriptArtifact(%q) = %#v, want %#v", spec.SourceID, gotSpec, spec) + } + } +} + +func TestPlannedTranscriptArtifacts(t *testing.T) { + want := []TranscriptArtifactSpec{ + { + SourceID: ArtifactTranscriptBase, + CanonicalRelPath: TranscriptPathBase, + ProducerStage: "merge", + OutputKind: TranscriptOutputKindBase, + }, + { + SourceID: ArtifactTranscriptPolished, + CanonicalRelPath: TranscriptPathPolished, + ProducerStage: "polish", + OutputKind: TranscriptOutputKindPolished, + }, + { + SourceID: ArtifactTranscriptFinal, + CanonicalRelPath: TranscriptPathFinal, + ProducerStage: "normalize", + OutputKind: TranscriptOutputKindFinal, + }, + { + SourceID: ArtifactTranscriptFinalTrimmed, + CanonicalRelPath: TranscriptPathFinalTrimmed, + ProducerStage: "trim", + OutputKind: TranscriptOutputKindFinalTrimmed, + }, + } + + got := PlannedTranscriptArtifacts() + if !reflect.DeepEqual(got, want) { + t.Fatalf("PlannedTranscriptArtifacts() = %#v, want %#v", got, want) + } + + for _, spec := range want { + gotSpec, ok := LookupPlannedTranscriptArtifact(spec.SourceID) + if !ok { + t.Fatalf("LookupPlannedTranscriptArtifact(%q) ok = false, want true", spec.SourceID) + } + if gotSpec != spec { + t.Fatalf("LookupPlannedTranscriptArtifact(%q) = %#v, want %#v", spec.SourceID, gotSpec, spec) + } + } +} + +func TestTranscriptArtifactSlicesAreCopies(t *testing.T) { + runtime := RuntimeTranscriptArtifacts() + runtime[0].SourceID = "changed" + if got := RuntimeTranscriptArtifacts()[0].SourceID; got != ArtifactTranscriptBase { + t.Fatalf("RuntimeTranscriptArtifacts()[0].SourceID = %q, want %q", got, ArtifactTranscriptBase) + } + + planned := PlannedTranscriptArtifacts() + planned[0].SourceID = "changed" + if got := PlannedTranscriptArtifacts()[0].SourceID; got != ArtifactTranscriptBase { + t.Fatalf("PlannedTranscriptArtifacts()[0].SourceID = %q, want %q", got, ArtifactTranscriptBase) + } +} + +func TestRuntimeArtifactRegistryUsesTranscriptSpecs(t *testing.T) { + for _, transcript := range RuntimeTranscriptArtifacts() { + spec, ok := artifactRegistry[transcript.SourceID] + if !ok { + t.Fatalf("artifactRegistry missing %q", transcript.SourceID) + } + if spec.CanonicalRelPath != transcript.CanonicalRelPath || + spec.ProducerStage != transcript.ProducerStage || + spec.OutputKind != transcript.OutputKind || + spec.ContentKind != contentTranscriptJSON { + t.Fatalf("artifactRegistry[%q] = %#v, want transcript spec %#v", transcript.SourceID, spec, transcript) + } + } +} diff --git a/internal/config/defaults.go b/internal/config/defaults.go index c93178f..576d633 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -1,5 +1,7 @@ package config +import "gitea.maximumdirect.net/eric/narratio/internal/artifactmodel" + // Default filesystem locations for config lookup when config path flags are // omitted. Order is highest to lowest precedence. const ( @@ -40,32 +42,32 @@ const ( DefaultTrimBoundsTimeout = "10m" DefaultTrimSeriatimReport = false - DefaultNormalizeOutputPath = "transcripts/normalized.json" + DefaultNormalizeOutputPath = artifactmodel.TranscriptPathFinal DefaultNormalizeOutputSchema = "seriatim-intermediate" DefaultNormalizeReport = true DefaultArchiveEnabled = true DefaultArchiveUploadRun = true - PathWorkDirSegment = "work" - PathInputsDirSegment = "inputs" - PathAudioDirSegment = "audio" - PathTranscriptsSegment = "transcripts" - PathTranscriptsRaw = "transcripts/raw" - PathTranscriptsTrimmed = "transcripts/trimmed" - PathArtifactsDirSegment = "artifacts" - PathReportsDirSegment = "reports" - PathConfigDirSegment = "config" - PathLogsDirSegment = "logs" - PathCurrentDirSegment = "current" - PathRunsDirSegment = "runs" - PathPreviousDirSegment = "previous" - PathManifestFile = "manifest.json" - PathLockFile = ".lock" - PathTranscriptMerged = "transcripts/merged.json" - PathTranscriptProcessed = "transcripts/processed.json" - PathTranscriptNormalized = "transcripts/normalized.json" - PathTranscriptTrimmed = "transcripts/trimmed.json" + PathWorkDirSegment = "work" + PathInputsDirSegment = "inputs" + PathAudioDirSegment = "audio" + PathTranscriptsSegment = "transcripts" + PathTranscriptsRaw = "transcripts/raw" + PathTranscriptsTrimmed = "transcripts/trimmed" + PathArtifactsDirSegment = "artifacts" + PathReportsDirSegment = "reports" + PathConfigDirSegment = "config" + PathLogsDirSegment = "logs" + PathCurrentDirSegment = "current" + PathRunsDirSegment = "runs" + PathPreviousDirSegment = "previous" + PathManifestFile = "manifest.json" + PathLockFile = ".lock" + PathTranscriptBase = artifactmodel.TranscriptPathBase + PathTranscriptPolished = artifactmodel.TranscriptPathPolished + PathTranscriptFinal = artifactmodel.TranscriptPathFinal + PathTranscriptFinalTrimmed = artifactmodel.TranscriptPathFinalTrimmed S3CampaignsSegment = "campaigns" S3SessionsSegment = "sessions" @@ -78,7 +80,7 @@ const ( // DefaultArchivePromoteArtifacts defines the default archive promotion rules. // Callers should copy this slice before mutating. var DefaultArchivePromoteArtifacts = []ArchivePromotionRule{ - {Source: "narratio.transcript.trimmed", Dest: PathTranscriptTrimmed}, + {Source: artifactmodel.SourceTranscriptFinalTrimmed, Dest: PathTranscriptFinalTrimmed}, } // DefaultPipelineConfigSearchPaths defines the default search order for diff --git a/internal/config/load_validate_test.go b/internal/config/load_validate_test.go index e44fa24..9e4f216 100644 --- a/internal/config/load_validate_test.go +++ b/internal/config/load_validate_test.go @@ -853,8 +853,8 @@ inputs: if cfg.Pipeline.Normalize == nil { t.Fatal("normalize config should be present via defaults") } - if cfg.Pipeline.Normalize.OutputPath != "transcripts/normalized.json" { - t.Fatalf("normalize.output_path = %q, want %q", cfg.Pipeline.Normalize.OutputPath, "transcripts/normalized.json") + if cfg.Pipeline.Normalize.OutputPath != "transcripts/final.json" { + t.Fatalf("normalize.output_path = %q, want %q", cfg.Pipeline.Normalize.OutputPath, "transcripts/final.json") } if cfg.Pipeline.Normalize.OutputSchema != "seriatim-intermediate" { t.Fatalf("normalize.output_schema = %q, want %q", cfg.Pipeline.Normalize.OutputSchema, "seriatim-intermediate") diff --git a/internal/config/normalize_test.go b/internal/config/normalize_test.go index 40833fb..c749975 100644 --- a/internal/config/normalize_test.go +++ b/internal/config/normalize_test.go @@ -21,8 +21,8 @@ func TestNormalizeLoadAndValidate(t *testing.T) { if cfg.Pipeline.Normalize == nil { t.Fatal("normalize config should be present via defaults") } - if cfg.Pipeline.Normalize.OutputPath != "transcripts/normalized.json" { - t.Fatalf("normalize.output_path = %q, want %q", cfg.Pipeline.Normalize.OutputPath, "transcripts/normalized.json") + if cfg.Pipeline.Normalize.OutputPath != "transcripts/final.json" { + t.Fatalf("normalize.output_path = %q, want %q", cfg.Pipeline.Normalize.OutputPath, "transcripts/final.json") } if cfg.Pipeline.Normalize.OutputSchema != "seriatim-intermediate" { t.Fatalf("normalize.output_schema = %q, want %q", cfg.Pipeline.Normalize.OutputSchema, "seriatim-intermediate") @@ -58,7 +58,7 @@ func TestNormalizeLoadAndValidate(t *testing.T) { { name: "invalid normalize output schema fails", normalizeYAML: `normalize: - output_path: transcripts/normalized.json + output_path: transcripts/final.json output_schema: not-a-schema report: true `, @@ -76,7 +76,7 @@ func TestNormalizeLoadAndValidate(t *testing.T) { { name: "unknown normalize field fails strict decoding", normalizeYAML: `normalize: - output_path: transcripts/normalized.json + output_path: transcripts/final.json output_schema: seriatim-intermediate report: true bogus: true diff --git a/internal/config/scriptorium_test.go b/internal/config/scriptorium_test.go index 3b9a1da..c9acdf6 100644 --- a/internal/config/scriptorium_test.go +++ b/internal/config/scriptorium_test.go @@ -198,7 +198,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) { output_path: artifacts/session_recap.md inputs: transcript: - source: narratio.transcript.trimmed + source: narratio.transcript.final_trimmed required: true `, }, @@ -287,7 +287,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) { output_path: artifacts/session_recap.md inputs: transcript: - source: narratio.transcript.trimmed + source: narratio.transcript.final_trimmed required: true player_handout: enabled: true @@ -507,6 +507,42 @@ func TestScriptoriumLoadAndValidate(t *testing.T) { } } +func TestScriptoriumLegacyTranscriptSourcesRejected(t *testing.T) { + legacyTranscriptSources := []string{ + "narratio.transcript." + "merged", + "narratio.transcript." + "full", + "narratio.transcript." + "trimmed", + } + + for _, source := range legacyTranscriptSources { + t.Run(source, func(t *testing.T) { + pipelineYAML := testPipelineBaseYAML + ` +scriptorium: + binary: scriptorium + artifacts: + session_recap: + enabled: true + prompt_id: dnd.session_recap + output_path: artifacts/session_recap.md + inputs: + transcript: + source: ` + source + ` + required: true +` + pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML) + cfg, err := Load(pipelinePath, sessionPath) + if err != nil { + t.Fatalf("Load() error = %v", err) + } + err = Validate(cfg) + wantErr := `pipeline.scriptorium.artifacts.session_recap.inputs.transcript.source "` + source + `" is unsupported` + if err == nil || !strings.Contains(err.Error(), wantErr) { + t.Fatalf("Validate() error = %v, want to contain %q", err, wantErr) + } + }) + } +} + const testPipelineBaseYAML = `workspace: root: /tmp/narratio whisperx: diff --git a/internal/config/storage_archive_test.go b/internal/config/storage_archive_test.go index 245ee9c..af87812 100644 --- a/internal/config/storage_archive_test.go +++ b/internal/config/storage_archive_test.go @@ -186,11 +186,11 @@ func TestSpoolAndArchiveDefaults(t *testing.T) { if item.Required == nil || !*item.Required { t.Fatalf("archive.promote_artifacts[0].required = %#v, want true", item.Required) } - if item.Source != "narratio.transcript.trimmed" { - t.Fatalf("archive.promote_artifacts[0].source = %q, want narratio.transcript.trimmed", item.Source) + if item.Source != "narratio.transcript.final_trimmed" { + t.Fatalf("archive.promote_artifacts[0].source = %q, want narratio.transcript.final_trimmed", item.Source) } - if item.Dest != "transcripts/trimmed.json" { - t.Fatalf("archive.promote_artifacts[0].dest = %q, want transcripts/trimmed.json", item.Dest) + if item.Dest != "transcripts/final.trimmed.json" { + t.Fatalf("archive.promote_artifacts[0].dest = %q, want transcripts/final.trimmed.json", item.Dest) } } @@ -204,8 +204,8 @@ func TestArchivePromotionValidation(t *testing.T) { name: "absolute dest path rejected", ruleYML: `archive: promote_artifacts: - - source: "narratio.transcript.trimmed" - dest: "/transcripts/trimmed.json" + - source: "narratio.transcript.final_trimmed" + dest: "/transcripts/final.trimmed.json" `, wantErr: "must be a relative path", }, @@ -213,7 +213,7 @@ func TestArchivePromotionValidation(t *testing.T) { name: "traversal dest path rejected", ruleYML: `archive: promote_artifacts: - - source: "narratio.transcript.trimmed" + - source: "narratio.transcript.final_trimmed" dest: "../trimmed.json" `, wantErr: "must not contain path traversal", @@ -223,7 +223,7 @@ func TestArchivePromotionValidation(t *testing.T) { ruleYML: `archive: promote_artifacts: - source: "narratio.unknown" - dest: "transcripts/trimmed.json" + dest: "transcripts/final.trimmed.json" `, wantErr: "source \"narratio.unknown\" is unsupported", }, @@ -231,9 +231,9 @@ func TestArchivePromotionValidation(t *testing.T) { name: "duplicate destination rejected", ruleYML: `archive: promote_artifacts: - - source: "narratio.transcript.trimmed" + - source: "narratio.transcript.final_trimmed" dest: "artifacts/shared.md" - - source: "narratio.transcript.full" + - source: "narratio.transcript.final" dest: "artifacts/shared.md" `, wantErr: "duplicates another archive promotion destination", @@ -278,6 +278,35 @@ archive: } } +func TestArchivePromotionLegacyTranscriptSourcesRejected(t *testing.T) { + legacyTranscriptSources := []string{ + "narratio.transcript." + "merged", + "narratio.transcript." + "full", + "narratio.transcript." + "trimmed", + } + + for _, source := range legacyTranscriptSources { + t.Run(source, func(t *testing.T) { + pipelineYAML := testPipelineBaseYAML + ` +archive: + promote_artifacts: + - source: ` + source + ` + dest: transcripts/final.trimmed.json +` + pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML) + cfg, err := Load(pipelinePath, sessionPath) + if err != nil { + t.Fatalf("Load() error = %v", err) + } + err = Validate(cfg) + wantErr := `source "` + source + `" is unsupported` + if err == nil || !strings.Contains(err.Error(), wantErr) { + t.Fatalf("Validate() error = %v, want to contain %q", err, wantErr) + } + }) + } +} + func TestArchivePromotionDerivesDestinationWhenOmitted(t *testing.T) { tests := []struct { name string @@ -289,9 +318,9 @@ func TestArchivePromotionDerivesDestinationWhenOmitted(t *testing.T) { pipelineYML: testPipelineBaseYAML + ` archive: promote_artifacts: - - source: narratio.transcript.full + - source: narratio.transcript.final `, - wantDest: "transcripts/normalized.json", + wantDest: "transcripts/final.json", }, { name: "configured source derives configured output path", @@ -341,7 +370,7 @@ func TestArchiveLockValidation(t *testing.T) { pipelineYML: testPipelineBaseYAML + ` archive: locks: - - source: narratio.transcript.trimmed + - source: narratio.transcript.final_trimmed reason: reviewed transcript `, }, @@ -382,8 +411,8 @@ archive: pipelineYML: testPipelineBaseYAML + ` archive: locks: - - source: narratio.transcript.trimmed - - source: " narratio.transcript.trimmed " + - source: narratio.transcript.final_trimmed + - source: " narratio.transcript.final_trimmed " `, wantErr: "duplicates another archive lock source", }, @@ -410,12 +439,40 @@ archive: } } +func TestArchiveLockLegacyTranscriptSourcesRejected(t *testing.T) { + legacyTranscriptSources := []string{ + "narratio.transcript." + "merged", + "narratio.transcript." + "full", + "narratio.transcript." + "trimmed", + } + + for _, source := range legacyTranscriptSources { + t.Run(source, func(t *testing.T) { + pipelineYAML := testPipelineBaseYAML + ` +archive: + locks: + - source: ` + source + ` +` + pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML) + cfg, err := Load(pipelinePath, sessionPath) + if err != nil { + t.Fatalf("Load() error = %v", err) + } + err = Validate(cfg) + wantErr := `source "` + source + `" is unsupported` + if err == nil || !strings.Contains(err.Error(), wantErr) { + t.Fatalf("Validate() error = %v, want to contain %q", err, wantErr) + } + }) + } +} + func TestArchiveLockUnknownFieldFailsStrictDecode(t *testing.T) { pipelineYAML := testPipelineBaseYAML + ` archive: locks: - - source: narratio.transcript.trimmed - dest: transcripts/trimmed.json + - source: narratio.transcript.final_trimmed + dest: transcripts/final.trimmed.json ` pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML) _, err := Load(pipelinePath, sessionPath) @@ -428,8 +485,8 @@ func TestArchiveLegacyFromToFailsStrictDecode(t *testing.T) { pipelineYAML := testPipelineBaseYAML + ` archive: promote_artifacts: - - from: transcripts/trimmed.json - to: transcripts/trimmed.json + - from: transcripts/final.trimmed.json + to: transcripts/final.trimmed.json ` pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML) _, err := Load(pipelinePath, sessionPath) @@ -440,27 +497,27 @@ archive: func TestArchiveLockStoreBytesStrictDecodeAndValidation(t *testing.T) { store, err := LoadArchiveLockStoreBytes("locks.yml", []byte(`locks: - - source: narratio.transcript.trimmed + - source: narratio.transcript.final_trimmed reason: reviewed `), nil) if err != nil { t.Fatalf("LoadArchiveLockStoreBytes() error = %v", err) } - if len(store.Locks) != 1 || store.Locks[0].Source != "narratio.transcript.trimmed" || store.Locks[0].Reason != "reviewed" { + if len(store.Locks) != 1 || store.Locks[0].Source != "narratio.transcript.final_trimmed" || store.Locks[0].Reason != "reviewed" { t.Fatalf("locks = %#v", store.Locks) } _, err = LoadArchiveLockStoreBytes("locks.yml", []byte(`locks: - - source: narratio.transcript.trimmed - dest: transcripts/trimmed.json + - source: narratio.transcript.final_trimmed + dest: transcripts/final.trimmed.json `), nil) if err == nil || !strings.Contains(err.Error(), "strict decode failed") { t.Fatalf("unknown field error = %v, want strict decode failed", err) } _, err = LoadArchiveLockStoreBytes("locks.yml", []byte(`locks: - - source: narratio.transcript.trimmed - - source: narratio.transcript.trimmed + - source: narratio.transcript.final_trimmed + - source: narratio.transcript.final_trimmed `), nil) if err == nil || !strings.Contains(err.Error(), "duplicates another archive lock source") { t.Fatalf("duplicate error = %v", err) @@ -469,19 +526,19 @@ func TestArchiveLockStoreBytesStrictDecodeAndValidation(t *testing.T) { func TestMergeArchiveLockRulesStaticWins(t *testing.T) { merged := MergeArchiveLockRules( - []ArchiveLockRule{{Source: "narratio.transcript.trimmed", Reason: "static"}}, + []ArchiveLockRule{{Source: "narratio.transcript.final_trimmed", Reason: "static"}}, []ArchiveLockRule{ - {Source: "narratio.transcript.trimmed", Reason: "remote"}, - {Source: "narratio.transcript.full", Reason: "remote full"}, + {Source: "narratio.transcript.final_trimmed", Reason: "remote"}, + {Source: "narratio.transcript.final", Reason: "remote full"}, }, ) if len(merged) != 2 { t.Fatalf("merged len = %d, want 2: %#v", len(merged), merged) } - if merged[0].Source != "narratio.transcript.trimmed" || merged[0].Reason != "static" { + if merged[0].Source != "narratio.transcript.final_trimmed" || merged[0].Reason != "static" { t.Fatalf("merged[0] = %#v, want static lock", merged[0]) } - if merged[1].Source != "narratio.transcript.full" { + if merged[1].Source != "narratio.transcript.final" { t.Fatalf("merged[1] = %#v, want remote full lock", merged[1]) } } diff --git a/internal/config/trim_test.go b/internal/config/trim_test.go index 0a71f9f..ca95607 100644 --- a/internal/config/trim_test.go +++ b/internal/config/trim_test.go @@ -17,7 +17,7 @@ func TestTrimLoadAndValidate(t *testing.T) { name: "valid trim config", trimYAML: `trim: enabled: true - output_path: transcripts/trimmed.json + output_path: transcripts/final.trimmed.json bounds: prompt_id: dnd_session.bounds profile_id: "" @@ -45,7 +45,7 @@ func TestTrimLoadAndValidate(t *testing.T) { { name: "enabled omitted defaults disabled", trimYAML: `trim: - output_path: transcripts/trimmed.json + output_path: transcripts/final.trimmed.json bounds: prompt_id: dnd_session.bounds transcript_input_name: transcript @@ -65,7 +65,7 @@ func TestTrimLoadAndValidate(t *testing.T) { name: "missing prompt id fails when enabled", trimYAML: `trim: enabled: true - output_path: transcripts/trimmed.json + output_path: transcripts/final.trimmed.json bounds: transcript_input_name: transcript output_path: artifacts/session_bounds.json @@ -76,7 +76,7 @@ func TestTrimLoadAndValidate(t *testing.T) { name: "missing transcript input name fails when enabled", trimYAML: `trim: enabled: true - output_path: transcripts/trimmed.json + output_path: transcripts/final.trimmed.json bounds: prompt_id: dnd_session.bounds output_path: artifacts/session_bounds.json @@ -87,7 +87,7 @@ func TestTrimLoadAndValidate(t *testing.T) { name: "missing bounds output path fails when enabled", trimYAML: `trim: enabled: true - output_path: transcripts/trimmed.json + output_path: transcripts/final.trimmed.json bounds: prompt_id: dnd_session.bounds transcript_input_name: transcript @@ -109,7 +109,7 @@ func TestTrimLoadAndValidate(t *testing.T) { name: "invalid timeout fails", trimYAML: `trim: enabled: true - output_path: transcripts/trimmed.json + output_path: transcripts/final.trimmed.json bounds: prompt_id: dnd_session.bounds transcript_input_name: transcript @@ -122,7 +122,7 @@ func TestTrimLoadAndValidate(t *testing.T) { name: "render debug true requires render output path", trimYAML: `trim: enabled: true - output_path: transcripts/trimmed.json + output_path: transcripts/final.trimmed.json bounds: prompt_id: dnd_session.bounds transcript_input_name: transcript @@ -135,7 +135,7 @@ func TestTrimLoadAndValidate(t *testing.T) { name: "unknown trim field fails strict decoding", trimYAML: `trim: enabled: true - output_path: transcripts/trimmed.json + output_path: transcripts/final.trimmed.json bounds: prompt_id: dnd_session.bounds transcript_input_name: transcript diff --git a/internal/config/validate.go b/internal/config/validate.go index e683a09..dc4d854 100644 --- a/internal/config/validate.go +++ b/internal/config/validate.go @@ -7,6 +7,8 @@ import ( "regexp" "strings" "time" + + "gitea.maximumdirect.net/eric/narratio/internal/artifactmodel" ) // Validate checks resolved configuration for required fields and parseable durations. @@ -224,12 +226,11 @@ func MergeArchiveLockRules(staticLocks, remoteLocks []ArchiveLockRule) []Archive func archiveSourceKnown(source string, scriptorium *ScriptoriumConfig) (string, error) { trimmed := strings.TrimSpace(source) + if _, ok := artifactmodel.LookupRuntimeTranscriptArtifact(trimmed); ok { + return "", nil + } switch trimmed { - case "narratio.transcript.merged", - "narratio.transcript.polished", - "narratio.transcript.full", - "narratio.transcript.trimmed", - "narratio.bounds.session": + case "narratio.bounds.session": return "", nil } matches := narratioArtifactSourceRE.FindStringSubmatch(trimmed) @@ -248,15 +249,10 @@ func archiveSourceKnown(source string, scriptorium *ScriptoriumConfig) (string, func deriveArchivePromotionDest(source string, scriptorium *ScriptoriumConfig) (string, error) { trimmed := strings.TrimSpace(source) + if spec, ok := artifactmodel.LookupRuntimeTranscriptArtifact(trimmed); ok { + return spec.CanonicalRelPath, nil + } switch trimmed { - case "narratio.transcript.merged": - return PathTranscriptMerged, nil - case "narratio.transcript.polished": - return PathTranscriptProcessed, nil - case "narratio.transcript.full": - return PathTranscriptNormalized, nil - case "narratio.transcript.trimmed": - return PathTranscriptTrimmed, nil case "narratio.bounds.session": return filepath.ToSlash(filepath.Join(PathArtifactsDirSegment, "session_bounds.json")), nil } @@ -716,15 +712,10 @@ func validateScriptoriumInputSource(artifactName, inputName, source string, conf } func isStaticSupportedScriptoriumInputSource(source string) bool { + if _, ok := artifactmodel.LookupRuntimeTranscriptArtifact(source); ok { + return true + } switch source { - case "narratio.transcript.merged": - return true - case "narratio.transcript.polished": - return true - case "narratio.transcript.full": - return true - case "narratio.transcript.trimmed": - return true case "narratio.bounds.session": return true default: diff --git a/internal/contracts/bounds_test.go b/internal/contracts/bounds_test.go index 5edc729..06f33d7 100644 --- a/internal/contracts/bounds_test.go +++ b/internal/contracts/bounds_test.go @@ -11,7 +11,7 @@ import ( func TestValidBoundsOutputProducesKeepSelector(t *testing.T) { dir := t.TempDir() boundsPath := filepath.Join(dir, "bounds.json") - transcriptPath := filepath.Join(dir, "processed.json") + transcriptPath := filepath.Join(dir, "polished.json") writeBoundsTestFile(t, boundsPath, `{ "confidence":"high", @@ -103,7 +103,7 @@ func TestBoundsNonExistentEndIDFails(t *testing.T) { func TestInvalidTranscriptJSONFails(t *testing.T) { bounds := SessionBounds{TrimAction: "trim", StartSegmentID: intPtr(1), EndSegmentID: intPtr(2)} - path := filepath.Join(t.TempDir(), "processed.json") + path := filepath.Join(t.TempDir(), "polished.json") writeBoundsTestFile(t, path, "not-json") err := ValidateSessionBoundsAgainstTranscript(bounds, path) @@ -117,7 +117,7 @@ func TestInvalidTranscriptJSONFails(t *testing.T) { func TestTranscriptWithoutSegmentsFails(t *testing.T) { bounds := SessionBounds{TrimAction: "trim", StartSegmentID: intPtr(1), EndSegmentID: intPtr(2)} - path := filepath.Join(t.TempDir(), "processed.json") + path := filepath.Join(t.TempDir(), "polished.json") writeBoundsTestFile(t, path, `{"schema":"audita.processed.v1"}`) err := ValidateSessionBoundsAgainstTranscript(bounds, path) @@ -170,7 +170,7 @@ func TestNoTrimActionCopyIsSupported(t *testing.T) { func writeTranscriptWithIDs(t *testing.T, ids ...int) string { t.Helper() - path := filepath.Join(t.TempDir(), "processed.json") + path := filepath.Join(t.TempDir(), "polished.json") if len(ids) == 0 { writeBoundsTestFile(t, path, `{"segments":[]}`) return path diff --git a/internal/manifest/manifest_test.go b/internal/manifest/manifest_test.go index 6d78310..e39b6a3 100644 --- a/internal/manifest/manifest_test.go +++ b/internal/manifest/manifest_test.go @@ -23,7 +23,7 @@ func TestStageMarkHelpers(t *testing.T) { } succeededAt := runningAt.Add(2 * time.Minute) - outputs := []ArtifactRecord{{Kind: "transcript_processed", LocalPath: "transcripts/processed.json"}} + outputs := []ArtifactRecord{{Kind: "transcript_polished", LocalPath: "transcripts/polished.json"}} m.MarkStageSucceeded("transcribe", succeededAt, outputs) if stage.Status != StatusSucceeded { t.Fatalf("status = %q, want %q", stage.Status, StatusSucceeded) @@ -35,7 +35,7 @@ func TestStageMarkHelpers(t *testing.T) { t.Fatalf("outputs len = %d, want 1", len(stage.Outputs)) } outputs[0].LocalPath = "mutated.json" - if stage.Outputs[0].LocalPath != "transcripts/processed.json" { + if stage.Outputs[0].LocalPath != "transcripts/polished.json" { t.Fatalf("stage outputs should be copied, got %#v", stage.Outputs) } diff --git a/internal/manifest/store_test.go b/internal/manifest/store_test.go index 012e9a1..74b36ec 100644 --- a/internal/manifest/store_test.go +++ b/internal/manifest/store_test.go @@ -21,7 +21,7 @@ func TestLocalStoreCreateSaveLoadRoundTrip(t *testing.T) { now := time.Date(2026, 5, 3, 12, 0, 0, 0, time.UTC) m.MarkStageRunning("prepare", now) - m.MarkStageSucceeded("prepare", now.Add(2*time.Second), []ArtifactRecord{{Kind: "transcript", LocalPath: "transcripts/merged.json"}}) + m.MarkStageSucceeded("prepare", now.Add(2*time.Second), []ArtifactRecord{{Kind: "transcript", LocalPath: "transcripts/base.json"}}) m.Campaign = "forsaken" m.RunID = "20260515T031522Z-a1b2c3d4" m.LocalWorkDir = "/var/lib/narratio/work/forsaken/2026-05-03/20260515T031522Z-a1b2c3d4" diff --git a/internal/stage/analyze.go b/internal/stage/analyze.go index 06853d8..e547377 100644 --- a/internal/stage/analyze.go +++ b/internal/stage/analyze.go @@ -24,9 +24,9 @@ func (analyzeStage) Name() string { return "analyze" } func (analyzeStage) Declares() IODecl { return IODecl{ Inputs: []artifacts.Ref{ - {Kind: "transcript_processed", Category: "transcripts", RelativePath: "transcripts/processed.json"}, - {Kind: "transcript_normalized", Category: "transcripts", RelativePath: "transcripts/normalized.json"}, - {Kind: "transcript_trimmed", Category: "transcripts", RelativePath: "transcripts/trimmed.json"}, + {Kind: "transcript_polished", Category: "transcripts", RelativePath: "transcripts/polished.json"}, + {Kind: "transcript_final", Category: "transcripts", RelativePath: "transcripts/final.json"}, + {Kind: "transcript_final_trimmed", Category: "transcripts", RelativePath: "transcripts/final.trimmed.json"}, }, Outputs: nil, } @@ -562,7 +562,7 @@ func discoverProcessedTranscript(m *manifest.Manifest, paths artifacts.SessionPa if m != nil && m.Stages != nil { if sr := m.Stages["polish"]; sr != nil { for _, out := range sr.Outputs { - if out.Kind != "transcript_processed" { + if out.Kind != "transcript_polished" { continue } p := strings.TrimSpace(out.LocalPath) @@ -581,7 +581,7 @@ func discoverProcessedTranscript(m *manifest.Manifest, paths artifacts.SessionPa } } - fallback := filepath.Join(paths.TranscriptsDir, "processed.json") + fallback := filepath.Join(paths.TranscriptsDir, "polished.json") if info, err := os.Stat(fallback); err == nil && !info.IsDir() { return filepath.Clean(fallback), "fallback.transcripts_dir", nil } @@ -602,8 +602,8 @@ type analyzeTranscriptInputs struct { func discoverAnalyzeTranscriptRefs(m *manifest.Manifest, paths artifacts.SessionPaths) analyzeTranscriptInputs { processedPath, processedSource := discoverAnalyzeArtifactRef(m, paths, artifacts.ArtifactTranscriptPolished) - normalizedPath, normalizedSource := discoverAnalyzeArtifactRef(m, paths, artifacts.ArtifactTranscriptFull) - trimmedPath, trimmedSource := discoverAnalyzeArtifactRef(m, paths, artifacts.ArtifactTranscriptTrimmed) + normalizedPath, normalizedSource := discoverAnalyzeArtifactRef(m, paths, artifacts.ArtifactTranscriptFinal) + trimmedPath, trimmedSource := discoverAnalyzeArtifactRef(m, paths, artifacts.ArtifactTranscriptFinalTrimmed) return analyzeTranscriptInputs{ ProcessedPath: processedPath, ProcessedSource: processedSource, @@ -669,9 +669,9 @@ func resolveScriptoriumInput( switch normalized { case artifacts.ArtifactTranscriptPolished: return "", false, nil, nil - case artifacts.ArtifactTranscriptFull: + case artifacts.ArtifactTranscriptFinal: return "", false, nil, fmt.Errorf("normalized transcript input is unavailable; run normalize stage first") - case artifacts.ArtifactTranscriptTrimmed: + case artifacts.ArtifactTranscriptFinalTrimmed: return "", false, nil, fmt.Errorf("trimmed transcript input is unavailable; run trim stage first") default: return "", false, nil, nil diff --git a/internal/stage/analyze_test.go b/internal/stage/analyze_test.go index 7bd81fa..c2cd8f8 100644 --- a/internal/stage/analyze_test.go +++ b/internal/stage/analyze_test.go @@ -19,7 +19,7 @@ import ( func TestAnalyzeGeneratesSessionRecapFromTrimmedTranscript(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) result, err := (analyzeStage{}).Run(context.Background(), env, m) if err != nil { @@ -36,7 +36,7 @@ func TestAnalyzeGeneratesSessionRecapFromTrimmedTranscript(t *testing.T) { if req.ProfileID != "local-quality" { t.Fatalf("profile id = %q, want local-quality", req.ProfileID) } - if req.InputPaths["transcript"] != filepath.Join(paths.TranscriptsDir, "trimmed.json") { + if req.InputPaths["transcript"] != filepath.Join(paths.TranscriptsDir, "final.trimmed.json") { t.Fatalf("transcript input = %q, want trimmed transcript path", req.InputPaths["transcript"]) } if req.OutputPath != filepath.Join(paths.ArtifactsDir, "session_recap.md") { @@ -72,7 +72,7 @@ func TestAnalyzeGeneratesSessionRecapFromTrimmedTranscript(t *testing.T) { func TestAnalyzeRenderDebugFalseDoesNotCallRenderArtifact(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) env.Config.Pipeline.Scriptorium.RenderDebug = false @@ -88,7 +88,7 @@ func TestAnalyzeRenderDebugFalseDoesNotCallRenderArtifact(t *testing.T) { func TestAnalyzeRenderDebugArtifactOverrideFalseWinsOverGlobalTrue(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) env.Config.Pipeline.Scriptorium.RenderDebug = true artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] @@ -111,7 +111,7 @@ func TestAnalyzeRenderDebugArtifactOverrideFalseWinsOverGlobalTrue(t *testing.T) func TestAnalyzeRenderDebugTrueCallsRenderBeforeRun(t *testing.T) { env, m, _ := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) env.Config.Pipeline.Scriptorium.RenderDebug = true runner := &orderedScriptoriumRunner{ @@ -132,7 +132,7 @@ func TestAnalyzeRenderDebugTrueCallsRenderBeforeRun(t *testing.T) { func TestAnalyzeRenderOutputPathIsRecorded(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) env.Config.Pipeline.Scriptorium.RenderDebug = true result, err := (analyzeStage{}).Run(context.Background(), env, m) @@ -156,7 +156,7 @@ func TestAnalyzeRenderOutputPathIsRecorded(t *testing.T) { func TestAnalyzeRenderFailurePreventsRun(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) env.Config.Pipeline.Scriptorium.RenderDebug = true fake.RenderErr = errors.New("render boom") @@ -178,7 +178,7 @@ func TestAnalyzeRenderFailurePreventsRun(t *testing.T) { func TestAnalyzeRenderInvalidJSONFailsClearly(t *testing.T) { env, m, _ := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) env.Config.Pipeline.Scriptorium.RenderDebug = true runner := &orderedScriptoriumRunner{ RenderBody: `not-json`, @@ -201,7 +201,7 @@ func TestAnalyzeRenderInvalidJSONFailsClearly(t *testing.T) { func TestAnalyzeRunStillSucceedsWhenRenderSucceeds(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) env.Config.Pipeline.Scriptorium.RenderDebug = true result, err := (analyzeStage{}).Run(context.Background(), env, m) @@ -222,7 +222,7 @@ func TestAnalyzeRunStillSucceedsWhenRenderSucceeds(t *testing.T) { func TestAnalyzeOmitsOptionalCanonicalPreviousRecapWhenUnavailable(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[]}`) env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = config.ScriptoriumArtifactConfig{ Enabled: true, @@ -261,7 +261,7 @@ func TestAnalyzeUsesRunLocalPathsAndPromotesCanonical(t *testing.T) { m.Campaign = "sample-campaign" m.RunID = "20260518T010203Z-abcdef12" paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) result, err := (analyzeStage{}).Run(context.Background(), env, m) if err != nil { @@ -344,7 +344,7 @@ func (r *orderedScriptoriumRunner) RunArtifact(_ context.Context, req scriptoriu func TestAnalyzeIncludesCanonicalPreviousRecapWhenPreparedCacheExists(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[]}`) previousRecapPath := artifacts.SessionPreviousArtifactPath(paths, "artifacts/session_recap.md") writeAnalyzeFile(t, previousRecapPath, "previous recap\n") @@ -383,7 +383,7 @@ func TestAnalyzeIncludesCanonicalPreviousRecapWhenPreparedCacheExists(t *testing func TestAnalyzeFailsWhenRequiredCanonicalPreviousRecapMissing(t *testing.T) { env, m, _ := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[]}`) env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = config.ScriptoriumArtifactConfig{ Enabled: true, @@ -413,7 +413,7 @@ func TestAnalyzeFailsWhenRequiredCanonicalPreviousRecapMissing(t *testing.T) { func TestAnalyzeResolvesConfiguredArtifactInputFromDisabledArtifactOutput(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) playerHandoutPath := filepath.Join(paths.ArtifactsDir, "player_handout.md") writeAnalyzeFile(t, playerHandoutPath, "handout\n") @@ -443,7 +443,7 @@ func TestAnalyzeResolvesConfiguredArtifactInputFromDisabledArtifactOutput(t *tes func TestAnalyzeMetadataIncludesGeneratedAndReusedArtifacts(t *testing.T) { env, m, _ := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) playerHandoutPath := filepath.Join(paths.ArtifactsDir, "player_handout.md") writeAnalyzeFile(t, playerHandoutPath, "handout\n") @@ -512,7 +512,7 @@ func TestAnalyzeMetadataIncludesGeneratedAndReusedArtifacts(t *testing.T) { func TestAnalyzeRunsMultipleIndependentArtifactsInDeterministicOrder(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) env.Config.Pipeline.Scriptorium.Artifacts["player_handout"] = config.ScriptoriumArtifactConfig{ Enabled: true, @@ -521,7 +521,7 @@ func TestAnalyzeRunsMultipleIndependentArtifactsInDeterministicOrder(t *testing. OutputPath: "artifacts/player_handout.md", Inputs: map[string]config.ScriptoriumInputConfig{ "transcript": { - Source: "narratio.transcript.trimmed", + Source: "narratio.transcript.final_trimmed", Required: true, }, }, @@ -553,7 +553,7 @@ func TestAnalyzeRunsMultipleIndependentArtifactsInDeterministicOrder(t *testing. func TestAnalyzeRunsDependenciesBeforeDependents(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) env.Config.Pipeline.Scriptorium.Artifacts["player_handout"] = config.ScriptoriumArtifactConfig{ Enabled: true, @@ -567,7 +567,7 @@ func TestAnalyzeRunsDependenciesBeforeDependents(t *testing.T) { Required: true, }, "transcript": { - Source: "narratio.transcript.trimmed", + Source: "narratio.transcript.final_trimmed", Required: true, }, }, @@ -594,7 +594,7 @@ func TestAnalyzeRunsDependenciesBeforeDependents(t *testing.T) { func TestAnalyzeAppliesSelectedArtifactsFilter(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) env.Config.Pipeline.Scriptorium.Artifacts["player_handout"] = config.ScriptoriumArtifactConfig{ Enabled: true, @@ -603,7 +603,7 @@ func TestAnalyzeAppliesSelectedArtifactsFilter(t *testing.T) { OutputPath: "artifacts/player_handout.md", Inputs: map[string]config.ScriptoriumInputConfig{ "transcript": { - Source: "narratio.transcript.trimmed", + Source: "narratio.transcript.final_trimmed", Required: true, }, }, @@ -628,7 +628,7 @@ func TestAnalyzeAppliesSelectedArtifactsFilter(t *testing.T) { func TestAnalyzeMetadataIncludesMultipleGeneratedArtifacts(t *testing.T) { env, m, _ := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) env.Config.Pipeline.Scriptorium.Artifacts["player_handout"] = config.ScriptoriumArtifactConfig{ Enabled: true, @@ -637,7 +637,7 @@ func TestAnalyzeMetadataIncludesMultipleGeneratedArtifacts(t *testing.T) { OutputPath: "artifacts/player_handout.md", Inputs: map[string]config.ScriptoriumInputConfig{ "transcript": { - Source: "narratio.transcript.trimmed", + Source: "narratio.transcript.final_trimmed", Required: true, }, }, @@ -663,7 +663,7 @@ func TestAnalyzeMetadataIncludesMultipleGeneratedArtifacts(t *testing.T) { func TestAnalyzeFailsWhenRequiredConfiguredArtifactMissing(t *testing.T) { env, m, _ := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) sessionRecap := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] sessionRecap.Inputs["recap"] = config.ScriptoriumInputConfig{ @@ -688,7 +688,7 @@ func TestAnalyzeFailsWhenRequiredConfiguredArtifactMissing(t *testing.T) { func TestAnalyzeOmitsOptionalMissingConfiguredArtifactInput(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) sessionRecap := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] sessionRecap.Inputs["recap"] = config.ScriptoriumInputConfig{ @@ -735,7 +735,7 @@ func TestAnalyzeRequiredPreviousSessionArtifactInputGuidesPrepareForce(t *testin func TestAnalyzeResolvesCanonicalPreviousSessionArtifactFromManifestInput(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) previousPath := artifacts.SessionPreviousArtifactPath(paths, "artifacts/session_recap.md") writeAnalyzeFile(t, previousPath, "previous recap\n") @@ -766,7 +766,7 @@ func TestAnalyzeResolvesCanonicalPreviousSessionArtifactFromManifestInput(t *tes func TestAnalyzeOmitsOptionalMissingCanonicalPreviousSessionArtifact(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) sessionRecap := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] sessionRecap.Inputs["previous_recap"] = config.ScriptoriumInputConfig{ @@ -790,7 +790,7 @@ func TestAnalyzeOmitsOptionalMissingCanonicalPreviousSessionArtifact(t *testing. func TestAnalyzeRenderDebugWithCanonicalPreviousSessionInput(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) env.Config.Pipeline.Scriptorium.RenderDebug = true previousPath := artifacts.SessionPreviousArtifactPath(paths, "artifacts/session_recap.md") @@ -825,7 +825,7 @@ func TestAnalyzeRenderDebugWithCanonicalPreviousSessionInput(t *testing.T) { func TestAnalyzeDoesNotCallObjectStoreForCanonicalPreviousSessionInput(t *testing.T) { env, m, _ := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) previousPath := artifacts.SessionPreviousArtifactPath(paths, "artifacts/session_recap.md") writeAnalyzeFile(t, previousPath, "previous recap\n") @@ -856,7 +856,7 @@ func TestAnalyzeDoesNotCallObjectStoreForCanonicalPreviousSessionInput(t *testin func TestAnalyzeFailsWhenOutputPathMissing(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] artifact.OutputPath = "" @@ -891,7 +891,7 @@ func TestAnalyzeFailsWhenTrimmedTranscriptMissing(t *testing.T) { func TestAnalyzeSupportsProcessedTranscriptSourceWhenConfigured(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[]}`) artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{ @@ -907,7 +907,7 @@ func TestAnalyzeSupportsProcessedTranscriptSourceWhenConfigured(t *testing.T) { if len(fake.RunRequests) != 1 { t.Fatalf("run requests = %d, want 1", len(fake.RunRequests)) } - if fake.RunRequests[0].InputPaths["transcript"] != filepath.Join(paths.TranscriptsDir, "processed.json") { + if fake.RunRequests[0].InputPaths["transcript"] != filepath.Join(paths.TranscriptsDir, "polished.json") { t.Fatalf("transcript input = %q, want processed transcript path", fake.RunRequests[0].InputPaths["transcript"]) } } @@ -915,11 +915,11 @@ func TestAnalyzeSupportsProcessedTranscriptSourceWhenConfigured(t *testing.T) { func TestAnalyzeSupportsCanonicalTrimmedTranscriptSourceWhenConfigured(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{ - Source: "narratio.transcript.trimmed", + Source: "narratio.transcript.final_trimmed", Required: true, } env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact @@ -931,7 +931,7 @@ func TestAnalyzeSupportsCanonicalTrimmedTranscriptSourceWhenConfigured(t *testin if len(fake.RunRequests) != 1 { t.Fatalf("run requests = %d, want 1", len(fake.RunRequests)) } - if fake.RunRequests[0].InputPaths["transcript"] != filepath.Join(paths.TranscriptsDir, "trimmed.json") { + if fake.RunRequests[0].InputPaths["transcript"] != filepath.Join(paths.TranscriptsDir, "final.trimmed.json") { t.Fatalf("transcript input = %q, want trimmed transcript path", fake.RunRequests[0].InputPaths["transcript"]) } } @@ -939,12 +939,12 @@ func TestAnalyzeSupportsCanonicalTrimmedTranscriptSourceWhenConfigured(t *testin func TestAnalyzeSupportsNormalizedTranscriptSourceWhenConfigured(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - normalizedPath := filepath.Join(paths.TranscriptsDir, "normalized.json") + normalizedPath := filepath.Join(paths.TranscriptsDir, "final.json") writeAnalyzeFile(t, normalizedPath, `{"segments":[{"id":1}]}`) artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{ - Source: "narratio.transcript.full", + Source: "narratio.transcript.final", Required: true, } env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact @@ -964,17 +964,17 @@ func TestAnalyzeSupportsNormalizedTranscriptSourceWhenConfigured(t *testing.T) { func TestAnalyzeSupportsCanonicalNormalizedTranscriptSourceFromManifestOutput(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - fallbackPath := filepath.Join(paths.TranscriptsDir, "normalized.json") + fallbackPath := filepath.Join(paths.TranscriptsDir, "final.json") manifestPath := filepath.Join(paths.ArtifactsDir, "normalized.from-manifest.json") writeAnalyzeFile(t, fallbackPath, `{"segments":[{"id":999}]}`) writeAnalyzeFile(t, manifestPath, `{"segments":[{"id":10}]}`) m.MarkStageSucceeded("normalize", time.Now().UTC(), []manifest.ArtifactRecord{ - {Kind: "transcript_normalized", LocalPath: manifestPath}, + {Kind: "transcript_final", LocalPath: manifestPath}, }) artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{ - Source: "narratio.transcript.full", + Source: "narratio.transcript.final", Required: true, } env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact @@ -994,17 +994,17 @@ func TestAnalyzeSupportsCanonicalNormalizedTranscriptSourceFromManifestOutput(t func TestAnalyzeSupportsNormalizedTranscriptSourceFromManifestOutput(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - fallbackPath := filepath.Join(paths.TranscriptsDir, "normalized.json") + fallbackPath := filepath.Join(paths.TranscriptsDir, "final.json") manifestPath := filepath.Join(paths.ArtifactsDir, "normalized.from-manifest.json") writeAnalyzeFile(t, fallbackPath, `{"segments":[{"id":999}]}`) writeAnalyzeFile(t, manifestPath, `{"segments":[{"id":10}]}`) m.MarkStageSucceeded("normalize", time.Now().UTC(), []manifest.ArtifactRecord{ - {Kind: "transcript_normalized", LocalPath: manifestPath}, + {Kind: "transcript_final", LocalPath: manifestPath}, }) artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{ - Source: "narratio.transcript.full", + Source: "narratio.transcript.final", Required: true, } env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact @@ -1025,7 +1025,7 @@ func TestAnalyzeFailsWhenNormalizedTranscriptMissing(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{ - Source: "narratio.transcript.full", + Source: "narratio.transcript.final", Required: true, } env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact @@ -1048,7 +1048,7 @@ func TestAnalyzeFailsWhenNormalizedTranscriptMissing(t *testing.T) { func TestAnalyzeFailsWhenProcessedTranscriptInvalidJSON(t *testing.T) { env, m, _ := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{not-json`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{not-json`) _, err := (analyzeStage{}).Run(context.Background(), env, m) if err == nil { @@ -1062,7 +1062,7 @@ func TestAnalyzeFailsWhenProcessedTranscriptInvalidJSON(t *testing.T) { func TestAnalyzeFailsWhenProcessedTranscriptMissingSegmentsArray(t *testing.T) { env, m, _ := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"not_segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"not_segments":[]}`) _, err := (analyzeStage{}).Run(context.Background(), env, m) if err == nil { @@ -1076,7 +1076,7 @@ func TestAnalyzeFailsWhenProcessedTranscriptMissingSegmentsArray(t *testing.T) { func TestAnalyzeRecordsRefsAndMetadata(t *testing.T) { env, m, _ := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) result, err := (analyzeStage{}).Run(context.Background(), env, m) if err != nil { @@ -1105,7 +1105,7 @@ func TestAnalyzeRecordsRefsAndMetadata(t *testing.T) { func TestAnalyzeHandlesAdapterError(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) fake.RunErr = errors.New("adapter boom") _, err := (analyzeStage{}).Run(context.Background(), env, m) @@ -1120,7 +1120,7 @@ func TestAnalyzeHandlesAdapterError(t *testing.T) { func TestAnalyzeHandlesValidationFailedResultAsError(t *testing.T) { env, m, fake := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) fake.RunResult = scriptorium.ArtifactResult{ ValidationFailed: true, ExitCode: 2, @@ -1139,7 +1139,7 @@ func TestAnalyzeHandlesValidationFailedResultAsError(t *testing.T) { func TestAnalyzeSkipsWhenNoEnabledScriptoriumArtifactsConfigured(t *testing.T) { env, m, _ := setupAnalyzeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`) + writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`) env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = config.ScriptoriumArtifactConfig{ Enabled: false, @@ -1199,7 +1199,7 @@ func setupAnalyzeEnv(t *testing.T) (*Env, *manifest.Manifest, *scriptorium.FakeR Timeout: "2m", Inputs: map[string]config.ScriptoriumInputConfig{ "transcript": { - Source: "narratio.transcript.trimmed", + Source: "narratio.transcript.final_trimmed", Required: true, }, "previous_recap": { diff --git a/internal/stage/archive_test.go b/internal/stage/archive_test.go index 612d89b..7937335 100644 --- a/internal/stage/archive_test.go +++ b/internal/stage/archive_test.go @@ -82,7 +82,7 @@ func TestArchiveUploadsRunRecordPromotionsAndCurrentPointer(t *testing.T) { "manifest.json", "polish/reports/audita.report.json", "transcribe/outputs/transcripts/raw/speaker.json", - "trim/outputs/transcripts/trimmed.json", + "trim/outputs/transcripts/final.trimmed.json", } for _, rel := range wantRunUploads { key := runPrefix + rel @@ -91,7 +91,7 @@ func TestArchiveUploadsRunRecordPromotionsAndCurrentPointer(t *testing.T) { } } - trimmedKey := sessionPrefix + "transcripts/trimmed.json" + trimmedKey := sessionPrefix + "transcripts/final.trimmed.json" recapKey := sessionPrefix + "artifacts/session_recap.md" if _, ok := fake.Objects[trimmedKey]; !ok { t.Fatalf("missing promoted key %q", trimmedKey) @@ -180,7 +180,7 @@ func TestArchiveToleratesMissingPreviousCache(t *testing.T) { func TestArchiveUsesCustomPromotionRules(t *testing.T) { env, m, _ := archiveFixture(t) env.Config.Pipeline.Archive.PromoteArtifacts = []config.ArchivePromotionRule{ - {Source: "narratio.transcript.trimmed", Dest: "published/trimmed.json", Required: boolPtr(true)}, + {Source: "narratio.transcript.final_trimmed", Dest: "published/trimmed.json", Required: boolPtr(true)}, {Source: "narratio.artifact.session_recap", Dest: "published/recap.md", Required: boolPtr(true)}, } @@ -201,8 +201,8 @@ func TestArchiveUsesCustomPromotionRules(t *testing.T) { func TestArchiveSkipsOptionalMissingPromotion(t *testing.T) { env, m, _ := archiveFixture(t) env.Config.Pipeline.Archive.PromoteArtifacts = []config.ArchivePromotionRule{ - {Source: "narratio.transcript.trimmed", Dest: "transcripts/trimmed.json", Required: boolPtr(true)}, - {Source: "narratio.transcript.merged", Dest: "transcripts/merged.json", Required: boolPtr(false)}, + {Source: "narratio.transcript.final_trimmed", Dest: "transcripts/final.trimmed.json", Required: boolPtr(true)}, + {Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(false)}, } result, err := archiveStage{}.Run(context.Background(), env, m) @@ -210,7 +210,7 @@ func TestArchiveSkipsOptionalMissingPromotion(t *testing.T) { t.Fatalf("Run() error = %v", err) } got, _ := result.Metadata["skipped_optional_promotions"].([]string) - want := []string{"transcripts/merged.json"} + want := []string{"transcripts/base.json"} if !reflect.DeepEqual(got, want) { t.Fatalf("skipped_optional_promotions = %#v, want %#v", got, want) } @@ -230,7 +230,7 @@ func TestArchiveSkipsRequiredUnselectedConfiguredPromotion(t *testing.T) { if err != nil { t.Fatalf("Run() error = %v", err) } - if _, ok := fake.Objects[m.S3SessionPrefix+"transcripts/trimmed.json"]; !ok { + if _, ok := fake.Objects[m.S3SessionPrefix+"transcripts/final.trimmed.json"]; !ok { t.Fatalf("missing built-in promoted trimmed key") } if _, ok := fake.Objects[m.S3SessionPrefix+"artifacts/session_recap.md"]; ok { @@ -321,7 +321,7 @@ func TestArchiveLockedUnselectedConfiguredPromotionSkipsAsUnselected(t *testing. func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) { env, m, _ := archiveFixture(t) env.Config.Pipeline.Archive.Locks = []config.ArchiveLockRule{ - {Source: "narratio.transcript.trimmed", Reason: "human reviewed"}, + {Source: "narratio.transcript.final_trimmed", Reason: "human reviewed"}, } fake := env.ObjectStore.(*storage.FakeBackend) @@ -330,7 +330,7 @@ func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) { t.Fatalf("Run() error = %v", err) } - trimmedKey := m.S3SessionPrefix + "transcripts/trimmed.json" + trimmedKey := m.S3SessionPrefix + "transcripts/final.trimmed.json" if _, ok := fake.Objects[trimmedKey]; ok { t.Fatalf("locked promotion key %q should not be uploaded", trimmedKey) } @@ -338,7 +338,7 @@ func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) { if _, ok := fake.Objects[recapKey]; !ok { t.Fatalf("unlocked promotion key %q should be uploaded", recapKey) } - runTrimmedKey := m.S3RunPrefix + "trim/outputs/transcripts/trimmed.json" + runTrimmedKey := m.S3RunPrefix + "trim/outputs/transcripts/final.trimmed.json" if _, ok := fake.Objects[runTrimmedKey]; !ok { t.Fatalf("run-local locked source output %q should still be uploaded", runTrimmedKey) } @@ -358,8 +358,8 @@ func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) { if len(locked) != 1 { t.Fatalf("locked_promotions = %#v, want one item", locked) } - if locked[0]["source"] != "narratio.transcript.trimmed" || - locked[0]["dest"] != "transcripts/trimmed.json" || + if locked[0]["source"] != "narratio.transcript.final_trimmed" || + locked[0]["dest"] != "transcripts/final.trimmed.json" || locked[0]["remote_key"] != trimmedKey || locked[0]["reason"] != "human reviewed" || locked[0]["required"] != true || @@ -388,10 +388,10 @@ func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) { func TestArchiveLockedRequiredMissingPromotionSucceeds(t *testing.T) { env, m, _ := archiveFixture(t) env.Config.Pipeline.Archive.PromoteArtifacts = []config.ArchivePromotionRule{ - {Source: "narratio.transcript.merged", Dest: "transcripts/merged.json", Required: boolPtr(true)}, + {Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(true)}, } env.Config.Pipeline.Archive.Locks = []config.ArchiveLockRule{ - {Source: "narratio.transcript.merged", Reason: "manual merge is locked"}, + {Source: "narratio.transcript.base", Reason: "manual merge is locked"}, } result, err := archiveStage{}.Run(context.Background(), env, m) @@ -400,7 +400,7 @@ func TestArchiveLockedRequiredMissingPromotionSucceeds(t *testing.T) { } fake := env.ObjectStore.(*storage.FakeBackend) - mergedKey := m.S3SessionPrefix + "transcripts/merged.json" + mergedKey := m.S3SessionPrefix + "transcripts/base.json" if _, ok := fake.Objects[mergedKey]; ok { t.Fatalf("locked missing promotion key %q should not be uploaded", mergedKey) } @@ -419,10 +419,10 @@ func TestArchiveLockedRequiredMissingPromotionSucceeds(t *testing.T) { func TestArchiveLockDoesNotOverwriteExistingPromotion(t *testing.T) { env, m, _ := archiveFixture(t) env.Config.Pipeline.Archive.Locks = []config.ArchiveLockRule{ - {Source: "narratio.transcript.trimmed", Reason: "already published"}, + {Source: "narratio.transcript.final_trimmed", Reason: "already published"}, } fake := env.ObjectStore.(*storage.FakeBackend) - trimmedKey := m.S3SessionPrefix + "transcripts/trimmed.json" + trimmedKey := m.S3SessionPrefix + "transcripts/final.trimmed.json" fake.SeedObject(storage.FakeObject{Key: trimmedKey, Data: []byte("previously published\n")}) if _, err := (archiveStage{}).Run(context.Background(), env, m); err != nil { @@ -443,7 +443,7 @@ func TestArchiveLockDoesNotOverwriteExistingPromotion(t *testing.T) { func TestArchiveFailsWhenRequiredPromotionMissing(t *testing.T) { env, m, _ := archiveFixture(t) env.Config.Pipeline.Archive.PromoteArtifacts = []config.ArchivePromotionRule{ - {Source: "narratio.transcript.merged", Dest: "transcripts/merged.json", Required: boolPtr(true)}, + {Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(true)}, } _, err := archiveStage{}.Run(context.Background(), env, m) @@ -470,7 +470,7 @@ func TestArchiveFailsWhenCanonicalRunRootMissing(t *testing.T) { func TestArchiveDoesNotWriteCurrentPointerWhenPromotionUploadFails(t *testing.T) { env, m, _ := archiveFixture(t) fake := env.ObjectStore.(*storage.FakeBackend) - trimmedKey := m.S3SessionPrefix + "transcripts/trimmed.json" + trimmedKey := m.S3SessionPrefix + "transcripts/final.trimmed.json" origUploadErr := fake.UploadErr fake.UploadErr = nil @@ -528,13 +528,13 @@ func archiveFixture(t *testing.T) (*Env, *manifest.Manifest, string) { sessionRoot := artifacts.SessionWorkDirForCampaign(root, campaign, sessionID) runRoot := artifacts.SessionRunRootForCampaign(root, campaign, sessionID, runID) - writeStageTestFile(t, filepath.Join(sessionRoot, "transcripts", "trimmed.json"), "{\"segments\":[]}\n") + writeStageTestFile(t, filepath.Join(sessionRoot, "transcripts", "final.trimmed.json"), "{\"segments\":[]}\n") writeStageTestFile(t, filepath.Join(sessionRoot, "artifacts", "session_recap.md"), "# recap\n") writeStageTestFile(t, filepath.Join(runRoot, "prepare", "inputs", "session.yml"), "session_id: 2026-04-19\n") writeStageTestFile(t, filepath.Join(runRoot, "prepare", "outputs", "audio", "speaker.flac"), "flac\n") writeStageTestFile(t, filepath.Join(runRoot, "transcribe", "outputs", "transcripts", "raw", "speaker.json"), "{}\n") - writeStageTestFile(t, filepath.Join(runRoot, "trim", "outputs", "transcripts", "trimmed.json"), "{\"segments\":[]}\n") + writeStageTestFile(t, filepath.Join(runRoot, "trim", "outputs", "transcripts", "final.trimmed.json"), "{\"segments\":[]}\n") writeStageTestFile(t, filepath.Join(runRoot, "analyze", "outputs", "artifacts", "session_recap.md"), "# recap\n") writeStageTestFile(t, filepath.Join(runRoot, "polish", "reports", "audita.report.json"), "{}\n") writeStageTestFile(t, filepath.Join(runRoot, "merge", "config", "seriatim.generated.yml"), "key: value\n") @@ -567,7 +567,7 @@ func archiveFixture(t *testing.T) (*Env, *manifest.Manifest, string) { Enabled: boolPtr(true), UploadRun: boolPtr(true), PromoteArtifacts: []config.ArchivePromotionRule{ - {Source: "narratio.transcript.trimmed", Dest: "transcripts/trimmed.json", Required: boolPtr(true)}, + {Source: "narratio.transcript.final_trimmed", Dest: "transcripts/final.trimmed.json", Required: boolPtr(true)}, {Source: "narratio.artifact.session_recap", Dest: "artifacts/session_recap.md", Required: boolPtr(true)}, }, }, diff --git a/internal/stage/merge.go b/internal/stage/merge.go index af599ea..5d096ee 100644 --- a/internal/stage/merge.go +++ b/internal/stage/merge.go @@ -26,7 +26,7 @@ func (mergeStage) Declares() IODecl { {Kind: "autocorrect", Category: "inputs", RelativePath: "inputs/autocorrect.yml"}, }, Outputs: []artifacts.Ref{ - {Kind: "transcript_merged", Category: "transcripts", RelativePath: "transcripts/merged.json"}, + {Kind: "transcript_base", Category: "transcripts", RelativePath: "transcripts/base.json"}, {Kind: "seriatim_report", Category: "artifacts", RelativePath: "artifacts/seriatim.report.json"}, }, } @@ -85,7 +85,7 @@ func (mergeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Sta return nil, fmt.Errorf("merge: %w", err) } - canonicalMergedPath := filepath.Join(paths.TranscriptsDir, "merged.json") + canonicalMergedPath := filepath.Join(paths.TranscriptsDir, "base.json") mergedPath, err := runLocalPathForCanonical(runLayout, paths, canonicalMergedPath) if err != nil { return nil, fmt.Errorf("merge: resolve run-local merged transcript path: %w", err) @@ -151,7 +151,7 @@ func (mergeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Sta } promotedMerged, err := promoteRunLocalOutput(env.ArtifactStore, finalMergedPath, canonicalMergedPath, artifacts.Ref{ - Kind: "transcript_merged", + Kind: "transcript_base", Category: "transcripts", SessionID: sessionID, }) diff --git a/internal/stage/merge_test.go b/internal/stage/merge_test.go index c1db7b3..5b4cd5b 100644 --- a/internal/stage/merge_test.go +++ b/internal/stage/merge_test.go @@ -86,8 +86,8 @@ func TestMergeStageMergesRawTranscriptsAndRecordsMetadata(t *testing.T) { if len(result.Outputs) != 2 { t.Fatalf("outputs len = %d, want 2", len(result.Outputs)) } - if result.Outputs[0].Kind != "transcript_merged" { - t.Fatalf("output[0] kind = %q, want transcript_merged", result.Outputs[0].Kind) + if result.Outputs[0].Kind != "transcript_base" { + t.Fatalf("output[0] kind = %q, want transcript_base", result.Outputs[0].Kind) } if result.Outputs[1].Kind != "seriatim_report" { t.Fatalf("output[1] kind = %q, want seriatim_report", result.Outputs[1].Kind) diff --git a/internal/stage/normalize.go b/internal/stage/normalize.go index d116ec9..3e4cb06 100644 --- a/internal/stage/normalize.go +++ b/internal/stage/normalize.go @@ -20,10 +20,10 @@ func (normalizeStage) Name() string { return "normalize" } func (normalizeStage) Declares() IODecl { return IODecl{ Inputs: []artifacts.Ref{ - {Kind: "transcript_processed", Category: "transcripts", RelativePath: "transcripts/processed.json"}, + {Kind: "transcript_polished", Category: "transcripts", RelativePath: "transcripts/polished.json"}, }, Outputs: []artifacts.Ref{ - {Kind: "transcript_normalized", Category: "transcripts", RelativePath: "transcripts/normalized.json"}, + {Kind: "transcript_final", Category: "transcripts", RelativePath: "transcripts/final.json"}, {Kind: "seriatim_normalize_report", Category: "artifacts", RelativePath: "artifacts/seriatim.normalize.report.json"}, }, } @@ -134,7 +134,7 @@ func (normalizeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) ( } promotedNormalized, err := promoteRunLocalOutput(env.ArtifactStore, finalNormalizedPath, canonicalNormalizedPath, artifacts.Ref{ - Kind: "transcript_normalized", + Kind: "transcript_final", Category: "transcripts", SessionID: sessionID, }) @@ -202,7 +202,7 @@ func normalizeConfigOrDefault(cfg *config.NormalizeConfig) *config.NormalizeConf } report := true return &config.NormalizeConfig{ - OutputPath: "transcripts/normalized.json", + OutputPath: "transcripts/final.json", OutputSchema: "seriatim-intermediate", Report: &report, } @@ -213,7 +213,7 @@ func discoverNormalizedTranscript(m *manifest.Manifest, paths artifacts.SessionP if m != nil && m.Stages != nil { if sr := m.Stages["normalize"]; sr != nil { for _, out := range sr.Outputs { - if out.Kind != "transcript_normalized" { + if out.Kind != "transcript_final" { continue } p := strings.TrimSpace(out.LocalPath) @@ -232,7 +232,7 @@ func discoverNormalizedTranscript(m *manifest.Manifest, paths artifacts.SessionP } } - fallback := filepath.Join(paths.TranscriptsDir, "normalized.json") + fallback := filepath.Join(paths.TranscriptsDir, "final.json") if info, err := os.Stat(fallback); err == nil && !info.IsDir() { return filepath.Clean(fallback), "fallback.transcripts_dir", nil } diff --git a/internal/stage/normalize_test.go b/internal/stage/normalize_test.go index 948ae34..5527bad 100644 --- a/internal/stage/normalize_test.go +++ b/internal/stage/normalize_test.go @@ -19,9 +19,9 @@ func TestNormalizeStageConsumesProcessedTranscriptFromManifest(t *testing.T) { paths := sessionPathsForEnv(env, m.SessionID) manifestProcessed := filepath.Join(paths.ArtifactsDir, "processed.from-manifest.json") writeFile(t, manifestProcessed, `{"segments":[{"id":10}]}`) - writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":99}]}`) + writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":99}]}`) m.MarkStageSucceeded("polish", time.Now().UTC(), []manifest.ArtifactRecord{{ - Kind: "transcript_processed", + Kind: "transcript_polished", LocalPath: manifestProcessed, }}) @@ -44,7 +44,7 @@ func TestNormalizeStageConsumesProcessedTranscriptFromManifest(t *testing.T) { func TestNormalizeStageFallsBackToProcessedTranscriptPath(t *testing.T) { env, m, ser := setupNormalizeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - fallback := filepath.Join(paths.TranscriptsDir, "processed.json") + fallback := filepath.Join(paths.TranscriptsDir, "polished.json") writeFile(t, fallback, `{"segments":[{"id":1}]}`) _, err := (normalizeStage{}).Run(context.Background(), env, m) @@ -73,7 +73,7 @@ func TestNormalizeStageFailsWhenProcessedTranscriptMissing(t *testing.T) { func TestNormalizeStageFailsWhenProcessedTranscriptInvalidJSON(t *testing.T) { env, m, _ := setupNormalizeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), "not-json") + writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), "not-json") _, err := (normalizeStage{}).Run(context.Background(), env, m) if err == nil { @@ -87,7 +87,7 @@ func TestNormalizeStageFailsWhenProcessedTranscriptInvalidJSON(t *testing.T) { func TestNormalizeStageFailsWhenProcessedTranscriptMissingSegments(t *testing.T) { env, m, _ := setupNormalizeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"schema":"audita.processed.v1"}`) + writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"schema":"audita.processed.v1"}`) _, err := (normalizeStage{}).Run(context.Background(), env, m) if err == nil { @@ -101,7 +101,7 @@ func TestNormalizeStageFailsWhenProcessedTranscriptMissingSegments(t *testing.T) func TestNormalizeStagePassesConfiguredOutputSchemaToAdapter(t *testing.T) { env, m, ser := setupNormalizeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":1}]}`) + writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":1}]}`) env.Config.Pipeline.Normalize.OutputSchema = "seriatim-full" result, err := (normalizeStage{}).Run(context.Background(), env, m) @@ -122,24 +122,24 @@ func TestNormalizeStagePassesConfiguredOutputSchemaToAdapter(t *testing.T) { func TestNormalizeStageRecordsNormalizedTranscriptOutputKind(t *testing.T) { env, m, _ := setupNormalizeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":1}]}`) + writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":1}]}`) result, err := (normalizeStage{}).Run(context.Background(), env, m) if err != nil { t.Fatalf("normalize.Run() error = %v", err) } if len(result.Outputs) == 0 { - t.Fatalf("outputs = %#v, want at least transcript_normalized", result.Outputs) + t.Fatalf("outputs = %#v, want at least transcript_final", result.Outputs) } - if result.Outputs[0].Kind != "transcript_normalized" { - t.Fatalf("outputs[0].kind = %q, want transcript_normalized", result.Outputs[0].Kind) + if result.Outputs[0].Kind != "transcript_final" { + t.Fatalf("outputs[0].kind = %q, want transcript_final", result.Outputs[0].Kind) } } func TestNormalizeStageRecordsReportLogAndGeneratedConfigRefs(t *testing.T) { env, m, _ := setupNormalizeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":1}]}`) + writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":1}]}`) report := true env.Config.Pipeline.Normalize.Report = &report @@ -148,7 +148,7 @@ func TestNormalizeStageRecordsReportLogAndGeneratedConfigRefs(t *testing.T) { t.Fatalf("normalize.Run() error = %v", err) } if len(result.Outputs) < 2 { - t.Fatalf("outputs = %#v, want transcript_normalized + report", result.Outputs) + t.Fatalf("outputs = %#v, want transcript_final + report", result.Outputs) } if result.Outputs[1].Kind != "seriatim_normalize_report" { t.Fatalf("outputs[1].kind = %q, want seriatim_normalize_report", result.Outputs[1].Kind) @@ -165,7 +165,7 @@ func TestNormalizeStageRecordsReportLogAndGeneratedConfigRefs(t *testing.T) { func TestNormalizeStageFailsWhenAdapterReturnsError(t *testing.T) { env, m, ser := setupNormalizeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":1}]}`) + writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":1}]}`) ser.NormalizeErr = errors.New("normalize failed") _, err := (normalizeStage{}).Run(context.Background(), env, m) @@ -180,7 +180,7 @@ func TestNormalizeStageFailsWhenAdapterReturnsError(t *testing.T) { func TestNormalizeStageFailsWhenNormalizedOutputInvalid(t *testing.T) { env, m, ser := setupNormalizeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":1}]}`) + writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":1}]}`) badOutput := filepath.Join(paths.TranscriptsDir, "normalized.bad.json") writeFile(t, badOutput, "not-json") ser.NormalizeResult = seriatim.NormalizeResult{OutputNormalizedPath: badOutput} @@ -197,7 +197,7 @@ func TestNormalizeStageFailsWhenNormalizedOutputInvalid(t *testing.T) { func TestNormalizeStageReportEnabledFailsWhenReportMissing(t *testing.T) { env, m, ser := setupNormalizeEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":1}]}`) + writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":1}]}`) report := true env.Config.Pipeline.Normalize.Report = &report ser.NormalizeResult = seriatim.NormalizeResult{ReportPath: filepath.Join(paths.ArtifactsDir, "missing-report.json")} @@ -217,7 +217,7 @@ func TestNormalizeStageUsesRunLocalPathsAndPromotesCanonical(t *testing.T) { m.Campaign = "sample-campaign" m.RunID = "20260518T010203Z-abcdef12" paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":1}]}`) + writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":1}]}`) result, err := (normalizeStage{}).Run(context.Background(), env, m) if err != nil { @@ -261,7 +261,7 @@ func setupNormalizeEnv(t *testing.T) (*Env, *manifest.Manifest, *seriatim.FakeRu Report: &report, }, Normalize: &config.NormalizeConfig{ - OutputPath: "transcripts/normalized.json", + OutputPath: "transcripts/final.json", OutputSchema: "seriatim-intermediate", Report: &report, }, diff --git a/internal/stage/placeholders_test.go b/internal/stage/placeholders_test.go index c9c7be6..240e650 100644 --- a/internal/stage/placeholders_test.go +++ b/internal/stage/placeholders_test.go @@ -163,8 +163,8 @@ func TestStagesReturnExpectedMetadata(t *testing.T) { if result.Metadata["output_schema"] == nil { t.Fatalf("normalize metadata = %#v, want output_schema metadata", result.Metadata) } - if len(result.Outputs) == 0 || result.Outputs[0].Kind != "transcript_normalized" { - t.Fatalf("normalize outputs = %#v, want transcript_normalized output", result.Outputs) + if len(result.Outputs) == 0 || result.Outputs[0].Kind != "transcript_final" { + t.Fatalf("normalize outputs = %#v, want transcript_final output", result.Outputs) } if len(result.Logs) != 2 { t.Fatalf("normalize logs = %#v, want stdout+stderr", result.Logs) @@ -190,8 +190,8 @@ func TestStagesReturnExpectedMetadata(t *testing.T) { if result.Metadata["trim_action"] != "copy_disabled" { t.Fatalf("trim metadata = %#v, want trim_action=copy_disabled", result.Metadata) } - if len(result.Outputs) == 0 || result.Outputs[0].Kind != "transcript_trimmed" { - t.Fatalf("trim outputs = %#v, want transcript_trimmed output", result.Outputs) + if len(result.Outputs) == 0 || result.Outputs[0].Kind != "transcript_final_trimmed" { + t.Fatalf("trim outputs = %#v, want transcript_final_trimmed output", result.Outputs) } continue } diff --git a/internal/stage/polish.go b/internal/stage/polish.go index eeedf05..8af1145 100644 --- a/internal/stage/polish.go +++ b/internal/stage/polish.go @@ -20,11 +20,11 @@ func (polishStage) Name() string { return "polish" } func (polishStage) Declares() IODecl { return IODecl{ Inputs: []artifacts.Ref{ - {Kind: "transcript_merged", Category: "transcripts", RelativePath: "transcripts/merged.json"}, + {Kind: "transcript_base", Category: "transcripts", RelativePath: "transcripts/base.json"}, {Kind: "glossary", Category: "inputs", RelativePath: "inputs/glossary.yml"}, }, Outputs: []artifacts.Ref{ - {Kind: "transcript_processed", Category: "transcripts", RelativePath: "transcripts/processed.json"}, + {Kind: "transcript_polished", Category: "transcripts", RelativePath: "transcripts/polished.json"}, {Kind: "audita_report", Category: "artifacts", RelativePath: "artifacts/audita.report.json"}, }, } @@ -76,7 +76,7 @@ func (polishStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*St return nil, fmt.Errorf("polish: %w", err) } - canonicalProcessedPath := filepath.Join(paths.TranscriptsDir, "processed.json") + canonicalProcessedPath := filepath.Join(paths.TranscriptsDir, "polished.json") processedPath, err := runLocalPathForCanonical(runLayout, paths, canonicalProcessedPath) if err != nil { return nil, fmt.Errorf("polish: resolve run-local processed transcript path: %w", err) @@ -150,7 +150,7 @@ func (polishStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*St } promotedProcessed, err := promoteRunLocalOutput(env.ArtifactStore, finalProcessedPath, canonicalProcessedPath, artifacts.Ref{ - Kind: "transcript_processed", + Kind: "transcript_polished", Category: "transcripts", SessionID: sessionID, }) @@ -250,7 +250,7 @@ func discoverMergedTranscript(m *manifest.Manifest, paths artifacts.SessionPaths if m != nil && m.Stages != nil { if sr := m.Stages["merge"]; sr != nil { for _, out := range sr.Outputs { - if out.Kind != "transcript_merged" { + if out.Kind != "transcript_base" { continue } p := strings.TrimSpace(out.LocalPath) @@ -270,7 +270,7 @@ func discoverMergedTranscript(m *manifest.Manifest, paths artifacts.SessionPaths } } - fallback := filepath.Join(paths.TranscriptsDir, "merged.json") + fallback := filepath.Join(paths.TranscriptsDir, "base.json") if info, err := os.Stat(fallback); err == nil && !info.IsDir() { return filepath.Clean(fallback), "fallback.transcripts_dir", nil } diff --git a/internal/stage/polish_test.go b/internal/stage/polish_test.go index 7ef63f4..12742f1 100644 --- a/internal/stage/polish_test.go +++ b/internal/stage/polish_test.go @@ -18,11 +18,11 @@ func TestPolishStagePolishesMergedTranscriptAndRecordsMetadata(t *testing.T) { env, m := setupPolishEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - mergedPath := filepath.Join(paths.TranscriptsDir, "merged.json") + mergedPath := filepath.Join(paths.TranscriptsDir, "base.json") writeFile(t, mergedPath, `{"segments":[]}`) writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n") m.MarkStageSucceeded("merge", time.Now().UTC(), []manifest.ArtifactRecord{ - {Kind: "transcript_merged", LocalPath: mergedPath}, + {Kind: "transcript_base", LocalPath: mergedPath}, }) fake := &audita.FakeRunner{} @@ -85,8 +85,8 @@ func TestPolishStagePolishesMergedTranscriptAndRecordsMetadata(t *testing.T) { if len(result.Outputs) != 2 { t.Fatalf("outputs len = %d, want 2", len(result.Outputs)) } - if result.Outputs[0].Kind != "transcript_processed" { - t.Fatalf("output[0] kind = %q, want transcript_processed", result.Outputs[0].Kind) + if result.Outputs[0].Kind != "transcript_polished" { + t.Fatalf("output[0] kind = %q, want transcript_polished", result.Outputs[0].Kind) } if result.Outputs[1].Kind != "audita_report" { t.Fatalf("output[1] kind = %q, want audita_report", result.Outputs[1].Kind) @@ -121,7 +121,7 @@ func TestPolishStagePolishesMergedTranscriptAndRecordsMetadata(t *testing.T) { func TestPolishStageFallsBackToMergedTranscriptPath(t *testing.T) { env, m := setupPolishEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - mergedPath := filepath.Join(paths.TranscriptsDir, "merged.json") + mergedPath := filepath.Join(paths.TranscriptsDir, "base.json") writeFile(t, mergedPath, `{"segments":[]}`) writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n") @@ -158,7 +158,7 @@ func TestPolishStageFailsWhenMergedTranscriptMissing(t *testing.T) { func TestPolishStageFailsWhenMergedTranscriptInvalidJSON(t *testing.T) { env, m := setupPolishEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "merged.json"), "not-json") + writeFile(t, filepath.Join(paths.TranscriptsDir, "base.json"), "not-json") writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n") env.Audita = &audita.FakeRunner{} @@ -174,7 +174,7 @@ func TestPolishStageFailsWhenMergedTranscriptInvalidJSON(t *testing.T) { func TestPolishStageFailsWhenGlossaryMissing(t *testing.T) { env, m := setupPolishEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "merged.json"), `{"segments":[]}`) + writeFile(t, filepath.Join(paths.TranscriptsDir, "base.json"), `{"segments":[]}`) env.Audita = &audita.FakeRunner{} _, err := (polishStage{}).Run(context.Background(), env, m) @@ -189,7 +189,7 @@ func TestPolishStageFailsWhenGlossaryMissing(t *testing.T) { func TestPolishStageFailsWhenAdapterFails(t *testing.T) { env, m := setupPolishEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "merged.json"), `{"segments":[]}`) + writeFile(t, filepath.Join(paths.TranscriptsDir, "base.json"), `{"segments":[]}`) writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n") env.Audita = &audita.FakeRunner{Err: errors.New("audita failed")} @@ -205,7 +205,7 @@ func TestPolishStageFailsWhenAdapterFails(t *testing.T) { func TestPolishStageFailsWhenProcessedOutputInvalid(t *testing.T) { env, m := setupPolishEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "merged.json"), `{"segments":[]}`) + writeFile(t, filepath.Join(paths.TranscriptsDir, "base.json"), `{"segments":[]}`) writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n") badPath := filepath.Join(paths.TranscriptsDir, "processed.invalid.json") writeFile(t, badPath, `{"schema":"audita.processed.v1","segments":"wrong-type"}`) @@ -223,7 +223,7 @@ func TestPolishStageFailsWhenProcessedOutputInvalid(t *testing.T) { func TestPolishStageFailsWhenReportInvalid(t *testing.T) { env, m := setupPolishEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "merged.json"), `{"segments":[]}`) + writeFile(t, filepath.Join(paths.TranscriptsDir, "base.json"), `{"segments":[]}`) writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n") badReport := filepath.Join(paths.ArtifactsDir, "bad.report.json") writeFile(t, badReport, "not-json") @@ -244,7 +244,7 @@ func TestPolishStageUsesRunLocalPathsAndPromotesCanonical(t *testing.T) { m.Campaign = "sample-campaign" m.RunID = "20260518T010203Z-abcdef12" paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "merged.json"), `{"segments":[]}`) + writeFile(t, filepath.Join(paths.TranscriptsDir, "base.json"), `{"segments":[]}`) writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n") fake := &audita.FakeRunner{} diff --git a/internal/stage/run_local_test.go b/internal/stage/run_local_test.go index 3bbc38b..357e223 100644 --- a/internal/stage/run_local_test.go +++ b/internal/stage/run_local_test.go @@ -19,13 +19,13 @@ func TestRunLocalPathForCanonicalCreatesParentDirectories(t *testing.T) { t.Fatalf("mkdir outputs dir: %v", err) } - canonical := filepath.Join(sessionRoot, "transcripts", "merged.json") + canonical := filepath.Join(sessionRoot, "transcripts", "base.json") got, err := runLocalPathForCanonical(layout, artifacts.SessionPaths{Root: sessionRoot}, canonical) if err != nil { t.Fatalf("runLocalPathForCanonical() error = %v", err) } - want := filepath.Join(layout.OutputsDir, "transcripts", "merged.json") + want := filepath.Join(layout.OutputsDir, "transcripts", "base.json") if got != want { t.Fatalf("runLocalPathForCanonical() = %q, want %q", got, want) } diff --git a/internal/stage/trim.go b/internal/stage/trim.go index bb1ea72..c4cee83 100644 --- a/internal/stage/trim.go +++ b/internal/stage/trim.go @@ -23,10 +23,10 @@ func (trimStage) Name() string { return "trim" } func (trimStage) Declares() IODecl { return IODecl{ Inputs: []artifacts.Ref{ - {Kind: "transcript_normalized", Category: "transcripts", RelativePath: "transcripts/normalized.json"}, + {Kind: "transcript_final", Category: "transcripts", RelativePath: "transcripts/final.json"}, }, Outputs: []artifacts.Ref{ - {Kind: "transcript_trimmed", Category: "transcripts", RelativePath: "transcripts/trimmed.json"}, + {Kind: "transcript_final_trimmed", Category: "transcripts", RelativePath: "transcripts/final.trimmed.json"}, {Kind: "session_bounds", Category: "artifacts", RelativePath: "artifacts/session_bounds.json"}, }, } @@ -101,7 +101,7 @@ func (trimStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Stag return nil, fmt.Errorf("trim: copied trimmed transcript %q invalid: %w", trimmedPath, err) } promotedTrimmed, err := promoteRunLocalOutput(env.ArtifactStore, trimmedPath, canonicalTrimmedPath, artifacts.Ref{ - Kind: "transcript_trimmed", + Kind: "transcript_final_trimmed", Category: "transcripts", SessionID: sessionID, }) @@ -352,7 +352,7 @@ func (trimStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Stag } promotedTrimmed, err := promoteRunLocalOutput(env.ArtifactStore, trimmedPath, canonicalTrimmedPath, artifacts.Ref{ - Kind: "transcript_trimmed", + Kind: "transcript_final_trimmed", Category: "transcripts", SessionID: sessionID, }) @@ -382,7 +382,7 @@ func resolveTrimmedOutputPath(paths artifacts.SessionPaths, cfg *config.TrimConf configured = strings.TrimSpace(cfg.OutputPath) } if configured == "" { - return filepath.Join(paths.TranscriptsDir, "trimmed.json"), nil + return filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), nil } return resolveScriptoriumOutputPath(paths, configured) } diff --git a/internal/stage/trim_test.go b/internal/stage/trim_test.go index af7084d..4b19dde 100644 --- a/internal/stage/trim_test.go +++ b/internal/stage/trim_test.go @@ -19,10 +19,10 @@ import ( func TestTrimStageConsumesNormalizedAndProducesTrimmedTranscript(t *testing.T) { env, m, scr, ser := setupTrimEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - normalized := filepath.Join(paths.TranscriptsDir, "normalized.json") + normalized := filepath.Join(paths.TranscriptsDir, "final.json") writeFile(t, normalized, `{"segments":[{"id":10},{"id":868}]}`) m.MarkStageSucceeded("normalize", time.Now().UTC(), []manifest.ArtifactRecord{ - {Kind: "transcript_normalized", LocalPath: normalized}, + {Kind: "transcript_final", LocalPath: normalized}, }) scr.BoundsBody = `{"confidence":"high","trim_action":"trim","start_segment_id":10,"end_segment_id":868,"warnings":[]}` @@ -44,7 +44,7 @@ func TestTrimStageConsumesNormalizedAndProducesTrimmedTranscript(t *testing.T) { if trimReq.KeepSelector != "10-868" { t.Fatalf("keep selector = %q, want %q", trimReq.KeepSelector, "10-868") } - if trimReq.OutputTrimmedPath != filepath.Join(paths.TranscriptsDir, "trimmed.json") { + if trimReq.OutputTrimmedPath != filepath.Join(paths.TranscriptsDir, "final.trimmed.json") { t.Fatalf("trim output = %q", trimReq.OutputTrimmedPath) } @@ -54,13 +54,13 @@ func TestTrimStageConsumesNormalizedAndProducesTrimmedTranscript(t *testing.T) { if len(result.Outputs) < 2 { t.Fatalf("outputs = %#v, want trimmed+bounds outputs", result.Outputs) } - if result.Outputs[0].Kind != "transcript_trimmed" { - t.Fatalf("output[0] kind = %q, want transcript_trimmed", result.Outputs[0].Kind) + if result.Outputs[0].Kind != "transcript_final_trimmed" { + t.Fatalf("output[0] kind = %q, want transcript_final_trimmed", result.Outputs[0].Kind) } if result.Outputs[1].Kind != "session_bounds" { t.Fatalf("output[1] kind = %q, want session_bounds", result.Outputs[1].Kind) } - if result.Metadata["trimmed_output_path"] != filepath.Join(paths.TranscriptsDir, "trimmed.json") { + if result.Metadata["trimmed_output_path"] != filepath.Join(paths.TranscriptsDir, "final.trimmed.json") { t.Fatalf("metadata trimmed_output_path = %#v", result.Metadata["trimmed_output_path"]) } if result.Metadata["keep_selector"] != "10-868" { @@ -71,7 +71,7 @@ func TestTrimStageConsumesNormalizedAndProducesTrimmedTranscript(t *testing.T) { func TestTrimStageUsesConfiguredScriptoriumInputName(t *testing.T) { env, m, scr, _ := setupTrimEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - normalized := filepath.Join(paths.TranscriptsDir, "normalized.json") + normalized := filepath.Join(paths.TranscriptsDir, "final.json") writeFile(t, normalized, `{"segments":[{"id":10},{"id":11}]}`) scr.BoundsBody = `{"trim_action":"trim","start_segment_id":10,"end_segment_id":11}` @@ -95,7 +95,7 @@ func TestTrimStageUsesConfiguredScriptoriumInputName(t *testing.T) { func TestTrimStageRecordsLogAndGeneratedConfigRefs(t *testing.T) { env, m, scr, _ := setupTrimEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), `{"segments":[{"id":1},{"id":2}]}`) + writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":1},{"id":2}]}`) scr.BoundsBody = `{"trim_action":"trim","start_segment_id":1,"end_segment_id":2}` result, err := (trimStage{}).Run(context.Background(), env, m) @@ -115,7 +115,7 @@ func TestTrimStageRecordsLogAndGeneratedConfigRefs(t *testing.T) { func TestTrimStageRenderDebugDiagnosticsAreNotStageOutputs(t *testing.T) { env, m, scr, _ := setupTrimEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), `{"segments":[{"id":1},{"id":2}]}`) + writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":1},{"id":2}]}`) scr.BoundsBody = `{"trim_action":"trim","start_segment_id":1,"end_segment_id":2}` cfg := *env.Config.Pipeline.Trim @@ -153,7 +153,7 @@ func TestTrimStageFailsWhenNormalizedTranscriptMissing(t *testing.T) { func TestTrimStageDoesNotFallBackToProcessedTranscript(t *testing.T) { env, m, scr, ser := setupTrimEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":1},{"id":2}]}`) + writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":1},{"id":2}]}`) _, err := (trimStage{}).Run(context.Background(), env, m) if err == nil { @@ -173,7 +173,7 @@ func TestTrimStageDoesNotFallBackToProcessedTranscript(t *testing.T) { func TestTrimStageFailsWhenNormalizedTranscriptInvalidJSON(t *testing.T) { env, m, _, _ := setupTrimEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), "not-json") + writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), "not-json") _, err := (trimStage{}).Run(context.Background(), env, m) if err == nil { t.Fatal("expected error, got nil") @@ -186,7 +186,7 @@ func TestTrimStageFailsWhenNormalizedTranscriptInvalidJSON(t *testing.T) { func TestTrimStageFailsWhenNormalizedTranscriptMissingSegmentsArray(t *testing.T) { env, m, _, _ := setupTrimEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), `{"schema":"audita.processed.v1"}`) + writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"schema":"audita.processed.v1"}`) _, err := (trimStage{}).Run(context.Background(), env, m) if err == nil { t.Fatal("expected error, got nil") @@ -199,7 +199,7 @@ func TestTrimStageFailsWhenNormalizedTranscriptMissingSegmentsArray(t *testing.T func TestTrimStageFailsWhenBoundsOutputInvalidJSON(t *testing.T) { env, m, scr, _ := setupTrimEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), `{"segments":[{"id":1},{"id":2}]}`) + writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":1},{"id":2}]}`) scr.BoundsBody = "not-json" _, err := (trimStage{}).Run(context.Background(), env, m) if err == nil { @@ -213,7 +213,7 @@ func TestTrimStageFailsWhenBoundsOutputInvalidJSON(t *testing.T) { func TestTrimStageFailsWhenBoundsRangeIsDescending(t *testing.T) { env, m, scr, _ := setupTrimEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), `{"segments":[{"id":1},{"id":2}]}`) + writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":1},{"id":2}]}`) scr.BoundsBody = `{"trim_action":"trim","start_segment_id":2,"end_segment_id":1}` _, err := (trimStage{}).Run(context.Background(), env, m) if err == nil { @@ -227,7 +227,7 @@ func TestTrimStageFailsWhenBoundsRangeIsDescending(t *testing.T) { func TestTrimStageFailsWhenBoundsIDsMissingFromTranscript(t *testing.T) { env, m, scr, _ := setupTrimEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), `{"segments":[{"id":20},{"id":21}]}`) + writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":20},{"id":21}]}`) scr.BoundsBody = `{"trim_action":"trim","start_segment_id":10,"end_segment_id":21}` _, err := (trimStage{}).Run(context.Background(), env, m) if err == nil { @@ -241,7 +241,7 @@ func TestTrimStageFailsWhenBoundsIDsMissingFromTranscript(t *testing.T) { func TestTrimStageFailsWhenScriptoriumAdapterFails(t *testing.T) { env, m, scr, _ := setupTrimEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), `{"segments":[{"id":1},{"id":2}]}`) + writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":1},{"id":2}]}`) scr.RunErr = errors.New("bounds failed") _, err := (trimStage{}).Run(context.Background(), env, m) if err == nil { @@ -255,7 +255,7 @@ func TestTrimStageFailsWhenScriptoriumAdapterFails(t *testing.T) { func TestTrimStageFailsWhenSeriatimTrimAdapterFails(t *testing.T) { env, m, scr, ser := setupTrimEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), `{"segments":[{"id":1},{"id":2}]}`) + writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":1},{"id":2}]}`) scr.BoundsBody = `{"trim_action":"trim","start_segment_id":1,"end_segment_id":2}` ser.TrimErr = errors.New("trim failed") _, err := (trimStage{}).Run(context.Background(), env, m) @@ -270,7 +270,7 @@ func TestTrimStageFailsWhenSeriatimTrimAdapterFails(t *testing.T) { func TestTrimStageDisabledCopiesNormalizedTranscript(t *testing.T) { env, m, scr, ser := setupTrimEnv(t) paths := sessionPathsForEnv(env, m.SessionID) - normalized := filepath.Join(paths.TranscriptsDir, "normalized.json") + normalized := filepath.Join(paths.TranscriptsDir, "final.json") normalizedBody := `{"segments":[{"id":1,"text":"alpha"},{"id":2,"text":"beta"}]}` writeFile(t, normalized, normalizedBody) @@ -288,7 +288,7 @@ func TestTrimStageDisabledCopiesNormalizedTranscript(t *testing.T) { if result.Metadata["trim_action"] != "copy_disabled" { t.Fatalf("metadata trim_action = %#v, want copy_disabled", result.Metadata["trim_action"]) } - trimmedPath := filepath.Join(paths.TranscriptsDir, "trimmed.json") + trimmedPath := filepath.Join(paths.TranscriptsDir, "final.trimmed.json") data, err := os.ReadFile(trimmedPath) if err != nil { t.Fatalf("read trimmed transcript: %v", err) @@ -304,7 +304,7 @@ func TestTrimStageUsesRunLocalPathsAndPromotesCanonical(t *testing.T) { m.Campaign = "sample-campaign" m.RunID = "20260518T010203Z-abcdef12" paths := sessionPathsForEnv(env, m.SessionID) - normalized := filepath.Join(paths.TranscriptsDir, "normalized.json") + normalized := filepath.Join(paths.TranscriptsDir, "final.json") writeFile(t, normalized, `{"segments":[{"id":10},{"id":11}]}`) scr.BoundsBody = `{"trim_action":"trim","start_segment_id":10,"end_segment_id":11,"warnings":[]}` @@ -438,7 +438,7 @@ func setupTrimEnv(t *testing.T) (*Env, *manifest.Manifest, *boundsScriptoriumRun }, Trim: &config.TrimConfig{ Enabled: true, - OutputPath: "transcripts/trimmed.json", + OutputPath: "transcripts/final.trimmed.json", Bounds: config.TrimBoundsConfig{ PromptID: "dnd_session.bounds", ProfileID: "",