Updated transcript artifact names and canonical paths to use a consistent, role-based nomenclature

This commit is contained in:
2026-05-22 19:05:23 -05:00
parent e920f3a8d5
commit cee52aa092
56 changed files with 991 additions and 463 deletions

View File

@@ -361,8 +361,8 @@ Examples:
```bash ```bash
narratio locks --session-id 2026-04-04 narratio locks --session-id 2026-04-04
narratio locks add --session-id 2026-04-04 --reason "manual transcript review" 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.trimmed narratio locks remove --session-id 2026-04-04 narratio.transcript.final_trimmed
``` ```
### `run-stage` ### `run-stage`

View File

@@ -185,8 +185,8 @@ archive:
enabled: true enabled: true
upload_run: true upload_run: true
promote_artifacts: promote_artifacts:
- source: narratio.transcript.trimmed - source: narratio.transcript.final_trimmed
dest: transcripts/trimmed.json dest: transcripts/final.trimmed.json
required: true required: true
- source: narratio.artifact.session_recap - source: narratio.artifact.session_recap
dest: artifacts/session_recap.md dest: artifacts/session_recap.md
@@ -206,7 +206,7 @@ scriptorium:
output_path: artifacts/session_recap.md output_path: artifacts/session_recap.md
inputs: inputs:
transcript: transcript:
source: narratio.transcript.trimmed source: narratio.transcript.final_trimmed
required: true required: true
previous_recap: previous_recap:
source: narratio.previous_session.artifact.session_recap source: narratio.previous_session.artifact.session_recap
@@ -280,7 +280,7 @@ Operational notes:
| `pipeline.cache.s3_audio` | bool | No | `true` | | `pipeline.cache.s3_audio` | bool | No | `true` |
| `pipeline.archive.enabled` | bool | No | `true` | | `pipeline.archive.enabled` | bool | No | `true` |
| `pipeline.archive.upload_run` | 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[].source` | string | Yes (per rule) | none |
| `pipeline.archive.promote_artifacts[].dest` | string | No | derived from source | | `pipeline.archive.promote_artifacts[].dest` | string | No | derived from source |
| `pipeline.archive.promote_artifacts[].required` | bool | No | `true` | | `pipeline.archive.promote_artifacts[].required` | bool | No | `true` |
@@ -317,7 +317,7 @@ Operational notes:
| `pipeline.audita.output_schema` | string | No | empty | | `pipeline.audita.output_schema` | string | No | empty |
| `pipeline.audita.work_dir_retention` | string | No | empty | | `pipeline.audita.work_dir_retention` | string | No | empty |
| `pipeline.audita.report` | bool | No | `true` | | `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.output_schema` | string | No | `seriatim-intermediate` |
| `pipeline.normalize.report` | bool | No | `true` | | `pipeline.normalize.report` | bool | No | `true` |
| `pipeline.trim.enabled` | bool | No | `false` | | `pipeline.trim.enabled` | bool | No | `false` |
@@ -366,19 +366,19 @@ Scriptorium artifact-key and dependency rules:
Allowed `pipeline.scriptorium.artifacts.<name>.inputs.<key>.source` values: Allowed `pipeline.scriptorium.artifacts.<name>.inputs.<key>.source` values:
- `narratio.previous_session.artifact.<configured_artifact_key>` - `narratio.previous_session.artifact.<configured_artifact_key>`
- `narratio.transcript.merged` - `narratio.transcript.base`
- `narratio.transcript.polished` - `narratio.transcript.polished`
- `narratio.transcript.full` - `narratio.transcript.final`
- `narratio.transcript.trimmed` - `narratio.transcript.final_trimmed`
- `narratio.bounds.session` - `narratio.bounds.session`
- `narratio.artifact.<configured_artifact_key>` - `narratio.artifact.<configured_artifact_key>`
`pipeline.archive.promote_artifacts[].source` values: `pipeline.archive.promote_artifacts[].source` values:
- `narratio.transcript.merged` - `narratio.transcript.base`
- `narratio.transcript.polished` - `narratio.transcript.polished`
- `narratio.transcript.full` - `narratio.transcript.final`
- `narratio.transcript.trimmed` - `narratio.transcript.final_trimmed`
- `narratio.bounds.session` - `narratio.bounds.session`
- `narratio.artifact.<configured_artifact_key>` - `narratio.artifact.<configured_artifact_key>`

View File

@@ -5,9 +5,9 @@ Define Narratio's adapter contract for transcript polishing via Audita CLI subpr
## Inputs and Outputs ## Inputs and Outputs
Inputs (`audita.PolishRequest`): Inputs (`audita.PolishRequest`):
- merged transcript path - base transcript path
- glossary path - glossary path
- output processed transcript path - output polished transcript path
- optional report path (required when report enabled) - optional report path (required when report enabled)
- work dir - work dir
- generated config path - generated config path
@@ -15,7 +15,7 @@ Inputs (`audita.PolishRequest`):
- optional module/model/base URL and concurrency knobs - optional module/model/base URL and concurrency knobs
Outputs (`audita.PolishResult`): Outputs (`audita.PolishResult`):
- processed transcript path - polished transcript path
- optional report path - optional report path
- generated config path - generated config path
- stdout/stderr log paths - stdout/stderr log paths
@@ -27,7 +27,7 @@ Owns:
- Deterministic CLI argument construction for `audita process` - Deterministic CLI argument construction for `audita process`
- Environment bridging for API credentials - Environment bridging for API credentials
- Invocation config emission - Invocation config emission
- Output validation for processed transcript and report - Output validation for polished transcript and report
Does not own: Does not own:
- Upstream/downstream stage orchestration - Upstream/downstream stage orchestration
@@ -52,7 +52,7 @@ Via `pipeline.audita.*` mapped in app/stage wiring:
## Failure Behavior ## Failure Behavior
- Constructor validation fails on invalid binary/timeout/schema/concurrency/URL values. - 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. - Failures preserve stdout/stderr paths in returned result metadata.
## Tests to Inspect Before Changing ## Tests to Inspect Before Changing
@@ -61,6 +61,6 @@ Via `pipeline.audita.*` mapped in app/stage wiring:
- `internal/stage/polish_test.go` - `internal/stage/polish_test.go`
## Architectural Invariants ## 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. - When report is enabled, report output must be valid JSON.
- If `llm_api_key_env` is configured, credential must be present in environment. - If `llm_api_key_env` is configured, credential must be present in environment.

View File

@@ -5,7 +5,7 @@ Define Narratio's adapter contract for merge, normalize, and trim subprocess inv
## Inputs and Outputs ## Inputs and Outputs
Inputs: 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 - `NormalizeRequest`: input transcript, output path, schema, optional report, timeout/log/config
- `TrimRequest`: input transcript, output path, keep selector, 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 ## Failure Behavior
- Constructor fails for invalid binary/timeout/output-schema/coalesce-gap. - 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. - 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. - 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 trimmed output JSON shape. - Trim fails on missing input/output/keep selector, subprocess errors, invalid final-trimmed output JSON shape.
## Tests to Inspect Before Changing ## Tests to Inspect Before Changing
- `internal/adapters/seriatim/subprocess_test.go` - `internal/adapters/seriatim/subprocess_test.go`
@@ -56,5 +56,5 @@ Via `pipeline.seriatim.*` mapped in app/stage wiring:
## Architectural Invariants ## Architectural Invariants
- Supported output schemas are limited to `seriatim-minimal`, `seriatim-intermediate`, `seriatim-full`. - 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. - Merge/normalize/trim all route through deterministic subprocess invocation.

View File

@@ -33,10 +33,10 @@ Does not own:
## Built-in IDs ## Built-in IDs
| Artifact ID | Canonical file | Producer stage | Output kind | | Artifact ID | Canonical file | Producer stage | Output kind |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `narratio.transcript.merged` | `transcripts/merged.json` | `merge` | `transcript_merged` | | `narratio.transcript.base` | `transcripts/base.json` | `merge` | `transcript_base` |
| `narratio.transcript.polished` | `transcripts/processed.json` | `polish` | `transcript_processed` | | `narratio.transcript.polished` | `transcripts/polished.json` | `polish` | `transcript_polished` |
| `narratio.transcript.full` | `transcripts/normalized.json` | `normalize` | `transcript_normalized` | | `narratio.transcript.final` | `transcripts/final.json` | `normalize` | `transcript_final` |
| `narratio.transcript.trimmed` | `transcripts/trimmed.json` | `trim` | `transcript_trimmed` | | `narratio.transcript.final_trimmed` | `transcripts/final.trimmed.json` | `trim` | `transcript_final_trimmed` |
| `narratio.bounds.session` | `artifacts/session_bounds.json` | `trim` | `session_bounds` | | `narratio.bounds.session` | `artifacts/session_bounds.json` | `trim` | `session_bounds` |
## Source families ## Source families

View File

@@ -1,7 +1,7 @@
# Stage: merge # Stage: merge
## Purpose ## 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 and Outputs
Inputs: Inputs:
@@ -10,7 +10,7 @@ Inputs:
- `inputs/autocorrect.yml` - `inputs/autocorrect.yml`
Outputs: Outputs:
- `transcripts/merged.json` - `transcripts/base.json`
- optional `artifacts/seriatim.report.json` (when report enabled) - optional `artifacts/seriatim.report.json` (when report enabled)
## Boundaries ## Boundaries
@@ -19,7 +19,7 @@ Owns:
- Per-input normalize calls to Seriatim - Per-input normalize calls to Seriatim
- Final merge call to Seriatim - Final merge call to Seriatim
- Run-local log/config/report path wiring - 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: Does not own:
- Transcript polishing or downstream artifact generation - Transcript polishing or downstream artifact generation
@@ -43,7 +43,7 @@ Does not own:
## State and Manifest Behavior ## State and Manifest Behavior
- Reads transcript inputs from transcribe stage outputs in manifest when present; falls back to canonical raw directory. - 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. - 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. - Records normalized-input provenance and adapter metadata in stage metadata.
## Skip and Resume Behavior ## 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. - Forced rerun of this or upstream stages can stale downstream succeeded stages via runner invalidation.
## Failure Behavior ## 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 ## Tests to Inspect Before Changing
- `internal/stage/merge_test.go` - `internal/stage/merge_test.go`
@@ -59,5 +59,5 @@ Does not own:
## Architectural Invariants ## Architectural Invariants
- Merge consumes normalized forms of each raw transcript. - 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. - Report output is optional and gated by config.

View File

@@ -1,22 +1,22 @@
# Stage: normalize # Stage: normalize
## Purpose ## 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 and Outputs
Inputs: Inputs:
- `transcripts/processed.json` - `transcripts/polished.json`
Outputs: Outputs:
- `transcripts/normalized.json` (or configured normalize output path) - `transcripts/final.json` (or configured normalize output path)
- optional `artifacts/seriatim.normalize.report.json` - optional `artifacts/seriatim.normalize.report.json`
## Boundaries ## Boundaries
Owns: Owns:
- Processed transcript discovery/validation - Polished transcript discovery/validation
- Normalize request construction and invocation - Normalize request construction and invocation
- Optional normalize report wiring - Optional normalize report wiring
- Promotion of normalized transcript and optional report - Promotion of final transcript and optional report
Does not own: Does not own:
- Bounds detection or segment trimming - Bounds detection or segment trimming
@@ -35,9 +35,9 @@ Does not own:
- Seriatim adapter (`Normalize`). - Seriatim adapter (`Normalize`).
## State and Manifest Behavior ## 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. - 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. - Records adapter/result metadata including source path selection.
## Skip and Resume Behavior ## Skip and Resume Behavior
@@ -45,12 +45,12 @@ Does not own:
- Forced reruns can stale downstream succeeded stages. - Forced reruns can stale downstream succeeded stages.
## Failure Behavior ## 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 ## Tests to Inspect Before Changing
- `internal/stage/normalize_test.go` - `internal/stage/normalize_test.go`
- `internal/adapters/seriatim/subprocess_test.go` - `internal/adapters/seriatim/subprocess_test.go`
## Architectural Invariants ## 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. - Default normalize config is applied when `pipeline.normalize` is unset.

View File

@@ -1,23 +1,23 @@
# Stage: polish # Stage: polish
## Purpose ## 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 and Outputs
Inputs: Inputs:
- `transcripts/merged.json` - `transcripts/base.json`
- `inputs/glossary.yml` - `inputs/glossary.yml`
Outputs: Outputs:
- `transcripts/processed.json` - `transcripts/polished.json`
- optional `artifacts/audita.report.json` (when report enabled) - optional `artifacts/audita.report.json` (when report enabled)
## Boundaries ## Boundaries
Owns: Owns:
- Merged transcript discovery/validation - Base transcript discovery/validation
- Audita invocation request construction - Audita invocation request construction
- Run-local logs/config/work-dir/report wiring - 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: Does not own:
- Upstream merge normalization - Upstream merge normalization
@@ -47,9 +47,9 @@ Does not own:
- Audita adapter (`env.Audita.Run`). - Audita adapter (`env.Audita.Run`).
## State and Manifest Behavior ## 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. - 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. - Records adapter invocation metadata, credential presence signal, and output provenance in stage metadata.
## Skip and Resume Behavior ## Skip and Resume Behavior
@@ -57,13 +57,13 @@ Does not own:
- Forced rerun can stale downstream succeeded stages via runner invalidation. - Forced rerun can stale downstream succeeded stages via runner invalidation.
## Failure Behavior ## 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 ## Tests to Inspect Before Changing
- `internal/stage/polish_test.go` - `internal/stage/polish_test.go`
- `internal/adapters/audita/subprocess_test.go` - `internal/adapters/audita/subprocess_test.go`
## Architectural Invariants ## 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. - Report behavior is strictly config-gated.
- Stage output canonicalization always ends at `transcripts/processed.json`. - Stage output canonicalization always ends at `transcripts/polished.json`.

View File

@@ -1,21 +1,21 @@
# Stage: trim # Stage: trim
## Purpose ## 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 and Outputs
Inputs: Inputs:
- `transcripts/normalized.json` - `transcripts/final.json`
Outputs: 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` - when trim enabled: `artifacts/session_bounds.json`
## Boundaries ## Boundaries
Owns: Owns:
- Trim-enabled switch behavior - Trim-enabled switch behavior
- Bounds generation via Scriptorium artifact run - Bounds generation via Scriptorium artifact run
- Bounds validation against normalized transcript - Bounds validation against final transcript
- Keep-selector derivation and Seriatim trim invocation - Keep-selector derivation and Seriatim trim invocation
- Copy-through behavior when disabled or bounds indicate unchanged transcript - Copy-through behavior when disabled or bounds indicate unchanged transcript
@@ -50,19 +50,19 @@ Does not own:
- `Trim` when bounds indicate trimming is required - `Trim` when bounds indicate trimming is required
## State and Manifest Behavior ## 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. - 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. - Records bounds diagnostics, trim action, keep selector, and adapter metadata.
## Skip and Resume Behavior ## Skip and Resume Behavior
- Runner-level skip applies when already succeeded and not forced. - Runner-level skip applies when already succeeded and not forced.
- Forced reruns can stale downstream succeeded stages. - 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 ## Failure Behavior
- Fails on missing/invalid normalized transcript. - 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 trimmed output. - 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 ## Tests to Inspect Before Changing
- `internal/stage/trim_test.go` - `internal/stage/trim_test.go`
@@ -70,6 +70,6 @@ Does not own:
- `internal/adapters/seriatim/subprocess_test.go` - `internal/adapters/seriatim/subprocess_test.go`
## Architectural Invariants ## 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. - `session_bounds` output exists only for enabled trim path.
- Render-debug artifacts are diagnostics and not declared stage outputs. - Render-debug artifacts are diagnostics and not declared stage outputs.

210
docs/roadmap/transcripts.md Normal file
View File

@@ -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`.

View File

@@ -39,8 +39,8 @@ archive:
upload_run: true upload_run: true
# Optional promotion rules; sources use Narratio artifact source IDs. # Optional promotion rules; sources use Narratio artifact source IDs.
promote_artifacts: promote_artifacts:
- source: narratio.transcript.trimmed - source: narratio.transcript.final_trimmed
dest: transcripts/trimmed.json dest: transcripts/final.trimmed.json
required: true required: true
- source: narratio.artifact.session_recap - source: narratio.artifact.session_recap
dest: artifacts/session_recap.md dest: artifacts/session_recap.md
@@ -93,14 +93,14 @@ audita:
normalize: normalize:
# Optional; defaults shown explicitly. # Optional; defaults shown explicitly.
output_path: transcripts/normalized.json output_path: transcripts/final.json
output_schema: seriatim-intermediate output_schema: seriatim-intermediate
report: true report: true
trim: trim:
# Keep disabled unless bounds prompt integration is configured. # Keep disabled unless bounds prompt integration is configured.
enabled: false enabled: false
output_path: transcripts/trimmed.json output_path: transcripts/final.trimmed.json
bounds: bounds:
prompt_id: dnd.session_bounds prompt_id: dnd.session_bounds
profile_id: local-fast profile_id: local-fast
@@ -127,7 +127,7 @@ scriptorium:
timeout: 10m timeout: 10m
inputs: inputs:
transcript: transcript:
source: narratio.transcript.trimmed source: narratio.transcript.final_trimmed
required: true required: true
previous_recap: previous_recap:
source: narratio.previous_session.artifact.session_recap source: narratio.previous_session.artifact.session_recap
@@ -155,7 +155,7 @@ scriptorium:
source: narratio.artifact.session_recap source: narratio.artifact.session_recap
required: true required: true
transcript: transcript:
source: narratio.transcript.trimmed source: narratio.transcript.final_trimmed
required: true required: true
vars: vars:
session_id: true session_id: true

View File

@@ -19,8 +19,8 @@ archive:
enabled: true enabled: true
upload_run: true upload_run: true
promote_artifacts: promote_artifacts:
- source: narratio.transcript.trimmed - source: narratio.transcript.final_trimmed
dest: transcripts/trimmed.json dest: transcripts/final.trimmed.json
required: true required: true
- source: narratio.artifact.session_recap - source: narratio.artifact.session_recap
dest: artifacts/session_recap.md dest: artifacts/session_recap.md
@@ -57,7 +57,7 @@ audita:
report: true report: true
normalize: normalize:
output_path: transcripts/normalized.json output_path: transcripts/final.json
output_schema: seriatim-intermediate output_schema: seriatim-intermediate
report: true report: true
@@ -78,7 +78,7 @@ scriptorium:
timeout: 10m timeout: 10m
inputs: inputs:
transcript: transcript:
source: narratio.transcript.trimmed source: narratio.transcript.final_trimmed
required: true required: true
previous_recap: previous_recap:
source: narratio.previous_session.artifact.session_recap source: narratio.previous_session.artifact.session_recap
@@ -102,7 +102,7 @@ scriptorium:
source: narratio.artifact.session_recap source: narratio.artifact.session_recap
required: true required: true
transcript: transcript:
source: narratio.transcript.trimmed source: narratio.transcript.final_trimmed
required: true required: true
vars: vars:
session_id: true session_id: true

View File

@@ -14,7 +14,7 @@ func TestFakeRunnerCapturesRequestAndReturnsPath(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
req := PolishRequest{ req := PolishRequest{
GeneratedConfigPath: filepath.Join(dir, "config", "audita.yml"), 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"), StdoutLogPath: filepath.Join(dir, "logs", "audita.stdout.log"),
StderrLogPath: filepath.Join(dir, "logs", "audita.stderr.log"), StderrLogPath: filepath.Join(dir, "logs", "audita.stderr.log"),
} }

View File

@@ -52,9 +52,9 @@ func TestSubprocessRunnerSuccessArgsEnvAndValidation(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
req := PolishRequest{ req := PolishRequest{
GeneratedConfigPath: filepath.Join(dir, "audita.generated.yml"), 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"), 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"), ReportPath: filepath.Join(dir, "audita.report.json"),
WorkDir: filepath.Join(dir, "artifacts", "audita-work"), WorkDir: filepath.Join(dir, "artifacts", "audita-work"),
StdoutLogPath: filepath.Join(dir, "audita.stdout.log"), 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 { func auditaReqForTest(t *testing.T, withReport bool) PolishRequest {
t.Helper() t.Helper()
dir := t.TempDir() dir := t.TempDir()
merged := filepath.Join(dir, "merged.json") merged := filepath.Join(dir, "base.json")
glossary := filepath.Join(dir, "glossary.yml") glossary := filepath.Join(dir, "glossary.yml")
writeAuditaTestFile(t, merged, `{"segments":[]}`) writeAuditaTestFile(t, merged, `{"segments":[]}`)
writeAuditaTestFile(t, glossary, "terms: []\n") writeAuditaTestFile(t, glossary, "terms: []\n")
@@ -579,7 +579,7 @@ func auditaReqForTest(t *testing.T, withReport bool) PolishRequest {
GeneratedConfigPath: filepath.Join(dir, "audita.generated.yml"), GeneratedConfigPath: filepath.Join(dir, "audita.generated.yml"),
MergedTranscriptPath: merged, MergedTranscriptPath: merged,
GlossaryPath: glossary, GlossaryPath: glossary,
OutputProcessedPath: filepath.Join(dir, "processed.json"), OutputProcessedPath: filepath.Join(dir, "polished.json"),
WorkDir: filepath.Join(dir, "artifacts", "audita-work"), WorkDir: filepath.Join(dir, "artifacts", "audita-work"),
StdoutLogPath: filepath.Join(dir, "audita.stdout.log"), StdoutLogPath: filepath.Join(dir, "audita.stdout.log"),
StderrLogPath: filepath.Join(dir, "audita.stderr.log"), StderrLogPath: filepath.Join(dir, "audita.stderr.log"),

View File

@@ -31,7 +31,7 @@ func TestSubprocessRunnerRunSuccessBuildsDeterministicArgsAndCapturesLogs(t *tes
ConfigPath: "/etc/scriptorium/config.yml", ConfigPath: "/etc/scriptorium/config.yml",
PromptID: "dnd.session_recap", PromptID: "dnd.session_recap",
ProfileID: "local-quality", 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"}, Vars: map[string]string{"session_id": "2026-05-03", "campaign_name": "Icewind Dale"},
OutputPath: filepath.Join(dir, "artifacts", "session_recap.md"), OutputPath: filepath.Join(dir, "artifacts", "session_recap.md"),
StdoutLogPath: filepath.Join(dir, "logs", "scriptorium.run.stdout.log"), StdoutLogPath: filepath.Join(dir, "logs", "scriptorium.run.stdout.log"),
@@ -180,7 +180,7 @@ func TestSubprocessRunnerRenderSuccess(t *testing.T) {
req := RenderArtifactRequest{ req := RenderArtifactRequest{
Binary: wrapper, Binary: wrapper,
PromptID: "dnd.session_recap", 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"), OutputPath: filepath.Join(dir, "artifacts", "session_recap.render.json"),
StdoutLogPath: filepath.Join(dir, "logs", "scriptorium.render.stdout.log"), StdoutLogPath: filepath.Join(dir, "logs", "scriptorium.render.stdout.log"),
StderrLogPath: filepath.Join(dir, "logs", "scriptorium.render.stderr.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 { func runReqForTest(t *testing.T, binary string) RunArtifactRequest {
t.Helper() t.Helper()
dir := t.TempDir() dir := t.TempDir()
transcriptPath := filepath.Join(dir, "processed.json") transcriptPath := filepath.Join(dir, "polished.json")
writeScriptoriumFile(t, transcriptPath, `{"segments":[]}`) writeScriptoriumFile(t, transcriptPath, `{"segments":[]}`)
return RunArtifactRequest{ return RunArtifactRequest{
Binary: binary, Binary: binary,

View File

@@ -14,7 +14,7 @@ func TestFakeRunnerCapturesRequestAndReturnsPath(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
req := MergeRequest{ req := MergeRequest{
GeneratedConfigPath: filepath.Join(dir, "config", "seriatim.yml"), 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"), StdoutLogPath: filepath.Join(dir, "logs", "seriatim.stdout.log"),
StderrLogPath: filepath.Join(dir, "logs", "seriatim.stderr.log"), StderrLogPath: filepath.Join(dir, "logs", "seriatim.stderr.log"),
} }
@@ -57,8 +57,8 @@ func TestFakeRunnerTrimCapturesRequestAndReturnsPath(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
req := TrimRequest{ req := TrimRequest{
GeneratedConfigPath: filepath.Join(dir, "config", "seriatim.trim.yml"), GeneratedConfigPath: filepath.Join(dir, "config", "seriatim.trim.yml"),
InputTranscriptPath: filepath.Join(dir, "transcripts", "processed.json"), InputTranscriptPath: filepath.Join(dir, "transcripts", "polished.json"),
OutputTrimmedPath: filepath.Join(dir, "transcripts", "trimmed.json"), OutputTrimmedPath: filepath.Join(dir, "transcripts", "final.trimmed.json"),
KeepSelector: "1-10", KeepSelector: "1-10",
StdoutLogPath: filepath.Join(dir, "logs", "seriatim.trim.stdout.log"), StdoutLogPath: filepath.Join(dir, "logs", "seriatim.trim.stdout.log"),
StderrLogPath: filepath.Join(dir, "logs", "seriatim.trim.stderr.log"), StderrLogPath: filepath.Join(dir, "logs", "seriatim.trim.stderr.log"),
@@ -105,8 +105,8 @@ func TestFakeRunnerNormalizeCapturesRequestAndReturnsPath(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
req := NormalizeRequest{ req := NormalizeRequest{
GeneratedConfigPath: filepath.Join(dir, "config", "seriatim.normalize.yml"), GeneratedConfigPath: filepath.Join(dir, "config", "seriatim.normalize.yml"),
InputTranscriptPath: filepath.Join(dir, "transcripts", "processed.json"), InputTranscriptPath: filepath.Join(dir, "transcripts", "polished.json"),
OutputNormalizedPath: filepath.Join(dir, "transcripts", "normalized.json"), OutputNormalizedPath: filepath.Join(dir, "transcripts", "final.json"),
OutputSchema: "seriatim-intermediate", OutputSchema: "seriatim-intermediate",
ReportPath: filepath.Join(dir, "artifacts", "seriatim.normalize.report.json"), ReportPath: filepath.Join(dir, "artifacts", "seriatim.normalize.report.json"),
StdoutLogPath: filepath.Join(dir, "logs", "seriatim.normalize.stdout.log"), StdoutLogPath: filepath.Join(dir, "logs", "seriatim.normalize.stdout.log"),

View File

@@ -50,7 +50,7 @@ func TestSubprocessRunnerSuccessWithReportArgsAndEnv(t *testing.T) {
req := MergeRequest{ req := MergeRequest{
GeneratedConfigPath: filepath.Join(dir, "seriatim.generated.yml"), GeneratedConfigPath: filepath.Join(dir, "seriatim.generated.yml"),
InputTranscriptPaths: []string{filepath.Join(dir, "a.json"), filepath.Join(dir, "b.json")}, 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"), ReportPath: filepath.Join(dir, "seriatim.report.json"),
SpeakersPath: filepath.Join(dir, "speakers.yml"), SpeakersPath: filepath.Join(dir, "speakers.yml"),
AutocorrectPath: filepath.Join(dir, "autocorrect.yml"), AutocorrectPath: filepath.Join(dir, "autocorrect.yml"),
@@ -732,7 +732,7 @@ func mergeReqForTest(t *testing.T, withReport bool) MergeRequest {
req := MergeRequest{ req := MergeRequest{
GeneratedConfigPath: filepath.Join(dir, "seriatim.generated.yml"), GeneratedConfigPath: filepath.Join(dir, "seriatim.generated.yml"),
InputTranscriptPaths: []string{in1, in2}, InputTranscriptPaths: []string{in1, in2},
OutputMergedTranscriptPath: filepath.Join(dir, "merged.json"), OutputMergedTranscriptPath: filepath.Join(dir, "base.json"),
StdoutLogPath: filepath.Join(dir, "seriatim.stdout.log"), StdoutLogPath: filepath.Join(dir, "seriatim.stdout.log"),
StderrLogPath: filepath.Join(dir, "seriatim.stderr.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 { func trimReqForTest(t *testing.T) TrimRequest {
t.Helper() t.Helper()
dir := t.TempDir() dir := t.TempDir()
input := filepath.Join(dir, "processed.json") input := filepath.Join(dir, "polished.json")
writeSeriatimFile(t, input, `{"schema":"seriatim.intermediate.v1","segments":[]}`) writeSeriatimFile(t, input, `{"schema":"seriatim.intermediate.v1","segments":[]}`)
return TrimRequest{ return TrimRequest{
InputTranscriptPath: input, InputTranscriptPath: input,
OutputTrimmedPath: filepath.Join(dir, "trimmed.json"), OutputTrimmedPath: filepath.Join(dir, "final.trimmed.json"),
KeepSelector: "5-12", KeepSelector: "5-12",
GeneratedConfigPath: filepath.Join(dir, "seriatim.trim.generated.yml"), GeneratedConfigPath: filepath.Join(dir, "seriatim.trim.generated.yml"),
StdoutLogPath: filepath.Join(dir, "seriatim.trim.stdout.log"), 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 { func normalizeReqForTest(t *testing.T, withReport bool) NormalizeRequest {
t.Helper() t.Helper()
dir := t.TempDir() dir := t.TempDir()
input := filepath.Join(dir, "processed.json") input := filepath.Join(dir, "polished.json")
writeSeriatimFile(t, input, `{"schema":"audita.processed.v1","segments":[]}`) writeSeriatimFile(t, input, `{"schema":"audita.processed.v1","segments":[]}`)
req := NormalizeRequest{ req := NormalizeRequest{
InputTranscriptPath: input, InputTranscriptPath: input,
OutputNormalizedPath: filepath.Join(dir, "normalized.json"), OutputNormalizedPath: filepath.Join(dir, "final.json"),
OutputSchema: "seriatim-intermediate", OutputSchema: "seriatim-intermediate",
GeneratedConfigPath: filepath.Join(dir, "seriatim.normalize.generated.yml"), GeneratedConfigPath: filepath.Join(dir, "seriatim.normalize.generated.yml"),
StdoutLogPath: filepath.Join(dir, "seriatim.normalize.stdout.log"), StdoutLogPath: filepath.Join(dir, "seriatim.normalize.stdout.log"),

View File

@@ -112,7 +112,7 @@ func TestExecuteRunStageNormalizeIsAccepted(t *testing.T) {
workspaceRoot := t.TempDir() workspaceRoot := t.TempDir()
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot, "https://example.com/transcribe") pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot, "https://example.com/transcribe")
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03") 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 stdout bytes.Buffer
var stderr bytes.Buffer var stderr bytes.Buffer
@@ -233,7 +233,7 @@ inputs:
}) })
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", sessionID) 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") mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "glossary.yml"), "[]\n")
var stdout bytes.Buffer var stdout bytes.Buffer

View File

@@ -894,10 +894,10 @@ func writeArtifactList(out io.Writer, cfg *config.Config, catalog *artifacts.Art
lockSet := lockSourceSet(locks.All) lockSet := lockSourceSet(locks.All)
fmt.Fprintln(out, "Built-in:") fmt.Fprintln(out, "Built-in:")
for _, id := range []string{ for _, id := range []string{
artifacts.ArtifactTranscriptMerged, artifacts.ArtifactTranscriptBase,
artifacts.ArtifactTranscriptPolished, artifacts.ArtifactTranscriptPolished,
artifacts.ArtifactTranscriptFull, artifacts.ArtifactTranscriptFinal,
artifacts.ArtifactTranscriptTrimmed, artifacts.ArtifactTranscriptFinalTrimmed,
artifacts.ArtifactBoundsSession, artifacts.ArtifactBoundsSession,
} { } {
writeArtifactLine(out, id, lockSet) writeArtifactLine(out, id, lockSet)

View File

@@ -476,7 +476,7 @@ func TestExecuteLocksAddListAndRemoveUseRemoteLockStore(t *testing.T) {
"--session", sessionPath, "--session", sessionPath,
"--session-id", "2026-05-03", "--session-id", "2026-05-03",
"--reason", "manual edit", "--reason", "manual edit",
"narratio.transcript.trimmed", "narratio.transcript.final_trimmed",
}, &stdout, &stderr) }, &stdout, &stderr)
if code != 0 { if code != 0 {
t.Fatalf("locks add exit code = %d, want 0; stderr=%q", code, stderr.String()) 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 { if !ok {
t.Fatalf("remote locks key %q not uploaded", key) 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)) t.Fatalf("lock store data = %q", string(obj.Data))
} }
@@ -502,7 +502,7 @@ func TestExecuteLocksAddListAndRemoveUseRemoteLockStore(t *testing.T) {
if code != 0 { if code != 0 {
t.Fatalf("locks list exit code = %d, want 0; stderr=%q", code, stderr.String()) 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()) t.Fatalf("stdout = %q, want remote lock", stdout.String())
} }
@@ -514,7 +514,7 @@ func TestExecuteLocksAddListAndRemoveUseRemoteLockStore(t *testing.T) {
"--campaign", campaignPath, "--campaign", campaignPath,
"--session", sessionPath, "--session", sessionPath,
"--session-id", "2026-05-03", "--session-id", "2026-05-03",
"narratio.transcript.trimmed", "narratio.transcript.final_trimmed",
}, &stdout, &stderr) }, &stdout, &stderr)
if code != 0 { if code != 0 {
t.Fatalf("locks remove exit code = %d, want 0; stderr=%q", code, stderr.String()) 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", sessionPath,
"--session-id", "2026-05-03", "--session-id", "2026-05-03",
"--reason", "first", "--reason", "first",
"narratio.transcript.trimmed", "narratio.transcript.final_trimmed",
}, &stdout, &stderr) }, &stdout, &stderr)
if code != 0 { if code != 0 {
t.Fatalf("initial locks add exit code = %d, want 0; stderr=%q", code, stderr.String()) 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", sessionPath,
"--session-id", "2026-05-03", "--session-id", "2026-05-03",
"--reason", "second", "--reason", "second",
"narratio.transcript.trimmed", "narratio.transcript.final_trimmed",
}, &stdout, &stderr) }, &stdout, &stderr)
if code == 0 { if code == 0 {
t.Fatal("duplicate locks add exit code = 0, want non-zero") 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", "--session-id", "2026-05-03",
"--reason", "second", "--reason", "second",
"--force", "--force",
"narratio.transcript.trimmed", "narratio.transcript.final_trimmed",
}, &stdout, &stderr) }, &stdout, &stderr)
if code != 0 { if code != 0 {
t.Fatalf("forced locks add exit code = %d, want 0; stderr=%q", code, stderr.String()) 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 want string
}{ }{
{"list", []string{"locks"}, "locks: --session-id is required"}, {"list", []string{"locks"}, "locks: --session-id is required"},
{"add", []string{"locks", "add", "narratio.transcript.trimmed"}, "locks add: --session-id is required"}, {"add", []string{"locks", "add", "narratio.transcript.final_trimmed"}, "locks add: --session-id is required"},
{"remove", []string{"locks", "remove", "narratio.transcript.trimmed"}, "locks remove: --session-id is required"}, {"remove", []string{"locks", "remove", "narratio.transcript.final_trimmed"}, "locks remove: --session-id is required"},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@@ -620,7 +620,7 @@ func TestExecuteLocksRequireSessionID(t *testing.T) {
func TestExecuteLocksCannotModifyStaticLocks(t *testing.T) { func TestExecuteLocksCannotModifyStaticLocks(t *testing.T) {
workspaceRoot := t.TempDir() workspaceRoot := t.TempDir()
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
addStaticArchiveLockToPipelineConfig(t, pipelinePath, "narratio.transcript.trimmed") addStaticArchiveLockToPipelineConfig(t, pipelinePath, "narratio.transcript.final_trimmed")
fake := &storage.FakeBackend{} fake := &storage.FakeBackend{}
var storeInitCalls int var storeInitCalls int
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath}) restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
@@ -633,7 +633,7 @@ func TestExecuteLocksCannotModifyStaticLocks(t *testing.T) {
"--campaign", campaignPath, "--campaign", campaignPath,
"--session", sessionPath, "--session", sessionPath,
"--session-id", "2026-05-03", "--session-id", "2026-05-03",
"narratio.transcript.trimmed", "narratio.transcript.final_trimmed",
}, &stdout, &stderr) }, &stdout, &stderr)
if code == 0 { if code == 0 {
t.Fatal("locks add static lock exit code = 0, want non-zero") t.Fatal("locks add static lock exit code = 0, want non-zero")
@@ -650,7 +650,7 @@ func TestExecuteLocksCannotModifyStaticLocks(t *testing.T) {
"--campaign", campaignPath, "--campaign", campaignPath,
"--session", sessionPath, "--session", sessionPath,
"--session-id", "2026-05-03", "--session-id", "2026-05-03",
"narratio.transcript.trimmed", "narratio.transcript.final_trimmed",
}, &stdout, &stderr) }, &stdout, &stderr)
if code == 0 { if code == 0 {
t.Fatal("locks remove static lock exit code = 0, want non-zero") 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) { t.Run(cmd, func(t *testing.T) {
var stdout bytes.Buffer var stdout bytes.Buffer
var stderr 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 { if code == 0 {
t.Fatal("exit code = 0, want non-zero") t.Fatal("exit code = 0, want non-zero")
} }
@@ -718,14 +718,14 @@ func TestExecuteArtifactsListRemoteReportsPromotedAvailability(t *testing.T) {
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
addArchivePromotionsToPipeline(t, pipelinePath, ` addArchivePromotionsToPipeline(t, pipelinePath, `
promote_artifacts: promote_artifacts:
- source: narratio.transcript.trimmed - source: narratio.transcript.final_trimmed
dest: transcripts/trimmed.json dest: transcripts/final.trimmed.json
required: true required: true
`) `)
fake := &storage.FakeBackend{} fake := &storage.FakeBackend{}
trimmedKey := artifacts.S3PromotedArtifactKey( trimmedKey := artifacts.S3PromotedArtifactKey(
artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"), artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"),
"transcripts/trimmed.json", "transcripts/final.trimmed.json",
) )
fake.SeedObject(storage.FakeObject{Key: trimmedKey, Data: []byte(`{"segments":[]}`)}) fake.SeedObject(storage.FakeObject{Key: trimmedKey, Data: []byte(`{"segments":[]}`)})
var storeInitCalls int var storeInitCalls int
@@ -743,7 +743,7 @@ func TestExecuteArtifactsListRemoteReportsPromotedAvailability(t *testing.T) {
if code != 0 { if code != 0 {
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) 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()) 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) pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
addArchivePromotionsToPipeline(t, pipelinePath, ` addArchivePromotionsToPipeline(t, pipelinePath, `
promote_artifacts: promote_artifacts:
- source: narratio.transcript.full - source: narratio.transcript.final
dest: transcripts/full.json dest: transcripts/full.json
required: true required: true
- source: narratio.bounds.session - source: narratio.bounds.session
@@ -781,7 +781,7 @@ func TestExecuteArtifactsListRemoteUsesPromotionDestinations(t *testing.T) {
} }
out := stdout.String() out := stdout.String()
for _, unwanted := range []string{ for _, unwanted := range []string{
"narratio.transcript.full remote=missing", "narratio.transcript.final remote=missing",
"narratio.bounds.session remote=missing", "narratio.bounds.session remote=missing",
} { } {
if strings.Contains(out, unwanted) { if strings.Contains(out, unwanted) {
@@ -789,7 +789,7 @@ func TestExecuteArtifactsListRemoteUsesPromotionDestinations(t *testing.T) {
} }
} }
for _, want := range []string{ 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", "narratio.bounds.session dest=transcripts/bounds.json remote=promoted",
} { } {
if !strings.Contains(out, want) { if !strings.Contains(out, want) {
@@ -803,24 +803,24 @@ func TestExecuteStatusReportsRemoteArtifactCatalog(t *testing.T) {
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
addArchivePromotionsToPipeline(t, pipelinePath, ` addArchivePromotionsToPipeline(t, pipelinePath, `
promote_artifacts: promote_artifacts:
- source: narratio.transcript.trimmed - source: narratio.transcript.final_trimmed
dest: transcripts/trimmed.json dest: transcripts/final.trimmed.json
required: true required: true
- source: narratio.transcript.full - source: narratio.transcript.final
dest: transcripts/full.json dest: transcripts/full.json
required: true required: true
`) `)
fake := &storage.FakeBackend{} fake := &storage.FakeBackend{}
sessionPrefix := artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03") sessionPrefix := artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03")
manifestKey, runIDKey := artifacts.ResolveArchiveCurrentStateKeys(sessionPrefix) 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") fullKey := artifacts.S3PromotedArtifactKey(sessionPrefix, "transcripts/full.json")
lockKey := artifacts.S3SessionLocksKey(sessionPrefix) lockKey := artifacts.S3SessionLocksKey(sessionPrefix)
fake.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")}) 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: manifestKey, Data: restoreManifestJSON(t, "2026-05-03", "sample-campaign")})
fake.SeedObject(storage.FakeObject{Key: trimmedKey, Data: []byte(`{"segments":[]}`)}) fake.SeedObject(storage.FakeObject{Key: trimmedKey, Data: []byte(`{"segments":[]}`)})
fake.SeedObject(storage.FakeObject{Key: fullKey, 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 var storeInitCalls int
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath}) restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
@@ -843,15 +843,15 @@ func TestExecuteStatusReportsRemoteArtifactCatalog(t *testing.T) {
"Configured:", "Configured:",
"Previous-session:", "Previous-session:",
"Promoted:", "Promoted:",
"narratio.transcript.trimmed locked", "narratio.transcript.final_trimmed locked",
"narratio.transcript.trimmed locked remote=promoted", "narratio.transcript.final_trimmed locked remote=promoted",
"narratio.transcript.full dest=transcripts/full.json remote=promoted", "narratio.transcript.final dest=transcripts/full.json remote=promoted",
} { } {
if !strings.Contains(out, want) { if !strings.Contains(out, want) {
t.Fatalf("stdout = %q, want %q", 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) 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) pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
addArchivePromotionsToPipeline(t, pipelinePath, ` addArchivePromotionsToPipeline(t, pipelinePath, `
promote_artifacts: promote_artifacts:
- source: narratio.transcript.trimmed - source: narratio.transcript.final_trimmed
dest: transcripts/trimmed.json dest: transcripts/final.trimmed.json
required: true required: true
`) `)
fake := &storage.FakeBackend{ExistsErr: fmt.Errorf("exists failed")} 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:") { if !strings.Contains(out, "Remote archive: missing or unavailable:") {
t.Fatalf("stdout = %q, want remote archive unavailable state", out) 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) t.Fatalf("stdout = %q, want remote output error state", out)
} }
if !strings.Contains(out, "Archive locks: error:") { if !strings.Contains(out, "Archive locks: error:") {
@@ -898,7 +898,7 @@ func TestExecuteArchiveLoadsRemoteLocks(t *testing.T) {
pipelinePath, campaignPath, sessionPath := writeValidArchiveConfigFiles(t, workspaceRoot) pipelinePath, campaignPath, sessionPath := writeValidArchiveConfigFiles(t, workspaceRoot)
fake := &storage.FakeBackend{} fake := &storage.FakeBackend{}
lockKey := artifacts.S3SessionLocksKey(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03")) 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 var storeInitCalls int
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath}) 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. // The archive stage only checks the manifest statuses and source files.
_ = stageName _ = 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 stdout bytes.Buffer
var stderr bytes.Buffer var stderr bytes.Buffer
@@ -915,7 +915,7 @@ func TestExecuteArchiveLoadsRemoteLocks(t *testing.T) {
if code != 0 { if code != 0 {
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String()) 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 { if _, ok := fake.Objects[promotedKey]; ok {
t.Fatalf("locked promoted key %q was uploaded", promotedKey) t.Fatalf("locked promoted key %q was uploaded", promotedKey)
} }

View File

@@ -197,7 +197,7 @@ func TestPostArchiveCleanupNotRunWhenPromotionIsMissing(t *testing.T) {
cfg.Pipeline.Spool.DeleteAudioAfterArchive = true cfg.Pipeline.Spool.DeleteAudioAfterArchive = true
cfg.Pipeline.Workspace.CleanupAfterArchive = true cfg.Pipeline.Workspace.CleanupAfterArchive = true
cfg.Pipeline.Archive.PromoteArtifacts = []config.ArchivePromotionRule{ 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") archiveStageImpl, err := stage.Select("archive")
@@ -333,7 +333,7 @@ func archiveStageCleanupFixture(t *testing.T) (*config.Config, cleanupSeed, stri
Enabled: boolPtr(true), Enabled: boolPtr(true),
UploadRun: boolPtr(true), UploadRun: boolPtr(true),
PromoteArtifacts: []config.ArchivePromotionRule{ 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)}, {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() t.Helper()
mustWriteFile(t, filepath.Join(runWorkDir, "prepare", "inputs", "session.yml"), "session_id: 2026-05-03\n") 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, "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, "analyze", "outputs", "artifacts", "session_recap.md"), "# recap\n")
mustWriteFile(t, filepath.Join(runWorkDir, "polish", "reports", "audita.report.json"), "{}\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, "merge", "config", "seriatim.generated.yml"), "key: value\n")
mustWriteFile(t, filepath.Join(runWorkDir, "logs", "audita.stderr.log"), "stderr\n") mustWriteFile(t, filepath.Join(runWorkDir, "logs", "audita.stderr.log"), "stderr\n")
mustWriteFile(t, filepath.Join(runWorkDir, "manifest.json"), "{}\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") mustWriteFile(t, filepath.Join(sessionRoot, "artifacts", "session_recap.md"), "# recap\n")
} }

View File

@@ -170,7 +170,7 @@ scriptorium:
output_path: artifacts/session_recap.md output_path: artifacts/session_recap.md
inputs: inputs:
transcript: transcript:
source: narratio.transcript.trimmed source: narratio.transcript.final_trimmed
required: true required: true
previous_recap: previous_recap:
source: narratio.previous_session.artifact.session_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) cfg, sessionPrefix, manifestKey, runIDKey := seedRestoreCommittedState(t, fakeStore, pipelinePath, campaignPath, sessionPath)
seedRestoreObject(fakeStore, runIDKey, []byte("20260519T010203Z-a1b2c3d4\n")) seedRestoreObject(fakeStore, runIDKey, []byte("20260519T010203Z-a1b2c3d4\n"))
seedRestoreObject(fakeStore, manifestKey, restoreWorkflowManifestJSON(t, cfg.Session.SessionID, cfg.Session.Campaign)) 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") seedRestorePreviousCurrent(t, fakeStore, cfg, "# previous recap\n")
restoreWithStoreAndRealPhases(t, fakeStore) restoreWithStoreAndRealPhases(t, fakeStore)
@@ -210,7 +210,7 @@ previous_session_id: 2026-04-26
} }
sessionRoot := artifacts.SessionWorkDirForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID) 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")) previousManifestBytes, err := os.ReadFile(filepath.Join(sessionRoot, "previous", "manifest.json"))
if err != nil { if err != nil {
t.Fatalf("read restored previous manifest: %v", err) 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)) t.Fatalf("scriptorium run requests = %d, want 1", len(scriptoriumFake.RunRequests))
} }
req := scriptoriumFake.RunRequests[0] 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) 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") { if got := req.InputPaths["previous_recap"]; got != filepath.Join(sessionRoot, "previous", "artifacts", "session_recap.md") {

View File

@@ -107,7 +107,7 @@ func TestRunStageExecutesOnlySelectedStage(t *testing.T) {
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json") manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03") 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") mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "glossary.yml"), "terms: []\n")
var out bytes.Buffer var out bytes.Buffer
@@ -137,7 +137,7 @@ func TestRunStageSkipAndForce(t *testing.T) {
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json") manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03") 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") mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "glossary.yml"), "terms: []\n")
store := &manifest.LocalStore{} store := &manifest.LocalStore{}
@@ -171,7 +171,7 @@ func TestRunStageForceMarksDownstreamStaleAndResumeContinuesFromStale(t *testing
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json") manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03") 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") mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "glossary.yml"), "terms: []\n")
store := &manifest.LocalStore{} store := &manifest.LocalStore{}
@@ -217,7 +217,7 @@ func TestRunStageTrimExecutes(t *testing.T) {
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot) pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json") manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03") 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 var out bytes.Buffer
err := RunStage(context.Background(), []string{"--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "trim"}, &out) 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) pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json") manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03") 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 var out bytes.Buffer
err := RunStage(context.Background(), []string{"--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "normalize"}, &out) err := RunStage(context.Background(), []string{"--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "normalize"}, &out)

View File

@@ -761,7 +761,7 @@ func TestExecuteStagesRunLocalArtifactsAndCanonicalPromotion(t *testing.T) {
filepath.Join(runRoot, "merge", "logs", "seriatim.stdout.log"), filepath.Join(runRoot, "merge", "logs", "seriatim.stdout.log"),
filepath.Join(runRoot, "polish", "config", "audita.generated.yml"), filepath.Join(runRoot, "polish", "config", "audita.generated.yml"),
filepath.Join(runRoot, "normalize", "logs", "seriatim.normalize.stdout.log"), 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 { for _, p := range runLocalChecks {
if _, statErr := os.Stat(p); statErr != nil { if _, statErr := os.Stat(p); statErr != nil {
@@ -771,10 +771,10 @@ func TestExecuteStagesRunLocalArtifactsAndCanonicalPromotion(t *testing.T) {
canonicalChecks := []string{ canonicalChecks := []string{
filepath.Join(paths.TranscriptsRawDir, "alice.json"), filepath.Join(paths.TranscriptsRawDir, "alice.json"),
filepath.Join(paths.TranscriptsDir, "merged.json"), filepath.Join(paths.TranscriptsDir, "base.json"),
filepath.Join(paths.TranscriptsDir, "processed.json"), filepath.Join(paths.TranscriptsDir, "polished.json"),
filepath.Join(paths.TranscriptsDir, "normalized.json"), filepath.Join(paths.TranscriptsDir, "final.json"),
filepath.Join(paths.TranscriptsDir, "trimmed.json"), filepath.Join(paths.TranscriptsDir, "final.trimmed.json"),
} }
for _, p := range canonicalChecks { for _, p := range canonicalChecks {
if _, statErr := os.Stat(p); statErr != nil { if _, statErr := os.Stat(p); statErr != nil {
@@ -919,7 +919,7 @@ func TestAdapterBackedStageFailureMarksManifestFailed(t *testing.T) {
if ensureErr != nil { if ensureErr != nil {
t.Fatalf("EnsureLayout() error = %v", ensureErr) 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) t.Fatalf("write merged transcript: %v", err)
} }
if err := os.WriteFile(filepath.Join(paths.InputsDir, "glossary.yml"), []byte("terms: []\n"), 0o644); err != nil { 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 { if ensureErr != nil {
t.Fatalf("EnsureLayout() error = %v", ensureErr) 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) t.Fatalf("write processed transcript: %v", err)
} }
cfg.Pipeline.Scriptorium = &config.ScriptoriumConfig{ cfg.Pipeline.Scriptorium = &config.ScriptoriumConfig{

View File

@@ -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
}

View File

@@ -9,20 +9,35 @@ import (
"regexp" "regexp"
"strings" "strings"
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
"gitea.maximumdirect.net/eric/narratio/internal/manifest" "gitea.maximumdirect.net/eric/narratio/internal/manifest"
) )
const ( const (
ArtifactTranscriptMerged = "narratio.transcript.merged" ArtifactTranscriptBase = artifactmodel.SourceTranscriptBase
ArtifactTranscriptPolished = "narratio.transcript.polished" ArtifactTranscriptPolished = artifactmodel.SourceTranscriptPolished
ArtifactTranscriptFull = "narratio.transcript.full" ArtifactTranscriptFinal = artifactmodel.SourceTranscriptFinal
ArtifactTranscriptTrimmed = "narratio.transcript.trimmed" ArtifactTranscriptFinalTrimmed = artifactmodel.SourceTranscriptFinalTrimmed
ArtifactBoundsSession = "narratio.bounds.session" ArtifactBoundsSession = "narratio.bounds.session"
ArtifactProvenancePreviousCacheManifestInput = "manifest.inputs.previous_cache" ArtifactProvenancePreviousCacheManifestInput = "manifest.inputs.previous_cache"
ArtifactProvenancePreviousCacheFilesystem = "current_session.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. // ErrSessionArtifactNotFound is returned when no readable artifact exists for a known ID.
var ErrSessionArtifactNotFound = errors.New("session artifact not found") var ErrSessionArtifactNotFound = errors.New("session artifact not found")
var configuredArtifactSourceRE = regexp.MustCompile(`^narratio\.artifact\.([a-z][a-z0-9_]*)$`) var configuredArtifactSourceRE = regexp.MustCompile(`^narratio\.artifact\.([a-z][a-z0-9_]*)$`)
@@ -44,42 +59,27 @@ type artifactSpec struct {
ContentKind artifactContentKind ContentKind artifactContentKind
} }
var artifactRegistry = map[string]artifactSpec{ var artifactRegistry = buildArtifactRegistry()
ArtifactTranscriptMerged: {
ID: ArtifactTranscriptMerged, func buildArtifactRegistry() map[string]artifactSpec {
CanonicalRelPath: "transcripts/merged.json", registry := map[string]artifactSpec{}
ProducerStage: "merge", for _, transcript := range RuntimeTranscriptArtifacts() {
OutputKind: "transcript_merged", registry[transcript.SourceID] = artifactSpec{
ContentKind: contentTranscriptJSON, ID: transcript.SourceID,
}, CanonicalRelPath: transcript.CanonicalRelPath,
ArtifactTranscriptPolished: { ProducerStage: transcript.ProducerStage,
ID: ArtifactTranscriptPolished, OutputKind: transcript.OutputKind,
CanonicalRelPath: "transcripts/processed.json", ContentKind: contentTranscriptJSON,
ProducerStage: "polish", }
OutputKind: "transcript_processed", }
ContentKind: contentTranscriptJSON, registry[ArtifactBoundsSession] = artifactSpec{
},
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: {
ID: ArtifactBoundsSession, ID: ArtifactBoundsSession,
CanonicalRelPath: "artifacts/session_bounds.json", CanonicalRelPath: "artifacts/session_bounds.json",
ProducerStage: "trim", ProducerStage: "trim",
OutputKind: "session_bounds", OutputKind: "session_bounds",
ContentKind: contentJSON, ContentKind: contentJSON,
}, }
return registry
} }
// ResolvedSessionArtifact describes one session-level artifact lookup result. // ResolvedSessionArtifact describes one session-level artifact lookup result.

View File

@@ -23,7 +23,7 @@ func TestNormalizeSessionArtifactSource(t *testing.T) {
{name: "legacy alias normalized unsupported", source: "normalized_transcript", wantErr: "unsupported artifact source"}, {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: "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: "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"}, {name: "unsupported", source: "narratio.unknown", wantErr: "unsupported artifact source"},
} }
@@ -77,7 +77,7 @@ func TestConfiguredArtifactSourceHelpers(t *testing.T) {
}, },
{ {
name: "built-in", name: "built-in",
source: ArtifactTranscriptMerged, source: ArtifactTranscriptBase,
wantMatch: false, wantMatch: false,
}, },
} }
@@ -160,7 +160,7 @@ func TestResolveSessionArtifactPrefersManifestOutput(t *testing.T) {
if err := os.WriteFile(manifestPath, []byte(`{"segments":[]}`), 0o644); err != nil { if err := os.WriteFile(manifestPath, []byte(`{"segments":[]}`), 0o644); err != nil {
t.Fatalf("WriteFile() error = %v", err) 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 { if err := os.MkdirAll(filepath.Dir(canonicalPath), 0o755); err != nil {
t.Fatalf("MkdirAll() error = %v", err) t.Fatalf("MkdirAll() error = %v", err)
} }
@@ -170,10 +170,10 @@ func TestResolveSessionArtifactPrefersManifestOutput(t *testing.T) {
m := manifest.New("session", time.Now().UTC()) m := manifest.New("session", time.Now().UTC())
m.MarkStageSucceeded("normalize", time.Now().UTC(), []manifest.ArtifactRecord{ 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 { if err != nil {
t.Fatalf("ResolveSessionArtifact() error = %v", err) t.Fatalf("ResolveSessionArtifact() error = %v", err)
} }
@@ -191,7 +191,7 @@ func TestResolveSessionArtifactPrefersManifestOutput(t *testing.T) {
func TestResolveSessionArtifactFallsBackToCanonicalPath(t *testing.T) { func TestResolveSessionArtifactFallsBackToCanonicalPath(t *testing.T) {
workspace := t.TempDir() workspace := t.TempDir()
paths := buildSessionPaths(workspace, "campaign", "session") 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 { if err := os.MkdirAll(filepath.Dir(canonicalPath), 0o755); err != nil {
t.Fatalf("MkdirAll() error = %v", err) t.Fatalf("MkdirAll() error = %v", err)
} }
@@ -199,7 +199,7 @@ func TestResolveSessionArtifactFallsBackToCanonicalPath(t *testing.T) {
t.Fatalf("WriteFile() error = %v", err) t.Fatalf("WriteFile() error = %v", err)
} }
resolved, err := ResolveSessionArtifact(paths, nil, ArtifactTranscriptTrimmed) resolved, err := ResolveSessionArtifact(paths, nil, ArtifactTranscriptFinalTrimmed)
if err != nil { if err != nil {
t.Fatalf("ResolveSessionArtifact() error = %v", err) t.Fatalf("ResolveSessionArtifact() error = %v", err)
} }
@@ -215,7 +215,7 @@ func TestResolveSessionArtifactMissingReturnsTypedError(t *testing.T) {
workspace := t.TempDir() workspace := t.TempDir()
paths := buildSessionPaths(workspace, "campaign", "session") paths := buildSessionPaths(workspace, "campaign", "session")
_, err := ResolveSessionArtifact(paths, nil, ArtifactTranscriptTrimmed) _, err := ResolveSessionArtifact(paths, nil, ArtifactTranscriptFinalTrimmed)
if err == nil { if err == nil {
t.Fatal("expected error, got nil") t.Fatal("expected error, got nil")
} }
@@ -227,7 +227,7 @@ func TestResolveSessionArtifactMissingReturnsTypedError(t *testing.T) {
func TestResolveSessionArtifactValidatesTranscriptShape(t *testing.T) { func TestResolveSessionArtifactValidatesTranscriptShape(t *testing.T) {
workspace := t.TempDir() workspace := t.TempDir()
paths := buildSessionPaths(workspace, "campaign", "session") 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 { if err := os.MkdirAll(filepath.Dir(canonicalPath), 0o755); err != nil {
t.Fatalf("MkdirAll() error = %v", err) t.Fatalf("MkdirAll() error = %v", err)
} }
@@ -247,7 +247,7 @@ func TestResolveSessionArtifactValidatesTranscriptShape(t *testing.T) {
func TestResolveSessionArtifactWithCatalogBuiltInBehaviorUnchanged(t *testing.T) { func TestResolveSessionArtifactWithCatalogBuiltInBehaviorUnchanged(t *testing.T) {
workspace := t.TempDir() workspace := t.TempDir()
paths := buildSessionPaths(workspace, "campaign", "session") 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 { if err := os.MkdirAll(filepath.Dir(canonicalPath), 0o755); err != nil {
t.Fatalf("MkdirAll() error = %v", err) t.Fatalf("MkdirAll() error = %v", err)
} }
@@ -255,7 +255,7 @@ func TestResolveSessionArtifactWithCatalogBuiltInBehaviorUnchanged(t *testing.T)
t.Fatalf("WriteFile() error = %v", err) t.Fatalf("WriteFile() error = %v", err)
} }
resolved, err := ResolveSessionArtifactWithCatalog(paths, nil, ArtifactTranscriptTrimmed, NewArtifactCatalog()) resolved, err := ResolveSessionArtifactWithCatalog(paths, nil, ArtifactTranscriptFinalTrimmed, NewArtifactCatalog())
if err != nil { if err != nil {
t.Fatalf("ResolveSessionArtifactWithCatalog() error = %v", err) t.Fatalf("ResolveSessionArtifactWithCatalog() error = %v", err)
} }

View File

@@ -215,10 +215,10 @@ func (c *ArtifactCatalog) addEntry(entry CatalogEntry) error {
func runtimeBuiltInArtifactIDs() []string { func runtimeBuiltInArtifactIDs() []string {
return []string{ return []string{
ArtifactTranscriptMerged, ArtifactTranscriptBase,
ArtifactTranscriptPolished, ArtifactTranscriptPolished,
ArtifactTranscriptFull, ArtifactTranscriptFinal,
ArtifactTranscriptTrimmed, ArtifactTranscriptFinalTrimmed,
ArtifactBoundsSession, ArtifactBoundsSession,
} }
} }

View File

@@ -8,9 +8,9 @@ func TestArtifactCatalogRegisterBuiltInsAndLookup(t *testing.T) {
t.Fatalf("RegisterBuiltIns() error = %v", err) t.Fatalf("RegisterBuiltIns() error = %v", err)
} }
entry, ok := catalog.Lookup(ArtifactTranscriptFull) entry, ok := catalog.Lookup(ArtifactTranscriptFinal)
if !ok { if !ok {
t.Fatalf("Lookup(%q) ok = false, want true", ArtifactTranscriptFull) t.Fatalf("Lookup(%q) ok = false, want true", ArtifactTranscriptFinal)
} }
if !entry.Planned { if !entry.Planned {
t.Fatalf("entry.Planned = false, want true") t.Fatalf("entry.Planned = false, want true")
@@ -18,8 +18,8 @@ func TestArtifactCatalogRegisterBuiltInsAndLookup(t *testing.T) {
if entry.Executable { if entry.Executable {
t.Fatalf("entry.Executable = true, want false") t.Fatalf("entry.Executable = true, want false")
} }
if entry.CanonicalRelPath != "transcripts/normalized.json" { if entry.CanonicalRelPath != "transcripts/final.json" {
t.Fatalf("entry.CanonicalRelPath = %q, want transcripts/normalized.json", entry.CanonicalRelPath) t.Fatalf("entry.CanonicalRelPath = %q, want transcripts/final.json", entry.CanonicalRelPath)
} }
} }

View File

@@ -24,7 +24,7 @@ func TestCollectPreviousArtifactRequirements(t *testing.T) {
"session_recap": { "session_recap": {
Enabled: true, Enabled: true,
Inputs: map[string]config.ScriptoriumInputConfig{ Inputs: map[string]config.ScriptoriumInputConfig{
"transcript": {Source: "narratio.transcript.trimmed", Required: true}, "transcript": {Source: "narratio.transcript.final_trimmed", Required: true},
}, },
}, },
}, },

View File

@@ -43,8 +43,8 @@ func TestS3KeyConstruction(t *testing.T) {
t.Fatalf("manifest key = %q", manifestKey) t.Fatalf("manifest key = %q", manifestKey)
} }
promoted := S3PromotedArtifactKey(sessionPrefix, "transcripts/trimmed.json") promoted := S3PromotedArtifactKey(sessionPrefix, "transcripts/final.trimmed.json")
if promoted != "dnd/campaigns/forsaken/sessions/2026-04-19/transcripts/trimmed.json" { if promoted != "dnd/campaigns/forsaken/sessions/2026-04-19/transcripts/final.trimmed.json" {
t.Fatalf("promoted key = %q", promoted) t.Fatalf("promoted key = %q", promoted)
} }

View File

@@ -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)
}

View File

@@ -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)
}
}
}

View File

@@ -1,5 +1,7 @@
package config package config
import "gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
// Default filesystem locations for config lookup when config path flags are // Default filesystem locations for config lookup when config path flags are
// omitted. Order is highest to lowest precedence. // omitted. Order is highest to lowest precedence.
const ( const (
@@ -40,32 +42,32 @@ const (
DefaultTrimBoundsTimeout = "10m" DefaultTrimBoundsTimeout = "10m"
DefaultTrimSeriatimReport = false DefaultTrimSeriatimReport = false
DefaultNormalizeOutputPath = "transcripts/normalized.json" DefaultNormalizeOutputPath = artifactmodel.TranscriptPathFinal
DefaultNormalizeOutputSchema = "seriatim-intermediate" DefaultNormalizeOutputSchema = "seriatim-intermediate"
DefaultNormalizeReport = true DefaultNormalizeReport = true
DefaultArchiveEnabled = true DefaultArchiveEnabled = true
DefaultArchiveUploadRun = true DefaultArchiveUploadRun = true
PathWorkDirSegment = "work" PathWorkDirSegment = "work"
PathInputsDirSegment = "inputs" PathInputsDirSegment = "inputs"
PathAudioDirSegment = "audio" PathAudioDirSegment = "audio"
PathTranscriptsSegment = "transcripts" PathTranscriptsSegment = "transcripts"
PathTranscriptsRaw = "transcripts/raw" PathTranscriptsRaw = "transcripts/raw"
PathTranscriptsTrimmed = "transcripts/trimmed" PathTranscriptsTrimmed = "transcripts/trimmed"
PathArtifactsDirSegment = "artifacts" PathArtifactsDirSegment = "artifacts"
PathReportsDirSegment = "reports" PathReportsDirSegment = "reports"
PathConfigDirSegment = "config" PathConfigDirSegment = "config"
PathLogsDirSegment = "logs" PathLogsDirSegment = "logs"
PathCurrentDirSegment = "current" PathCurrentDirSegment = "current"
PathRunsDirSegment = "runs" PathRunsDirSegment = "runs"
PathPreviousDirSegment = "previous" PathPreviousDirSegment = "previous"
PathManifestFile = "manifest.json" PathManifestFile = "manifest.json"
PathLockFile = ".lock" PathLockFile = ".lock"
PathTranscriptMerged = "transcripts/merged.json" PathTranscriptBase = artifactmodel.TranscriptPathBase
PathTranscriptProcessed = "transcripts/processed.json" PathTranscriptPolished = artifactmodel.TranscriptPathPolished
PathTranscriptNormalized = "transcripts/normalized.json" PathTranscriptFinal = artifactmodel.TranscriptPathFinal
PathTranscriptTrimmed = "transcripts/trimmed.json" PathTranscriptFinalTrimmed = artifactmodel.TranscriptPathFinalTrimmed
S3CampaignsSegment = "campaigns" S3CampaignsSegment = "campaigns"
S3SessionsSegment = "sessions" S3SessionsSegment = "sessions"
@@ -78,7 +80,7 @@ const (
// DefaultArchivePromoteArtifacts defines the default archive promotion rules. // DefaultArchivePromoteArtifacts defines the default archive promotion rules.
// Callers should copy this slice before mutating. // Callers should copy this slice before mutating.
var DefaultArchivePromoteArtifacts = []ArchivePromotionRule{ var DefaultArchivePromoteArtifacts = []ArchivePromotionRule{
{Source: "narratio.transcript.trimmed", Dest: PathTranscriptTrimmed}, {Source: artifactmodel.SourceTranscriptFinalTrimmed, Dest: PathTranscriptFinalTrimmed},
} }
// DefaultPipelineConfigSearchPaths defines the default search order for // DefaultPipelineConfigSearchPaths defines the default search order for

View File

@@ -853,8 +853,8 @@ inputs:
if cfg.Pipeline.Normalize == nil { if cfg.Pipeline.Normalize == nil {
t.Fatal("normalize config should be present via defaults") t.Fatal("normalize config should be present via defaults")
} }
if 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/normalized.json") t.Fatalf("normalize.output_path = %q, want %q", cfg.Pipeline.Normalize.OutputPath, "transcripts/final.json")
} }
if cfg.Pipeline.Normalize.OutputSchema != "seriatim-intermediate" { if cfg.Pipeline.Normalize.OutputSchema != "seriatim-intermediate" {
t.Fatalf("normalize.output_schema = %q, want %q", cfg.Pipeline.Normalize.OutputSchema, "seriatim-intermediate") t.Fatalf("normalize.output_schema = %q, want %q", cfg.Pipeline.Normalize.OutputSchema, "seriatim-intermediate")

View File

@@ -21,8 +21,8 @@ func TestNormalizeLoadAndValidate(t *testing.T) {
if cfg.Pipeline.Normalize == nil { if cfg.Pipeline.Normalize == nil {
t.Fatal("normalize config should be present via defaults") t.Fatal("normalize config should be present via defaults")
} }
if 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/normalized.json") t.Fatalf("normalize.output_path = %q, want %q", cfg.Pipeline.Normalize.OutputPath, "transcripts/final.json")
} }
if cfg.Pipeline.Normalize.OutputSchema != "seriatim-intermediate" { if cfg.Pipeline.Normalize.OutputSchema != "seriatim-intermediate" {
t.Fatalf("normalize.output_schema = %q, want %q", 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", name: "invalid normalize output schema fails",
normalizeYAML: `normalize: normalizeYAML: `normalize:
output_path: transcripts/normalized.json output_path: transcripts/final.json
output_schema: not-a-schema output_schema: not-a-schema
report: true report: true
`, `,
@@ -76,7 +76,7 @@ func TestNormalizeLoadAndValidate(t *testing.T) {
{ {
name: "unknown normalize field fails strict decoding", name: "unknown normalize field fails strict decoding",
normalizeYAML: `normalize: normalizeYAML: `normalize:
output_path: transcripts/normalized.json output_path: transcripts/final.json
output_schema: seriatim-intermediate output_schema: seriatim-intermediate
report: true report: true
bogus: true bogus: true

View File

@@ -198,7 +198,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
output_path: artifacts/session_recap.md output_path: artifacts/session_recap.md
inputs: inputs:
transcript: transcript:
source: narratio.transcript.trimmed source: narratio.transcript.final_trimmed
required: true required: true
`, `,
}, },
@@ -287,7 +287,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
output_path: artifacts/session_recap.md output_path: artifacts/session_recap.md
inputs: inputs:
transcript: transcript:
source: narratio.transcript.trimmed source: narratio.transcript.final_trimmed
required: true required: true
player_handout: player_handout:
enabled: true 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: const testPipelineBaseYAML = `workspace:
root: /tmp/narratio root: /tmp/narratio
whisperx: whisperx:

View File

@@ -186,11 +186,11 @@ func TestSpoolAndArchiveDefaults(t *testing.T) {
if item.Required == nil || !*item.Required { if item.Required == nil || !*item.Required {
t.Fatalf("archive.promote_artifacts[0].required = %#v, want true", item.Required) t.Fatalf("archive.promote_artifacts[0].required = %#v, want true", item.Required)
} }
if item.Source != "narratio.transcript.trimmed" { if item.Source != "narratio.transcript.final_trimmed" {
t.Fatalf("archive.promote_artifacts[0].source = %q, want narratio.transcript.trimmed", item.Source) t.Fatalf("archive.promote_artifacts[0].source = %q, want narratio.transcript.final_trimmed", item.Source)
} }
if item.Dest != "transcripts/trimmed.json" { if item.Dest != "transcripts/final.trimmed.json" {
t.Fatalf("archive.promote_artifacts[0].dest = %q, want transcripts/trimmed.json", item.Dest) 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", name: "absolute dest path rejected",
ruleYML: `archive: ruleYML: `archive:
promote_artifacts: promote_artifacts:
- source: "narratio.transcript.trimmed" - source: "narratio.transcript.final_trimmed"
dest: "/transcripts/trimmed.json" dest: "/transcripts/final.trimmed.json"
`, `,
wantErr: "must be a relative path", wantErr: "must be a relative path",
}, },
@@ -213,7 +213,7 @@ func TestArchivePromotionValidation(t *testing.T) {
name: "traversal dest path rejected", name: "traversal dest path rejected",
ruleYML: `archive: ruleYML: `archive:
promote_artifacts: promote_artifacts:
- source: "narratio.transcript.trimmed" - source: "narratio.transcript.final_trimmed"
dest: "../trimmed.json" dest: "../trimmed.json"
`, `,
wantErr: "must not contain path traversal", wantErr: "must not contain path traversal",
@@ -223,7 +223,7 @@ func TestArchivePromotionValidation(t *testing.T) {
ruleYML: `archive: ruleYML: `archive:
promote_artifacts: promote_artifacts:
- source: "narratio.unknown" - source: "narratio.unknown"
dest: "transcripts/trimmed.json" dest: "transcripts/final.trimmed.json"
`, `,
wantErr: "source \"narratio.unknown\" is unsupported", wantErr: "source \"narratio.unknown\" is unsupported",
}, },
@@ -231,9 +231,9 @@ func TestArchivePromotionValidation(t *testing.T) {
name: "duplicate destination rejected", name: "duplicate destination rejected",
ruleYML: `archive: ruleYML: `archive:
promote_artifacts: promote_artifacts:
- source: "narratio.transcript.trimmed" - source: "narratio.transcript.final_trimmed"
dest: "artifacts/shared.md" dest: "artifacts/shared.md"
- source: "narratio.transcript.full" - source: "narratio.transcript.final"
dest: "artifacts/shared.md" dest: "artifacts/shared.md"
`, `,
wantErr: "duplicates another archive promotion destination", 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) { func TestArchivePromotionDerivesDestinationWhenOmitted(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@@ -289,9 +318,9 @@ func TestArchivePromotionDerivesDestinationWhenOmitted(t *testing.T) {
pipelineYML: testPipelineBaseYAML + ` pipelineYML: testPipelineBaseYAML + `
archive: archive:
promote_artifacts: 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", name: "configured source derives configured output path",
@@ -341,7 +370,7 @@ func TestArchiveLockValidation(t *testing.T) {
pipelineYML: testPipelineBaseYAML + ` pipelineYML: testPipelineBaseYAML + `
archive: archive:
locks: locks:
- source: narratio.transcript.trimmed - source: narratio.transcript.final_trimmed
reason: reviewed transcript reason: reviewed transcript
`, `,
}, },
@@ -382,8 +411,8 @@ archive:
pipelineYML: testPipelineBaseYAML + ` pipelineYML: testPipelineBaseYAML + `
archive: archive:
locks: locks:
- source: narratio.transcript.trimmed - source: narratio.transcript.final_trimmed
- source: " narratio.transcript.trimmed " - source: " narratio.transcript.final_trimmed "
`, `,
wantErr: "duplicates another archive lock source", 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) { func TestArchiveLockUnknownFieldFailsStrictDecode(t *testing.T) {
pipelineYAML := testPipelineBaseYAML + ` pipelineYAML := testPipelineBaseYAML + `
archive: archive:
locks: locks:
- source: narratio.transcript.trimmed - source: narratio.transcript.final_trimmed
dest: transcripts/trimmed.json dest: transcripts/final.trimmed.json
` `
pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML) pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML)
_, err := Load(pipelinePath, sessionPath) _, err := Load(pipelinePath, sessionPath)
@@ -428,8 +485,8 @@ func TestArchiveLegacyFromToFailsStrictDecode(t *testing.T) {
pipelineYAML := testPipelineBaseYAML + ` pipelineYAML := testPipelineBaseYAML + `
archive: archive:
promote_artifacts: promote_artifacts:
- from: transcripts/trimmed.json - from: transcripts/final.trimmed.json
to: transcripts/trimmed.json to: transcripts/final.trimmed.json
` `
pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML) pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML)
_, err := Load(pipelinePath, sessionPath) _, err := Load(pipelinePath, sessionPath)
@@ -440,27 +497,27 @@ archive:
func TestArchiveLockStoreBytesStrictDecodeAndValidation(t *testing.T) { func TestArchiveLockStoreBytesStrictDecodeAndValidation(t *testing.T) {
store, err := LoadArchiveLockStoreBytes("locks.yml", []byte(`locks: store, err := LoadArchiveLockStoreBytes("locks.yml", []byte(`locks:
- source: narratio.transcript.trimmed - source: narratio.transcript.final_trimmed
reason: reviewed reason: reviewed
`), nil) `), nil)
if err != nil { if err != nil {
t.Fatalf("LoadArchiveLockStoreBytes() error = %v", err) 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) t.Fatalf("locks = %#v", store.Locks)
} }
_, err = LoadArchiveLockStoreBytes("locks.yml", []byte(`locks: _, err = LoadArchiveLockStoreBytes("locks.yml", []byte(`locks:
- source: narratio.transcript.trimmed - source: narratio.transcript.final_trimmed
dest: transcripts/trimmed.json dest: transcripts/final.trimmed.json
`), nil) `), nil)
if err == nil || !strings.Contains(err.Error(), "strict decode failed") { if err == nil || !strings.Contains(err.Error(), "strict decode failed") {
t.Fatalf("unknown field error = %v, want strict decode failed", err) t.Fatalf("unknown field error = %v, want strict decode failed", err)
} }
_, err = LoadArchiveLockStoreBytes("locks.yml", []byte(`locks: _, err = LoadArchiveLockStoreBytes("locks.yml", []byte(`locks:
- source: narratio.transcript.trimmed - source: narratio.transcript.final_trimmed
- source: narratio.transcript.trimmed - source: narratio.transcript.final_trimmed
`), nil) `), nil)
if err == nil || !strings.Contains(err.Error(), "duplicates another archive lock source") { if err == nil || !strings.Contains(err.Error(), "duplicates another archive lock source") {
t.Fatalf("duplicate error = %v", err) t.Fatalf("duplicate error = %v", err)
@@ -469,19 +526,19 @@ func TestArchiveLockStoreBytesStrictDecodeAndValidation(t *testing.T) {
func TestMergeArchiveLockRulesStaticWins(t *testing.T) { func TestMergeArchiveLockRulesStaticWins(t *testing.T) {
merged := MergeArchiveLockRules( merged := MergeArchiveLockRules(
[]ArchiveLockRule{{Source: "narratio.transcript.trimmed", Reason: "static"}}, []ArchiveLockRule{{Source: "narratio.transcript.final_trimmed", Reason: "static"}},
[]ArchiveLockRule{ []ArchiveLockRule{
{Source: "narratio.transcript.trimmed", Reason: "remote"}, {Source: "narratio.transcript.final_trimmed", Reason: "remote"},
{Source: "narratio.transcript.full", Reason: "remote full"}, {Source: "narratio.transcript.final", Reason: "remote full"},
}, },
) )
if len(merged) != 2 { if len(merged) != 2 {
t.Fatalf("merged len = %d, want 2: %#v", len(merged), merged) 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]) 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]) t.Fatalf("merged[1] = %#v, want remote full lock", merged[1])
} }
} }

View File

@@ -17,7 +17,7 @@ func TestTrimLoadAndValidate(t *testing.T) {
name: "valid trim config", name: "valid trim config",
trimYAML: `trim: trimYAML: `trim:
enabled: true enabled: true
output_path: transcripts/trimmed.json output_path: transcripts/final.trimmed.json
bounds: bounds:
prompt_id: dnd_session.bounds prompt_id: dnd_session.bounds
profile_id: "" profile_id: ""
@@ -45,7 +45,7 @@ func TestTrimLoadAndValidate(t *testing.T) {
{ {
name: "enabled omitted defaults disabled", name: "enabled omitted defaults disabled",
trimYAML: `trim: trimYAML: `trim:
output_path: transcripts/trimmed.json output_path: transcripts/final.trimmed.json
bounds: bounds:
prompt_id: dnd_session.bounds prompt_id: dnd_session.bounds
transcript_input_name: transcript transcript_input_name: transcript
@@ -65,7 +65,7 @@ func TestTrimLoadAndValidate(t *testing.T) {
name: "missing prompt id fails when enabled", name: "missing prompt id fails when enabled",
trimYAML: `trim: trimYAML: `trim:
enabled: true enabled: true
output_path: transcripts/trimmed.json output_path: transcripts/final.trimmed.json
bounds: bounds:
transcript_input_name: transcript transcript_input_name: transcript
output_path: artifacts/session_bounds.json output_path: artifacts/session_bounds.json
@@ -76,7 +76,7 @@ func TestTrimLoadAndValidate(t *testing.T) {
name: "missing transcript input name fails when enabled", name: "missing transcript input name fails when enabled",
trimYAML: `trim: trimYAML: `trim:
enabled: true enabled: true
output_path: transcripts/trimmed.json output_path: transcripts/final.trimmed.json
bounds: bounds:
prompt_id: dnd_session.bounds prompt_id: dnd_session.bounds
output_path: artifacts/session_bounds.json output_path: artifacts/session_bounds.json
@@ -87,7 +87,7 @@ func TestTrimLoadAndValidate(t *testing.T) {
name: "missing bounds output path fails when enabled", name: "missing bounds output path fails when enabled",
trimYAML: `trim: trimYAML: `trim:
enabled: true enabled: true
output_path: transcripts/trimmed.json output_path: transcripts/final.trimmed.json
bounds: bounds:
prompt_id: dnd_session.bounds prompt_id: dnd_session.bounds
transcript_input_name: transcript transcript_input_name: transcript
@@ -109,7 +109,7 @@ func TestTrimLoadAndValidate(t *testing.T) {
name: "invalid timeout fails", name: "invalid timeout fails",
trimYAML: `trim: trimYAML: `trim:
enabled: true enabled: true
output_path: transcripts/trimmed.json output_path: transcripts/final.trimmed.json
bounds: bounds:
prompt_id: dnd_session.bounds prompt_id: dnd_session.bounds
transcript_input_name: transcript transcript_input_name: transcript
@@ -122,7 +122,7 @@ func TestTrimLoadAndValidate(t *testing.T) {
name: "render debug true requires render output path", name: "render debug true requires render output path",
trimYAML: `trim: trimYAML: `trim:
enabled: true enabled: true
output_path: transcripts/trimmed.json output_path: transcripts/final.trimmed.json
bounds: bounds:
prompt_id: dnd_session.bounds prompt_id: dnd_session.bounds
transcript_input_name: transcript transcript_input_name: transcript
@@ -135,7 +135,7 @@ func TestTrimLoadAndValidate(t *testing.T) {
name: "unknown trim field fails strict decoding", name: "unknown trim field fails strict decoding",
trimYAML: `trim: trimYAML: `trim:
enabled: true enabled: true
output_path: transcripts/trimmed.json output_path: transcripts/final.trimmed.json
bounds: bounds:
prompt_id: dnd_session.bounds prompt_id: dnd_session.bounds
transcript_input_name: transcript transcript_input_name: transcript

View File

@@ -7,6 +7,8 @@ import (
"regexp" "regexp"
"strings" "strings"
"time" "time"
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
) )
// Validate checks resolved configuration for required fields and parseable durations. // 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) { func archiveSourceKnown(source string, scriptorium *ScriptoriumConfig) (string, error) {
trimmed := strings.TrimSpace(source) trimmed := strings.TrimSpace(source)
if _, ok := artifactmodel.LookupRuntimeTranscriptArtifact(trimmed); ok {
return "", nil
}
switch trimmed { switch trimmed {
case "narratio.transcript.merged", case "narratio.bounds.session":
"narratio.transcript.polished",
"narratio.transcript.full",
"narratio.transcript.trimmed",
"narratio.bounds.session":
return "", nil return "", nil
} }
matches := narratioArtifactSourceRE.FindStringSubmatch(trimmed) matches := narratioArtifactSourceRE.FindStringSubmatch(trimmed)
@@ -248,15 +249,10 @@ func archiveSourceKnown(source string, scriptorium *ScriptoriumConfig) (string,
func deriveArchivePromotionDest(source string, scriptorium *ScriptoriumConfig) (string, error) { func deriveArchivePromotionDest(source string, scriptorium *ScriptoriumConfig) (string, error) {
trimmed := strings.TrimSpace(source) trimmed := strings.TrimSpace(source)
if spec, ok := artifactmodel.LookupRuntimeTranscriptArtifact(trimmed); ok {
return spec.CanonicalRelPath, nil
}
switch trimmed { 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": case "narratio.bounds.session":
return filepath.ToSlash(filepath.Join(PathArtifactsDirSegment, "session_bounds.json")), nil 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 { func isStaticSupportedScriptoriumInputSource(source string) bool {
if _, ok := artifactmodel.LookupRuntimeTranscriptArtifact(source); ok {
return true
}
switch source { 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": case "narratio.bounds.session":
return true return true
default: default:

View File

@@ -11,7 +11,7 @@ import (
func TestValidBoundsOutputProducesKeepSelector(t *testing.T) { func TestValidBoundsOutputProducesKeepSelector(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
boundsPath := filepath.Join(dir, "bounds.json") boundsPath := filepath.Join(dir, "bounds.json")
transcriptPath := filepath.Join(dir, "processed.json") transcriptPath := filepath.Join(dir, "polished.json")
writeBoundsTestFile(t, boundsPath, `{ writeBoundsTestFile(t, boundsPath, `{
"confidence":"high", "confidence":"high",
@@ -103,7 +103,7 @@ func TestBoundsNonExistentEndIDFails(t *testing.T) {
func TestInvalidTranscriptJSONFails(t *testing.T) { func TestInvalidTranscriptJSONFails(t *testing.T) {
bounds := SessionBounds{TrimAction: "trim", StartSegmentID: intPtr(1), EndSegmentID: intPtr(2)} 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") writeBoundsTestFile(t, path, "not-json")
err := ValidateSessionBoundsAgainstTranscript(bounds, path) err := ValidateSessionBoundsAgainstTranscript(bounds, path)
@@ -117,7 +117,7 @@ func TestInvalidTranscriptJSONFails(t *testing.T) {
func TestTranscriptWithoutSegmentsFails(t *testing.T) { func TestTranscriptWithoutSegmentsFails(t *testing.T) {
bounds := SessionBounds{TrimAction: "trim", StartSegmentID: intPtr(1), EndSegmentID: intPtr(2)} 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"}`) writeBoundsTestFile(t, path, `{"schema":"audita.processed.v1"}`)
err := ValidateSessionBoundsAgainstTranscript(bounds, path) err := ValidateSessionBoundsAgainstTranscript(bounds, path)
@@ -170,7 +170,7 @@ func TestNoTrimActionCopyIsSupported(t *testing.T) {
func writeTranscriptWithIDs(t *testing.T, ids ...int) string { func writeTranscriptWithIDs(t *testing.T, ids ...int) string {
t.Helper() t.Helper()
path := filepath.Join(t.TempDir(), "processed.json") path := filepath.Join(t.TempDir(), "polished.json")
if len(ids) == 0 { if len(ids) == 0 {
writeBoundsTestFile(t, path, `{"segments":[]}`) writeBoundsTestFile(t, path, `{"segments":[]}`)
return path return path

View File

@@ -23,7 +23,7 @@ func TestStageMarkHelpers(t *testing.T) {
} }
succeededAt := runningAt.Add(2 * time.Minute) 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) m.MarkStageSucceeded("transcribe", succeededAt, outputs)
if stage.Status != StatusSucceeded { if stage.Status != StatusSucceeded {
t.Fatalf("status = %q, want %q", 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)) t.Fatalf("outputs len = %d, want 1", len(stage.Outputs))
} }
outputs[0].LocalPath = "mutated.json" 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) t.Fatalf("stage outputs should be copied, got %#v", stage.Outputs)
} }

View File

@@ -21,7 +21,7 @@ func TestLocalStoreCreateSaveLoadRoundTrip(t *testing.T) {
now := time.Date(2026, 5, 3, 12, 0, 0, 0, time.UTC) now := time.Date(2026, 5, 3, 12, 0, 0, 0, time.UTC)
m.MarkStageRunning("prepare", now) 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.Campaign = "forsaken"
m.RunID = "20260515T031522Z-a1b2c3d4" m.RunID = "20260515T031522Z-a1b2c3d4"
m.LocalWorkDir = "/var/lib/narratio/work/forsaken/2026-05-03/20260515T031522Z-a1b2c3d4" m.LocalWorkDir = "/var/lib/narratio/work/forsaken/2026-05-03/20260515T031522Z-a1b2c3d4"

View File

@@ -24,9 +24,9 @@ func (analyzeStage) Name() string { return "analyze" }
func (analyzeStage) Declares() IODecl { func (analyzeStage) Declares() IODecl {
return IODecl{ return IODecl{
Inputs: []artifacts.Ref{ Inputs: []artifacts.Ref{
{Kind: "transcript_processed", Category: "transcripts", RelativePath: "transcripts/processed.json"}, {Kind: "transcript_polished", Category: "transcripts", RelativePath: "transcripts/polished.json"},
{Kind: "transcript_normalized", Category: "transcripts", RelativePath: "transcripts/normalized.json"}, {Kind: "transcript_final", Category: "transcripts", RelativePath: "transcripts/final.json"},
{Kind: "transcript_trimmed", Category: "transcripts", RelativePath: "transcripts/trimmed.json"}, {Kind: "transcript_final_trimmed", Category: "transcripts", RelativePath: "transcripts/final.trimmed.json"},
}, },
Outputs: nil, Outputs: nil,
} }
@@ -562,7 +562,7 @@ func discoverProcessedTranscript(m *manifest.Manifest, paths artifacts.SessionPa
if m != nil && m.Stages != nil { if m != nil && m.Stages != nil {
if sr := m.Stages["polish"]; sr != nil { if sr := m.Stages["polish"]; sr != nil {
for _, out := range sr.Outputs { for _, out := range sr.Outputs {
if out.Kind != "transcript_processed" { if out.Kind != "transcript_polished" {
continue continue
} }
p := strings.TrimSpace(out.LocalPath) 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() { if info, err := os.Stat(fallback); err == nil && !info.IsDir() {
return filepath.Clean(fallback), "fallback.transcripts_dir", nil return filepath.Clean(fallback), "fallback.transcripts_dir", nil
} }
@@ -602,8 +602,8 @@ type analyzeTranscriptInputs struct {
func discoverAnalyzeTranscriptRefs(m *manifest.Manifest, paths artifacts.SessionPaths) analyzeTranscriptInputs { func discoverAnalyzeTranscriptRefs(m *manifest.Manifest, paths artifacts.SessionPaths) analyzeTranscriptInputs {
processedPath, processedSource := discoverAnalyzeArtifactRef(m, paths, artifacts.ArtifactTranscriptPolished) processedPath, processedSource := discoverAnalyzeArtifactRef(m, paths, artifacts.ArtifactTranscriptPolished)
normalizedPath, normalizedSource := discoverAnalyzeArtifactRef(m, paths, artifacts.ArtifactTranscriptFull) normalizedPath, normalizedSource := discoverAnalyzeArtifactRef(m, paths, artifacts.ArtifactTranscriptFinal)
trimmedPath, trimmedSource := discoverAnalyzeArtifactRef(m, paths, artifacts.ArtifactTranscriptTrimmed) trimmedPath, trimmedSource := discoverAnalyzeArtifactRef(m, paths, artifacts.ArtifactTranscriptFinalTrimmed)
return analyzeTranscriptInputs{ return analyzeTranscriptInputs{
ProcessedPath: processedPath, ProcessedPath: processedPath,
ProcessedSource: processedSource, ProcessedSource: processedSource,
@@ -669,9 +669,9 @@ func resolveScriptoriumInput(
switch normalized { switch normalized {
case artifacts.ArtifactTranscriptPolished: case artifacts.ArtifactTranscriptPolished:
return "", false, nil, nil return "", false, nil, nil
case artifacts.ArtifactTranscriptFull: case artifacts.ArtifactTranscriptFinal:
return "", false, nil, fmt.Errorf("normalized transcript input is unavailable; run normalize stage first") 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") return "", false, nil, fmt.Errorf("trimmed transcript input is unavailable; run trim stage first")
default: default:
return "", false, nil, nil return "", false, nil, nil

View File

@@ -19,7 +19,7 @@ import (
func TestAnalyzeGeneratesSessionRecapFromTrimmedTranscript(t *testing.T) { func TestAnalyzeGeneratesSessionRecapFromTrimmedTranscript(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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) result, err := (analyzeStage{}).Run(context.Background(), env, m)
if err != nil { if err != nil {
@@ -36,7 +36,7 @@ func TestAnalyzeGeneratesSessionRecapFromTrimmedTranscript(t *testing.T) {
if req.ProfileID != "local-quality" { if req.ProfileID != "local-quality" {
t.Fatalf("profile id = %q, want local-quality", req.ProfileID) 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"]) t.Fatalf("transcript input = %q, want trimmed transcript path", req.InputPaths["transcript"])
} }
if req.OutputPath != filepath.Join(paths.ArtifactsDir, "session_recap.md") { if req.OutputPath != filepath.Join(paths.ArtifactsDir, "session_recap.md") {
@@ -72,7 +72,7 @@ func TestAnalyzeGeneratesSessionRecapFromTrimmedTranscript(t *testing.T) {
func TestAnalyzeRenderDebugFalseDoesNotCallRenderArtifact(t *testing.T) { func TestAnalyzeRenderDebugFalseDoesNotCallRenderArtifact(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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 env.Config.Pipeline.Scriptorium.RenderDebug = false
@@ -88,7 +88,7 @@ func TestAnalyzeRenderDebugFalseDoesNotCallRenderArtifact(t *testing.T) {
func TestAnalyzeRenderDebugArtifactOverrideFalseWinsOverGlobalTrue(t *testing.T) { func TestAnalyzeRenderDebugArtifactOverrideFalseWinsOverGlobalTrue(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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 env.Config.Pipeline.Scriptorium.RenderDebug = true
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
@@ -111,7 +111,7 @@ func TestAnalyzeRenderDebugArtifactOverrideFalseWinsOverGlobalTrue(t *testing.T)
func TestAnalyzeRenderDebugTrueCallsRenderBeforeRun(t *testing.T) { func TestAnalyzeRenderDebugTrueCallsRenderBeforeRun(t *testing.T) {
env, m, _ := setupAnalyzeEnv(t) env, m, _ := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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 env.Config.Pipeline.Scriptorium.RenderDebug = true
runner := &orderedScriptoriumRunner{ runner := &orderedScriptoriumRunner{
@@ -132,7 +132,7 @@ func TestAnalyzeRenderDebugTrueCallsRenderBeforeRun(t *testing.T) {
func TestAnalyzeRenderOutputPathIsRecorded(t *testing.T) { func TestAnalyzeRenderOutputPathIsRecorded(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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 env.Config.Pipeline.Scriptorium.RenderDebug = true
result, err := (analyzeStage{}).Run(context.Background(), env, m) result, err := (analyzeStage{}).Run(context.Background(), env, m)
@@ -156,7 +156,7 @@ func TestAnalyzeRenderOutputPathIsRecorded(t *testing.T) {
func TestAnalyzeRenderFailurePreventsRun(t *testing.T) { func TestAnalyzeRenderFailurePreventsRun(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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 env.Config.Pipeline.Scriptorium.RenderDebug = true
fake.RenderErr = errors.New("render boom") fake.RenderErr = errors.New("render boom")
@@ -178,7 +178,7 @@ func TestAnalyzeRenderFailurePreventsRun(t *testing.T) {
func TestAnalyzeRenderInvalidJSONFailsClearly(t *testing.T) { func TestAnalyzeRenderInvalidJSONFailsClearly(t *testing.T) {
env, m, _ := setupAnalyzeEnv(t) env, m, _ := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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 env.Config.Pipeline.Scriptorium.RenderDebug = true
runner := &orderedScriptoriumRunner{ runner := &orderedScriptoriumRunner{
RenderBody: `not-json`, RenderBody: `not-json`,
@@ -201,7 +201,7 @@ func TestAnalyzeRenderInvalidJSONFailsClearly(t *testing.T) {
func TestAnalyzeRunStillSucceedsWhenRenderSucceeds(t *testing.T) { func TestAnalyzeRunStillSucceedsWhenRenderSucceeds(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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 env.Config.Pipeline.Scriptorium.RenderDebug = true
result, err := (analyzeStage{}).Run(context.Background(), env, m) result, err := (analyzeStage{}).Run(context.Background(), env, m)
@@ -222,7 +222,7 @@ func TestAnalyzeRunStillSucceedsWhenRenderSucceeds(t *testing.T) {
func TestAnalyzeOmitsOptionalCanonicalPreviousRecapWhenUnavailable(t *testing.T) { func TestAnalyzeOmitsOptionalCanonicalPreviousRecapWhenUnavailable(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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{ env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = config.ScriptoriumArtifactConfig{
Enabled: true, Enabled: true,
@@ -261,7 +261,7 @@ func TestAnalyzeUsesRunLocalPathsAndPromotesCanonical(t *testing.T) {
m.Campaign = "sample-campaign" m.Campaign = "sample-campaign"
m.RunID = "20260518T010203Z-abcdef12" m.RunID = "20260518T010203Z-abcdef12"
paths := sessionPathsForEnv(env, m.SessionID) 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) result, err := (analyzeStage{}).Run(context.Background(), env, m)
if err != nil { if err != nil {
@@ -344,7 +344,7 @@ func (r *orderedScriptoriumRunner) RunArtifact(_ context.Context, req scriptoriu
func TestAnalyzeIncludesCanonicalPreviousRecapWhenPreparedCacheExists(t *testing.T) { func TestAnalyzeIncludesCanonicalPreviousRecapWhenPreparedCacheExists(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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") previousRecapPath := artifacts.SessionPreviousArtifactPath(paths, "artifacts/session_recap.md")
writeAnalyzeFile(t, previousRecapPath, "previous recap\n") writeAnalyzeFile(t, previousRecapPath, "previous recap\n")
@@ -383,7 +383,7 @@ func TestAnalyzeIncludesCanonicalPreviousRecapWhenPreparedCacheExists(t *testing
func TestAnalyzeFailsWhenRequiredCanonicalPreviousRecapMissing(t *testing.T) { func TestAnalyzeFailsWhenRequiredCanonicalPreviousRecapMissing(t *testing.T) {
env, m, _ := setupAnalyzeEnv(t) env, m, _ := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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{ env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = config.ScriptoriumArtifactConfig{
Enabled: true, Enabled: true,
@@ -413,7 +413,7 @@ func TestAnalyzeFailsWhenRequiredCanonicalPreviousRecapMissing(t *testing.T) {
func TestAnalyzeResolvesConfiguredArtifactInputFromDisabledArtifactOutput(t *testing.T) { func TestAnalyzeResolvesConfiguredArtifactInputFromDisabledArtifactOutput(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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") playerHandoutPath := filepath.Join(paths.ArtifactsDir, "player_handout.md")
writeAnalyzeFile(t, playerHandoutPath, "handout\n") writeAnalyzeFile(t, playerHandoutPath, "handout\n")
@@ -443,7 +443,7 @@ func TestAnalyzeResolvesConfiguredArtifactInputFromDisabledArtifactOutput(t *tes
func TestAnalyzeMetadataIncludesGeneratedAndReusedArtifacts(t *testing.T) { func TestAnalyzeMetadataIncludesGeneratedAndReusedArtifacts(t *testing.T) {
env, m, _ := setupAnalyzeEnv(t) env, m, _ := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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") playerHandoutPath := filepath.Join(paths.ArtifactsDir, "player_handout.md")
writeAnalyzeFile(t, playerHandoutPath, "handout\n") writeAnalyzeFile(t, playerHandoutPath, "handout\n")
@@ -512,7 +512,7 @@ func TestAnalyzeMetadataIncludesGeneratedAndReusedArtifacts(t *testing.T) {
func TestAnalyzeRunsMultipleIndependentArtifactsInDeterministicOrder(t *testing.T) { func TestAnalyzeRunsMultipleIndependentArtifactsInDeterministicOrder(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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{ env.Config.Pipeline.Scriptorium.Artifacts["player_handout"] = config.ScriptoriumArtifactConfig{
Enabled: true, Enabled: true,
@@ -521,7 +521,7 @@ func TestAnalyzeRunsMultipleIndependentArtifactsInDeterministicOrder(t *testing.
OutputPath: "artifacts/player_handout.md", OutputPath: "artifacts/player_handout.md",
Inputs: map[string]config.ScriptoriumInputConfig{ Inputs: map[string]config.ScriptoriumInputConfig{
"transcript": { "transcript": {
Source: "narratio.transcript.trimmed", Source: "narratio.transcript.final_trimmed",
Required: true, Required: true,
}, },
}, },
@@ -553,7 +553,7 @@ func TestAnalyzeRunsMultipleIndependentArtifactsInDeterministicOrder(t *testing.
func TestAnalyzeRunsDependenciesBeforeDependents(t *testing.T) { func TestAnalyzeRunsDependenciesBeforeDependents(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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{ env.Config.Pipeline.Scriptorium.Artifacts["player_handout"] = config.ScriptoriumArtifactConfig{
Enabled: true, Enabled: true,
@@ -567,7 +567,7 @@ func TestAnalyzeRunsDependenciesBeforeDependents(t *testing.T) {
Required: true, Required: true,
}, },
"transcript": { "transcript": {
Source: "narratio.transcript.trimmed", Source: "narratio.transcript.final_trimmed",
Required: true, Required: true,
}, },
}, },
@@ -594,7 +594,7 @@ func TestAnalyzeRunsDependenciesBeforeDependents(t *testing.T) {
func TestAnalyzeAppliesSelectedArtifactsFilter(t *testing.T) { func TestAnalyzeAppliesSelectedArtifactsFilter(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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{ env.Config.Pipeline.Scriptorium.Artifacts["player_handout"] = config.ScriptoriumArtifactConfig{
Enabled: true, Enabled: true,
@@ -603,7 +603,7 @@ func TestAnalyzeAppliesSelectedArtifactsFilter(t *testing.T) {
OutputPath: "artifacts/player_handout.md", OutputPath: "artifacts/player_handout.md",
Inputs: map[string]config.ScriptoriumInputConfig{ Inputs: map[string]config.ScriptoriumInputConfig{
"transcript": { "transcript": {
Source: "narratio.transcript.trimmed", Source: "narratio.transcript.final_trimmed",
Required: true, Required: true,
}, },
}, },
@@ -628,7 +628,7 @@ func TestAnalyzeAppliesSelectedArtifactsFilter(t *testing.T) {
func TestAnalyzeMetadataIncludesMultipleGeneratedArtifacts(t *testing.T) { func TestAnalyzeMetadataIncludesMultipleGeneratedArtifacts(t *testing.T) {
env, m, _ := setupAnalyzeEnv(t) env, m, _ := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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{ env.Config.Pipeline.Scriptorium.Artifacts["player_handout"] = config.ScriptoriumArtifactConfig{
Enabled: true, Enabled: true,
@@ -637,7 +637,7 @@ func TestAnalyzeMetadataIncludesMultipleGeneratedArtifacts(t *testing.T) {
OutputPath: "artifacts/player_handout.md", OutputPath: "artifacts/player_handout.md",
Inputs: map[string]config.ScriptoriumInputConfig{ Inputs: map[string]config.ScriptoriumInputConfig{
"transcript": { "transcript": {
Source: "narratio.transcript.trimmed", Source: "narratio.transcript.final_trimmed",
Required: true, Required: true,
}, },
}, },
@@ -663,7 +663,7 @@ func TestAnalyzeMetadataIncludesMultipleGeneratedArtifacts(t *testing.T) {
func TestAnalyzeFailsWhenRequiredConfiguredArtifactMissing(t *testing.T) { func TestAnalyzeFailsWhenRequiredConfiguredArtifactMissing(t *testing.T) {
env, m, _ := setupAnalyzeEnv(t) env, m, _ := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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 := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
sessionRecap.Inputs["recap"] = config.ScriptoriumInputConfig{ sessionRecap.Inputs["recap"] = config.ScriptoriumInputConfig{
@@ -688,7 +688,7 @@ func TestAnalyzeFailsWhenRequiredConfiguredArtifactMissing(t *testing.T) {
func TestAnalyzeOmitsOptionalMissingConfiguredArtifactInput(t *testing.T) { func TestAnalyzeOmitsOptionalMissingConfiguredArtifactInput(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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 := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
sessionRecap.Inputs["recap"] = config.ScriptoriumInputConfig{ sessionRecap.Inputs["recap"] = config.ScriptoriumInputConfig{
@@ -735,7 +735,7 @@ func TestAnalyzeRequiredPreviousSessionArtifactInputGuidesPrepareForce(t *testin
func TestAnalyzeResolvesCanonicalPreviousSessionArtifactFromManifestInput(t *testing.T) { func TestAnalyzeResolvesCanonicalPreviousSessionArtifactFromManifestInput(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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") previousPath := artifacts.SessionPreviousArtifactPath(paths, "artifacts/session_recap.md")
writeAnalyzeFile(t, previousPath, "previous recap\n") writeAnalyzeFile(t, previousPath, "previous recap\n")
@@ -766,7 +766,7 @@ func TestAnalyzeResolvesCanonicalPreviousSessionArtifactFromManifestInput(t *tes
func TestAnalyzeOmitsOptionalMissingCanonicalPreviousSessionArtifact(t *testing.T) { func TestAnalyzeOmitsOptionalMissingCanonicalPreviousSessionArtifact(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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 := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
sessionRecap.Inputs["previous_recap"] = config.ScriptoriumInputConfig{ sessionRecap.Inputs["previous_recap"] = config.ScriptoriumInputConfig{
@@ -790,7 +790,7 @@ func TestAnalyzeOmitsOptionalMissingCanonicalPreviousSessionArtifact(t *testing.
func TestAnalyzeRenderDebugWithCanonicalPreviousSessionInput(t *testing.T) { func TestAnalyzeRenderDebugWithCanonicalPreviousSessionInput(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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 env.Config.Pipeline.Scriptorium.RenderDebug = true
previousPath := artifacts.SessionPreviousArtifactPath(paths, "artifacts/session_recap.md") previousPath := artifacts.SessionPreviousArtifactPath(paths, "artifacts/session_recap.md")
@@ -825,7 +825,7 @@ func TestAnalyzeRenderDebugWithCanonicalPreviousSessionInput(t *testing.T) {
func TestAnalyzeDoesNotCallObjectStoreForCanonicalPreviousSessionInput(t *testing.T) { func TestAnalyzeDoesNotCallObjectStoreForCanonicalPreviousSessionInput(t *testing.T) {
env, m, _ := setupAnalyzeEnv(t) env, m, _ := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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") previousPath := artifacts.SessionPreviousArtifactPath(paths, "artifacts/session_recap.md")
writeAnalyzeFile(t, previousPath, "previous recap\n") writeAnalyzeFile(t, previousPath, "previous recap\n")
@@ -856,7 +856,7 @@ func TestAnalyzeDoesNotCallObjectStoreForCanonicalPreviousSessionInput(t *testin
func TestAnalyzeFailsWhenOutputPathMissing(t *testing.T) { func TestAnalyzeFailsWhenOutputPathMissing(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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 := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
artifact.OutputPath = "" artifact.OutputPath = ""
@@ -891,7 +891,7 @@ func TestAnalyzeFailsWhenTrimmedTranscriptMissing(t *testing.T) {
func TestAnalyzeSupportsProcessedTranscriptSourceWhenConfigured(t *testing.T) { func TestAnalyzeSupportsProcessedTranscriptSourceWhenConfigured(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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 := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{ artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
@@ -907,7 +907,7 @@ func TestAnalyzeSupportsProcessedTranscriptSourceWhenConfigured(t *testing.T) {
if len(fake.RunRequests) != 1 { if len(fake.RunRequests) != 1 {
t.Fatalf("run requests = %d, want 1", len(fake.RunRequests)) 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"]) 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) { func TestAnalyzeSupportsCanonicalTrimmedTranscriptSourceWhenConfigured(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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 := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{ artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
Source: "narratio.transcript.trimmed", Source: "narratio.transcript.final_trimmed",
Required: true, Required: true,
} }
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact
@@ -931,7 +931,7 @@ func TestAnalyzeSupportsCanonicalTrimmedTranscriptSourceWhenConfigured(t *testin
if len(fake.RunRequests) != 1 { if len(fake.RunRequests) != 1 {
t.Fatalf("run requests = %d, want 1", len(fake.RunRequests)) 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"]) 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) { func TestAnalyzeSupportsNormalizedTranscriptSourceWhenConfigured(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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}]}`) writeAnalyzeFile(t, normalizedPath, `{"segments":[{"id":1}]}`)
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{ artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
Source: "narratio.transcript.full", Source: "narratio.transcript.final",
Required: true, Required: true,
} }
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact
@@ -964,17 +964,17 @@ func TestAnalyzeSupportsNormalizedTranscriptSourceWhenConfigured(t *testing.T) {
func TestAnalyzeSupportsCanonicalNormalizedTranscriptSourceFromManifestOutput(t *testing.T) { func TestAnalyzeSupportsCanonicalNormalizedTranscriptSourceFromManifestOutput(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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") manifestPath := filepath.Join(paths.ArtifactsDir, "normalized.from-manifest.json")
writeAnalyzeFile(t, fallbackPath, `{"segments":[{"id":999}]}`) writeAnalyzeFile(t, fallbackPath, `{"segments":[{"id":999}]}`)
writeAnalyzeFile(t, manifestPath, `{"segments":[{"id":10}]}`) writeAnalyzeFile(t, manifestPath, `{"segments":[{"id":10}]}`)
m.MarkStageSucceeded("normalize", time.Now().UTC(), []manifest.ArtifactRecord{ 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 := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{ artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
Source: "narratio.transcript.full", Source: "narratio.transcript.final",
Required: true, Required: true,
} }
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact
@@ -994,17 +994,17 @@ func TestAnalyzeSupportsCanonicalNormalizedTranscriptSourceFromManifestOutput(t
func TestAnalyzeSupportsNormalizedTranscriptSourceFromManifestOutput(t *testing.T) { func TestAnalyzeSupportsNormalizedTranscriptSourceFromManifestOutput(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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") manifestPath := filepath.Join(paths.ArtifactsDir, "normalized.from-manifest.json")
writeAnalyzeFile(t, fallbackPath, `{"segments":[{"id":999}]}`) writeAnalyzeFile(t, fallbackPath, `{"segments":[{"id":999}]}`)
writeAnalyzeFile(t, manifestPath, `{"segments":[{"id":10}]}`) writeAnalyzeFile(t, manifestPath, `{"segments":[{"id":10}]}`)
m.MarkStageSucceeded("normalize", time.Now().UTC(), []manifest.ArtifactRecord{ 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 := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{ artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
Source: "narratio.transcript.full", Source: "narratio.transcript.final",
Required: true, Required: true,
} }
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact
@@ -1025,7 +1025,7 @@ func TestAnalyzeFailsWhenNormalizedTranscriptMissing(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{ artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
Source: "narratio.transcript.full", Source: "narratio.transcript.final",
Required: true, Required: true,
} }
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact
@@ -1048,7 +1048,7 @@ func TestAnalyzeFailsWhenNormalizedTranscriptMissing(t *testing.T) {
func TestAnalyzeFailsWhenProcessedTranscriptInvalidJSON(t *testing.T) { func TestAnalyzeFailsWhenProcessedTranscriptInvalidJSON(t *testing.T) {
env, m, _ := setupAnalyzeEnv(t) env, m, _ := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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) _, err := (analyzeStage{}).Run(context.Background(), env, m)
if err == nil { if err == nil {
@@ -1062,7 +1062,7 @@ func TestAnalyzeFailsWhenProcessedTranscriptInvalidJSON(t *testing.T) {
func TestAnalyzeFailsWhenProcessedTranscriptMissingSegmentsArray(t *testing.T) { func TestAnalyzeFailsWhenProcessedTranscriptMissingSegmentsArray(t *testing.T) {
env, m, _ := setupAnalyzeEnv(t) env, m, _ := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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) _, err := (analyzeStage{}).Run(context.Background(), env, m)
if err == nil { if err == nil {
@@ -1076,7 +1076,7 @@ func TestAnalyzeFailsWhenProcessedTranscriptMissingSegmentsArray(t *testing.T) {
func TestAnalyzeRecordsRefsAndMetadata(t *testing.T) { func TestAnalyzeRecordsRefsAndMetadata(t *testing.T) {
env, m, _ := setupAnalyzeEnv(t) env, m, _ := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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) result, err := (analyzeStage{}).Run(context.Background(), env, m)
if err != nil { if err != nil {
@@ -1105,7 +1105,7 @@ func TestAnalyzeRecordsRefsAndMetadata(t *testing.T) {
func TestAnalyzeHandlesAdapterError(t *testing.T) { func TestAnalyzeHandlesAdapterError(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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") fake.RunErr = errors.New("adapter boom")
_, err := (analyzeStage{}).Run(context.Background(), env, m) _, err := (analyzeStage{}).Run(context.Background(), env, m)
@@ -1120,7 +1120,7 @@ func TestAnalyzeHandlesAdapterError(t *testing.T) {
func TestAnalyzeHandlesValidationFailedResultAsError(t *testing.T) { func TestAnalyzeHandlesValidationFailedResultAsError(t *testing.T) {
env, m, fake := setupAnalyzeEnv(t) env, m, fake := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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{ fake.RunResult = scriptorium.ArtifactResult{
ValidationFailed: true, ValidationFailed: true,
ExitCode: 2, ExitCode: 2,
@@ -1139,7 +1139,7 @@ func TestAnalyzeHandlesValidationFailedResultAsError(t *testing.T) {
func TestAnalyzeSkipsWhenNoEnabledScriptoriumArtifactsConfigured(t *testing.T) { func TestAnalyzeSkipsWhenNoEnabledScriptoriumArtifactsConfigured(t *testing.T) {
env, m, _ := setupAnalyzeEnv(t) env, m, _ := setupAnalyzeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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{ env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = config.ScriptoriumArtifactConfig{
Enabled: false, Enabled: false,
@@ -1199,7 +1199,7 @@ func setupAnalyzeEnv(t *testing.T) (*Env, *manifest.Manifest, *scriptorium.FakeR
Timeout: "2m", Timeout: "2m",
Inputs: map[string]config.ScriptoriumInputConfig{ Inputs: map[string]config.ScriptoriumInputConfig{
"transcript": { "transcript": {
Source: "narratio.transcript.trimmed", Source: "narratio.transcript.final_trimmed",
Required: true, Required: true,
}, },
"previous_recap": { "previous_recap": {

View File

@@ -82,7 +82,7 @@ func TestArchiveUploadsRunRecordPromotionsAndCurrentPointer(t *testing.T) {
"manifest.json", "manifest.json",
"polish/reports/audita.report.json", "polish/reports/audita.report.json",
"transcribe/outputs/transcripts/raw/speaker.json", "transcribe/outputs/transcripts/raw/speaker.json",
"trim/outputs/transcripts/trimmed.json", "trim/outputs/transcripts/final.trimmed.json",
} }
for _, rel := range wantRunUploads { for _, rel := range wantRunUploads {
key := runPrefix + rel 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" recapKey := sessionPrefix + "artifacts/session_recap.md"
if _, ok := fake.Objects[trimmedKey]; !ok { if _, ok := fake.Objects[trimmedKey]; !ok {
t.Fatalf("missing promoted key %q", trimmedKey) t.Fatalf("missing promoted key %q", trimmedKey)
@@ -180,7 +180,7 @@ func TestArchiveToleratesMissingPreviousCache(t *testing.T) {
func TestArchiveUsesCustomPromotionRules(t *testing.T) { func TestArchiveUsesCustomPromotionRules(t *testing.T) {
env, m, _ := archiveFixture(t) env, m, _ := archiveFixture(t)
env.Config.Pipeline.Archive.PromoteArtifacts = []config.ArchivePromotionRule{ 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)}, {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) { func TestArchiveSkipsOptionalMissingPromotion(t *testing.T) {
env, m, _ := archiveFixture(t) env, m, _ := archiveFixture(t)
env.Config.Pipeline.Archive.PromoteArtifacts = []config.ArchivePromotionRule{ env.Config.Pipeline.Archive.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.transcript.merged", Dest: "transcripts/merged.json", Required: boolPtr(false)}, {Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(false)},
} }
result, err := archiveStage{}.Run(context.Background(), env, m) result, err := archiveStage{}.Run(context.Background(), env, m)
@@ -210,7 +210,7 @@ func TestArchiveSkipsOptionalMissingPromotion(t *testing.T) {
t.Fatalf("Run() error = %v", err) t.Fatalf("Run() error = %v", err)
} }
got, _ := result.Metadata["skipped_optional_promotions"].([]string) got, _ := result.Metadata["skipped_optional_promotions"].([]string)
want := []string{"transcripts/merged.json"} want := []string{"transcripts/base.json"}
if !reflect.DeepEqual(got, want) { if !reflect.DeepEqual(got, want) {
t.Fatalf("skipped_optional_promotions = %#v, want %#v", got, want) t.Fatalf("skipped_optional_promotions = %#v, want %#v", got, want)
} }
@@ -230,7 +230,7 @@ func TestArchiveSkipsRequiredUnselectedConfiguredPromotion(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Run() error = %v", err) 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") t.Fatalf("missing built-in promoted trimmed key")
} }
if _, ok := fake.Objects[m.S3SessionPrefix+"artifacts/session_recap.md"]; ok { if _, ok := fake.Objects[m.S3SessionPrefix+"artifacts/session_recap.md"]; ok {
@@ -321,7 +321,7 @@ func TestArchiveLockedUnselectedConfiguredPromotionSkipsAsUnselected(t *testing.
func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) { func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) {
env, m, _ := archiveFixture(t) env, m, _ := archiveFixture(t)
env.Config.Pipeline.Archive.Locks = []config.ArchiveLockRule{ 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) fake := env.ObjectStore.(*storage.FakeBackend)
@@ -330,7 +330,7 @@ func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) {
t.Fatalf("Run() error = %v", err) 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 { if _, ok := fake.Objects[trimmedKey]; ok {
t.Fatalf("locked promotion key %q should not be uploaded", trimmedKey) 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 { if _, ok := fake.Objects[recapKey]; !ok {
t.Fatalf("unlocked promotion key %q should be uploaded", recapKey) 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 { if _, ok := fake.Objects[runTrimmedKey]; !ok {
t.Fatalf("run-local locked source output %q should still be uploaded", runTrimmedKey) 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 { if len(locked) != 1 {
t.Fatalf("locked_promotions = %#v, want one item", locked) t.Fatalf("locked_promotions = %#v, want one item", locked)
} }
if locked[0]["source"] != "narratio.transcript.trimmed" || if locked[0]["source"] != "narratio.transcript.final_trimmed" ||
locked[0]["dest"] != "transcripts/trimmed.json" || locked[0]["dest"] != "transcripts/final.trimmed.json" ||
locked[0]["remote_key"] != trimmedKey || locked[0]["remote_key"] != trimmedKey ||
locked[0]["reason"] != "human reviewed" || locked[0]["reason"] != "human reviewed" ||
locked[0]["required"] != true || locked[0]["required"] != true ||
@@ -388,10 +388,10 @@ func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) {
func TestArchiveLockedRequiredMissingPromotionSucceeds(t *testing.T) { func TestArchiveLockedRequiredMissingPromotionSucceeds(t *testing.T) {
env, m, _ := archiveFixture(t) env, m, _ := archiveFixture(t)
env.Config.Pipeline.Archive.PromoteArtifacts = []config.ArchivePromotionRule{ 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{ 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) result, err := archiveStage{}.Run(context.Background(), env, m)
@@ -400,7 +400,7 @@ func TestArchiveLockedRequiredMissingPromotionSucceeds(t *testing.T) {
} }
fake := env.ObjectStore.(*storage.FakeBackend) fake := env.ObjectStore.(*storage.FakeBackend)
mergedKey := m.S3SessionPrefix + "transcripts/merged.json" mergedKey := m.S3SessionPrefix + "transcripts/base.json"
if _, ok := fake.Objects[mergedKey]; ok { if _, ok := fake.Objects[mergedKey]; ok {
t.Fatalf("locked missing promotion key %q should not be uploaded", mergedKey) 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) { func TestArchiveLockDoesNotOverwriteExistingPromotion(t *testing.T) {
env, m, _ := archiveFixture(t) env, m, _ := archiveFixture(t)
env.Config.Pipeline.Archive.Locks = []config.ArchiveLockRule{ 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) 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")}) fake.SeedObject(storage.FakeObject{Key: trimmedKey, Data: []byte("previously published\n")})
if _, err := (archiveStage{}).Run(context.Background(), env, m); err != nil { if _, err := (archiveStage{}).Run(context.Background(), env, m); err != nil {
@@ -443,7 +443,7 @@ func TestArchiveLockDoesNotOverwriteExistingPromotion(t *testing.T) {
func TestArchiveFailsWhenRequiredPromotionMissing(t *testing.T) { func TestArchiveFailsWhenRequiredPromotionMissing(t *testing.T) {
env, m, _ := archiveFixture(t) env, m, _ := archiveFixture(t)
env.Config.Pipeline.Archive.PromoteArtifacts = []config.ArchivePromotionRule{ 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) _, err := archiveStage{}.Run(context.Background(), env, m)
@@ -470,7 +470,7 @@ func TestArchiveFailsWhenCanonicalRunRootMissing(t *testing.T) {
func TestArchiveDoesNotWriteCurrentPointerWhenPromotionUploadFails(t *testing.T) { func TestArchiveDoesNotWriteCurrentPointerWhenPromotionUploadFails(t *testing.T) {
env, m, _ := archiveFixture(t) env, m, _ := archiveFixture(t)
fake := env.ObjectStore.(*storage.FakeBackend) fake := env.ObjectStore.(*storage.FakeBackend)
trimmedKey := m.S3SessionPrefix + "transcripts/trimmed.json" trimmedKey := m.S3SessionPrefix + "transcripts/final.trimmed.json"
origUploadErr := fake.UploadErr origUploadErr := fake.UploadErr
fake.UploadErr = nil fake.UploadErr = nil
@@ -528,13 +528,13 @@ func archiveFixture(t *testing.T) (*Env, *manifest.Manifest, string) {
sessionRoot := artifacts.SessionWorkDirForCampaign(root, campaign, sessionID) sessionRoot := artifacts.SessionWorkDirForCampaign(root, campaign, sessionID)
runRoot := artifacts.SessionRunRootForCampaign(root, campaign, sessionID, runID) 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(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", "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, "prepare", "outputs", "audio", "speaker.flac"), "flac\n")
writeStageTestFile(t, filepath.Join(runRoot, "transcribe", "outputs", "transcripts", "raw", "speaker.json"), "{}\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, "analyze", "outputs", "artifacts", "session_recap.md"), "# recap\n")
writeStageTestFile(t, filepath.Join(runRoot, "polish", "reports", "audita.report.json"), "{}\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") 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), Enabled: boolPtr(true),
UploadRun: boolPtr(true), UploadRun: boolPtr(true),
PromoteArtifacts: []config.ArchivePromotionRule{ 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)}, {Source: "narratio.artifact.session_recap", Dest: "artifacts/session_recap.md", Required: boolPtr(true)},
}, },
}, },

View File

@@ -26,7 +26,7 @@ func (mergeStage) Declares() IODecl {
{Kind: "autocorrect", Category: "inputs", RelativePath: "inputs/autocorrect.yml"}, {Kind: "autocorrect", Category: "inputs", RelativePath: "inputs/autocorrect.yml"},
}, },
Outputs: []artifacts.Ref{ 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"}, {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) 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) mergedPath, err := runLocalPathForCanonical(runLayout, paths, canonicalMergedPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("merge: resolve run-local merged transcript path: %w", err) 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{ promotedMerged, err := promoteRunLocalOutput(env.ArtifactStore, finalMergedPath, canonicalMergedPath, artifacts.Ref{
Kind: "transcript_merged", Kind: "transcript_base",
Category: "transcripts", Category: "transcripts",
SessionID: sessionID, SessionID: sessionID,
}) })

View File

@@ -86,8 +86,8 @@ func TestMergeStageMergesRawTranscriptsAndRecordsMetadata(t *testing.T) {
if len(result.Outputs) != 2 { if len(result.Outputs) != 2 {
t.Fatalf("outputs len = %d, want 2", len(result.Outputs)) t.Fatalf("outputs len = %d, want 2", len(result.Outputs))
} }
if result.Outputs[0].Kind != "transcript_merged" { if result.Outputs[0].Kind != "transcript_base" {
t.Fatalf("output[0] kind = %q, want transcript_merged", result.Outputs[0].Kind) t.Fatalf("output[0] kind = %q, want transcript_base", result.Outputs[0].Kind)
} }
if result.Outputs[1].Kind != "seriatim_report" { if result.Outputs[1].Kind != "seriatim_report" {
t.Fatalf("output[1] kind = %q, want seriatim_report", result.Outputs[1].Kind) t.Fatalf("output[1] kind = %q, want seriatim_report", result.Outputs[1].Kind)

View File

@@ -20,10 +20,10 @@ func (normalizeStage) Name() string { return "normalize" }
func (normalizeStage) Declares() IODecl { func (normalizeStage) Declares() IODecl {
return IODecl{ return IODecl{
Inputs: []artifacts.Ref{ Inputs: []artifacts.Ref{
{Kind: "transcript_processed", Category: "transcripts", RelativePath: "transcripts/processed.json"}, {Kind: "transcript_polished", Category: "transcripts", RelativePath: "transcripts/polished.json"},
}, },
Outputs: []artifacts.Ref{ 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"}, {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{ promotedNormalized, err := promoteRunLocalOutput(env.ArtifactStore, finalNormalizedPath, canonicalNormalizedPath, artifacts.Ref{
Kind: "transcript_normalized", Kind: "transcript_final",
Category: "transcripts", Category: "transcripts",
SessionID: sessionID, SessionID: sessionID,
}) })
@@ -202,7 +202,7 @@ func normalizeConfigOrDefault(cfg *config.NormalizeConfig) *config.NormalizeConf
} }
report := true report := true
return &config.NormalizeConfig{ return &config.NormalizeConfig{
OutputPath: "transcripts/normalized.json", OutputPath: "transcripts/final.json",
OutputSchema: "seriatim-intermediate", OutputSchema: "seriatim-intermediate",
Report: &report, Report: &report,
} }
@@ -213,7 +213,7 @@ func discoverNormalizedTranscript(m *manifest.Manifest, paths artifacts.SessionP
if m != nil && m.Stages != nil { if m != nil && m.Stages != nil {
if sr := m.Stages["normalize"]; sr != nil { if sr := m.Stages["normalize"]; sr != nil {
for _, out := range sr.Outputs { for _, out := range sr.Outputs {
if out.Kind != "transcript_normalized" { if out.Kind != "transcript_final" {
continue continue
} }
p := strings.TrimSpace(out.LocalPath) 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() { if info, err := os.Stat(fallback); err == nil && !info.IsDir() {
return filepath.Clean(fallback), "fallback.transcripts_dir", nil return filepath.Clean(fallback), "fallback.transcripts_dir", nil
} }

View File

@@ -19,9 +19,9 @@ func TestNormalizeStageConsumesProcessedTranscriptFromManifest(t *testing.T) {
paths := sessionPathsForEnv(env, m.SessionID) paths := sessionPathsForEnv(env, m.SessionID)
manifestProcessed := filepath.Join(paths.ArtifactsDir, "processed.from-manifest.json") manifestProcessed := filepath.Join(paths.ArtifactsDir, "processed.from-manifest.json")
writeFile(t, manifestProcessed, `{"segments":[{"id":10}]}`) 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{{ m.MarkStageSucceeded("polish", time.Now().UTC(), []manifest.ArtifactRecord{{
Kind: "transcript_processed", Kind: "transcript_polished",
LocalPath: manifestProcessed, LocalPath: manifestProcessed,
}}) }})
@@ -44,7 +44,7 @@ func TestNormalizeStageConsumesProcessedTranscriptFromManifest(t *testing.T) {
func TestNormalizeStageFallsBackToProcessedTranscriptPath(t *testing.T) { func TestNormalizeStageFallsBackToProcessedTranscriptPath(t *testing.T) {
env, m, ser := setupNormalizeEnv(t) env, m, ser := setupNormalizeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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}]}`) writeFile(t, fallback, `{"segments":[{"id":1}]}`)
_, err := (normalizeStage{}).Run(context.Background(), env, m) _, err := (normalizeStage{}).Run(context.Background(), env, m)
@@ -73,7 +73,7 @@ func TestNormalizeStageFailsWhenProcessedTranscriptMissing(t *testing.T) {
func TestNormalizeStageFailsWhenProcessedTranscriptInvalidJSON(t *testing.T) { func TestNormalizeStageFailsWhenProcessedTranscriptInvalidJSON(t *testing.T) {
env, m, _ := setupNormalizeEnv(t) env, m, _ := setupNormalizeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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) _, err := (normalizeStage{}).Run(context.Background(), env, m)
if err == nil { if err == nil {
@@ -87,7 +87,7 @@ func TestNormalizeStageFailsWhenProcessedTranscriptInvalidJSON(t *testing.T) {
func TestNormalizeStageFailsWhenProcessedTranscriptMissingSegments(t *testing.T) { func TestNormalizeStageFailsWhenProcessedTranscriptMissingSegments(t *testing.T) {
env, m, _ := setupNormalizeEnv(t) env, m, _ := setupNormalizeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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) _, err := (normalizeStage{}).Run(context.Background(), env, m)
if err == nil { if err == nil {
@@ -101,7 +101,7 @@ func TestNormalizeStageFailsWhenProcessedTranscriptMissingSegments(t *testing.T)
func TestNormalizeStagePassesConfiguredOutputSchemaToAdapter(t *testing.T) { func TestNormalizeStagePassesConfiguredOutputSchemaToAdapter(t *testing.T) {
env, m, ser := setupNormalizeEnv(t) env, m, ser := setupNormalizeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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" env.Config.Pipeline.Normalize.OutputSchema = "seriatim-full"
result, err := (normalizeStage{}).Run(context.Background(), env, m) result, err := (normalizeStage{}).Run(context.Background(), env, m)
@@ -122,24 +122,24 @@ func TestNormalizeStagePassesConfiguredOutputSchemaToAdapter(t *testing.T) {
func TestNormalizeStageRecordsNormalizedTranscriptOutputKind(t *testing.T) { func TestNormalizeStageRecordsNormalizedTranscriptOutputKind(t *testing.T) {
env, m, _ := setupNormalizeEnv(t) env, m, _ := setupNormalizeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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) result, err := (normalizeStage{}).Run(context.Background(), env, m)
if err != nil { if err != nil {
t.Fatalf("normalize.Run() error = %v", err) t.Fatalf("normalize.Run() error = %v", err)
} }
if len(result.Outputs) == 0 { 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" { if result.Outputs[0].Kind != "transcript_final" {
t.Fatalf("outputs[0].kind = %q, want transcript_normalized", result.Outputs[0].Kind) t.Fatalf("outputs[0].kind = %q, want transcript_final", result.Outputs[0].Kind)
} }
} }
func TestNormalizeStageRecordsReportLogAndGeneratedConfigRefs(t *testing.T) { func TestNormalizeStageRecordsReportLogAndGeneratedConfigRefs(t *testing.T) {
env, m, _ := setupNormalizeEnv(t) env, m, _ := setupNormalizeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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 report := true
env.Config.Pipeline.Normalize.Report = &report env.Config.Pipeline.Normalize.Report = &report
@@ -148,7 +148,7 @@ func TestNormalizeStageRecordsReportLogAndGeneratedConfigRefs(t *testing.T) {
t.Fatalf("normalize.Run() error = %v", err) t.Fatalf("normalize.Run() error = %v", err)
} }
if len(result.Outputs) < 2 { 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" { if result.Outputs[1].Kind != "seriatim_normalize_report" {
t.Fatalf("outputs[1].kind = %q, want seriatim_normalize_report", result.Outputs[1].Kind) 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) { func TestNormalizeStageFailsWhenAdapterReturnsError(t *testing.T) {
env, m, ser := setupNormalizeEnv(t) env, m, ser := setupNormalizeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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") ser.NormalizeErr = errors.New("normalize failed")
_, err := (normalizeStage{}).Run(context.Background(), env, m) _, err := (normalizeStage{}).Run(context.Background(), env, m)
@@ -180,7 +180,7 @@ func TestNormalizeStageFailsWhenAdapterReturnsError(t *testing.T) {
func TestNormalizeStageFailsWhenNormalizedOutputInvalid(t *testing.T) { func TestNormalizeStageFailsWhenNormalizedOutputInvalid(t *testing.T) {
env, m, ser := setupNormalizeEnv(t) env, m, ser := setupNormalizeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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") badOutput := filepath.Join(paths.TranscriptsDir, "normalized.bad.json")
writeFile(t, badOutput, "not-json") writeFile(t, badOutput, "not-json")
ser.NormalizeResult = seriatim.NormalizeResult{OutputNormalizedPath: badOutput} ser.NormalizeResult = seriatim.NormalizeResult{OutputNormalizedPath: badOutput}
@@ -197,7 +197,7 @@ func TestNormalizeStageFailsWhenNormalizedOutputInvalid(t *testing.T) {
func TestNormalizeStageReportEnabledFailsWhenReportMissing(t *testing.T) { func TestNormalizeStageReportEnabledFailsWhenReportMissing(t *testing.T) {
env, m, ser := setupNormalizeEnv(t) env, m, ser := setupNormalizeEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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 report := true
env.Config.Pipeline.Normalize.Report = &report env.Config.Pipeline.Normalize.Report = &report
ser.NormalizeResult = seriatim.NormalizeResult{ReportPath: filepath.Join(paths.ArtifactsDir, "missing-report.json")} 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.Campaign = "sample-campaign"
m.RunID = "20260518T010203Z-abcdef12" m.RunID = "20260518T010203Z-abcdef12"
paths := sessionPathsForEnv(env, m.SessionID) 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) result, err := (normalizeStage{}).Run(context.Background(), env, m)
if err != nil { if err != nil {
@@ -261,7 +261,7 @@ func setupNormalizeEnv(t *testing.T) (*Env, *manifest.Manifest, *seriatim.FakeRu
Report: &report, Report: &report,
}, },
Normalize: &config.NormalizeConfig{ Normalize: &config.NormalizeConfig{
OutputPath: "transcripts/normalized.json", OutputPath: "transcripts/final.json",
OutputSchema: "seriatim-intermediate", OutputSchema: "seriatim-intermediate",
Report: &report, Report: &report,
}, },

View File

@@ -163,8 +163,8 @@ func TestStagesReturnExpectedMetadata(t *testing.T) {
if result.Metadata["output_schema"] == nil { if result.Metadata["output_schema"] == nil {
t.Fatalf("normalize metadata = %#v, want output_schema metadata", result.Metadata) t.Fatalf("normalize metadata = %#v, want output_schema metadata", result.Metadata)
} }
if len(result.Outputs) == 0 || result.Outputs[0].Kind != "transcript_normalized" { if len(result.Outputs) == 0 || result.Outputs[0].Kind != "transcript_final" {
t.Fatalf("normalize outputs = %#v, want transcript_normalized output", result.Outputs) t.Fatalf("normalize outputs = %#v, want transcript_final output", result.Outputs)
} }
if len(result.Logs) != 2 { if len(result.Logs) != 2 {
t.Fatalf("normalize logs = %#v, want stdout+stderr", result.Logs) 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" { if result.Metadata["trim_action"] != "copy_disabled" {
t.Fatalf("trim metadata = %#v, want trim_action=copy_disabled", result.Metadata) t.Fatalf("trim metadata = %#v, want trim_action=copy_disabled", result.Metadata)
} }
if len(result.Outputs) == 0 || result.Outputs[0].Kind != "transcript_trimmed" { if len(result.Outputs) == 0 || result.Outputs[0].Kind != "transcript_final_trimmed" {
t.Fatalf("trim outputs = %#v, want transcript_trimmed output", result.Outputs) t.Fatalf("trim outputs = %#v, want transcript_final_trimmed output", result.Outputs)
} }
continue continue
} }

View File

@@ -20,11 +20,11 @@ func (polishStage) Name() string { return "polish" }
func (polishStage) Declares() IODecl { func (polishStage) Declares() IODecl {
return IODecl{ return IODecl{
Inputs: []artifacts.Ref{ 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"}, {Kind: "glossary", Category: "inputs", RelativePath: "inputs/glossary.yml"},
}, },
Outputs: []artifacts.Ref{ 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"}, {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) 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) processedPath, err := runLocalPathForCanonical(runLayout, paths, canonicalProcessedPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("polish: resolve run-local processed transcript path: %w", err) 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{ promotedProcessed, err := promoteRunLocalOutput(env.ArtifactStore, finalProcessedPath, canonicalProcessedPath, artifacts.Ref{
Kind: "transcript_processed", Kind: "transcript_polished",
Category: "transcripts", Category: "transcripts",
SessionID: sessionID, SessionID: sessionID,
}) })
@@ -250,7 +250,7 @@ func discoverMergedTranscript(m *manifest.Manifest, paths artifacts.SessionPaths
if m != nil && m.Stages != nil { if m != nil && m.Stages != nil {
if sr := m.Stages["merge"]; sr != nil { if sr := m.Stages["merge"]; sr != nil {
for _, out := range sr.Outputs { for _, out := range sr.Outputs {
if out.Kind != "transcript_merged" { if out.Kind != "transcript_base" {
continue continue
} }
p := strings.TrimSpace(out.LocalPath) 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() { if info, err := os.Stat(fallback); err == nil && !info.IsDir() {
return filepath.Clean(fallback), "fallback.transcripts_dir", nil return filepath.Clean(fallback), "fallback.transcripts_dir", nil
} }

View File

@@ -18,11 +18,11 @@ func TestPolishStagePolishesMergedTranscriptAndRecordsMetadata(t *testing.T) {
env, m := setupPolishEnv(t) env, m := setupPolishEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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, mergedPath, `{"segments":[]}`)
writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n") writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n")
m.MarkStageSucceeded("merge", time.Now().UTC(), []manifest.ArtifactRecord{ m.MarkStageSucceeded("merge", time.Now().UTC(), []manifest.ArtifactRecord{
{Kind: "transcript_merged", LocalPath: mergedPath}, {Kind: "transcript_base", LocalPath: mergedPath},
}) })
fake := &audita.FakeRunner{} fake := &audita.FakeRunner{}
@@ -85,8 +85,8 @@ func TestPolishStagePolishesMergedTranscriptAndRecordsMetadata(t *testing.T) {
if len(result.Outputs) != 2 { if len(result.Outputs) != 2 {
t.Fatalf("outputs len = %d, want 2", len(result.Outputs)) t.Fatalf("outputs len = %d, want 2", len(result.Outputs))
} }
if result.Outputs[0].Kind != "transcript_processed" { if result.Outputs[0].Kind != "transcript_polished" {
t.Fatalf("output[0] kind = %q, want transcript_processed", result.Outputs[0].Kind) t.Fatalf("output[0] kind = %q, want transcript_polished", result.Outputs[0].Kind)
} }
if result.Outputs[1].Kind != "audita_report" { if result.Outputs[1].Kind != "audita_report" {
t.Fatalf("output[1] kind = %q, want audita_report", result.Outputs[1].Kind) 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) { func TestPolishStageFallsBackToMergedTranscriptPath(t *testing.T) {
env, m := setupPolishEnv(t) env, m := setupPolishEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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, mergedPath, `{"segments":[]}`)
writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n") writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n")
@@ -158,7 +158,7 @@ func TestPolishStageFailsWhenMergedTranscriptMissing(t *testing.T) {
func TestPolishStageFailsWhenMergedTranscriptInvalidJSON(t *testing.T) { func TestPolishStageFailsWhenMergedTranscriptInvalidJSON(t *testing.T) {
env, m := setupPolishEnv(t) env, m := setupPolishEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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") writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n")
env.Audita = &audita.FakeRunner{} env.Audita = &audita.FakeRunner{}
@@ -174,7 +174,7 @@ func TestPolishStageFailsWhenMergedTranscriptInvalidJSON(t *testing.T) {
func TestPolishStageFailsWhenGlossaryMissing(t *testing.T) { func TestPolishStageFailsWhenGlossaryMissing(t *testing.T) {
env, m := setupPolishEnv(t) env, m := setupPolishEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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{} env.Audita = &audita.FakeRunner{}
_, err := (polishStage{}).Run(context.Background(), env, m) _, err := (polishStage{}).Run(context.Background(), env, m)
@@ -189,7 +189,7 @@ func TestPolishStageFailsWhenGlossaryMissing(t *testing.T) {
func TestPolishStageFailsWhenAdapterFails(t *testing.T) { func TestPolishStageFailsWhenAdapterFails(t *testing.T) {
env, m := setupPolishEnv(t) env, m := setupPolishEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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") writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n")
env.Audita = &audita.FakeRunner{Err: errors.New("audita failed")} env.Audita = &audita.FakeRunner{Err: errors.New("audita failed")}
@@ -205,7 +205,7 @@ func TestPolishStageFailsWhenAdapterFails(t *testing.T) {
func TestPolishStageFailsWhenProcessedOutputInvalid(t *testing.T) { func TestPolishStageFailsWhenProcessedOutputInvalid(t *testing.T) {
env, m := setupPolishEnv(t) env, m := setupPolishEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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") writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n")
badPath := filepath.Join(paths.TranscriptsDir, "processed.invalid.json") badPath := filepath.Join(paths.TranscriptsDir, "processed.invalid.json")
writeFile(t, badPath, `{"schema":"audita.processed.v1","segments":"wrong-type"}`) writeFile(t, badPath, `{"schema":"audita.processed.v1","segments":"wrong-type"}`)
@@ -223,7 +223,7 @@ func TestPolishStageFailsWhenProcessedOutputInvalid(t *testing.T) {
func TestPolishStageFailsWhenReportInvalid(t *testing.T) { func TestPolishStageFailsWhenReportInvalid(t *testing.T) {
env, m := setupPolishEnv(t) env, m := setupPolishEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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") writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n")
badReport := filepath.Join(paths.ArtifactsDir, "bad.report.json") badReport := filepath.Join(paths.ArtifactsDir, "bad.report.json")
writeFile(t, badReport, "not-json") writeFile(t, badReport, "not-json")
@@ -244,7 +244,7 @@ func TestPolishStageUsesRunLocalPathsAndPromotesCanonical(t *testing.T) {
m.Campaign = "sample-campaign" m.Campaign = "sample-campaign"
m.RunID = "20260518T010203Z-abcdef12" m.RunID = "20260518T010203Z-abcdef12"
paths := sessionPathsForEnv(env, m.SessionID) 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") writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n")
fake := &audita.FakeRunner{} fake := &audita.FakeRunner{}

View File

@@ -19,13 +19,13 @@ func TestRunLocalPathForCanonicalCreatesParentDirectories(t *testing.T) {
t.Fatalf("mkdir outputs dir: %v", err) 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) got, err := runLocalPathForCanonical(layout, artifacts.SessionPaths{Root: sessionRoot}, canonical)
if err != nil { if err != nil {
t.Fatalf("runLocalPathForCanonical() error = %v", err) 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 { if got != want {
t.Fatalf("runLocalPathForCanonical() = %q, want %q", got, want) t.Fatalf("runLocalPathForCanonical() = %q, want %q", got, want)
} }

View File

@@ -23,10 +23,10 @@ func (trimStage) Name() string { return "trim" }
func (trimStage) Declares() IODecl { func (trimStage) Declares() IODecl {
return IODecl{ return IODecl{
Inputs: []artifacts.Ref{ Inputs: []artifacts.Ref{
{Kind: "transcript_normalized", Category: "transcripts", RelativePath: "transcripts/normalized.json"}, {Kind: "transcript_final", Category: "transcripts", RelativePath: "transcripts/final.json"},
}, },
Outputs: []artifacts.Ref{ 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"}, {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) return nil, fmt.Errorf("trim: copied trimmed transcript %q invalid: %w", trimmedPath, err)
} }
promotedTrimmed, err := promoteRunLocalOutput(env.ArtifactStore, trimmedPath, canonicalTrimmedPath, artifacts.Ref{ promotedTrimmed, err := promoteRunLocalOutput(env.ArtifactStore, trimmedPath, canonicalTrimmedPath, artifacts.Ref{
Kind: "transcript_trimmed", Kind: "transcript_final_trimmed",
Category: "transcripts", Category: "transcripts",
SessionID: sessionID, 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{ promotedTrimmed, err := promoteRunLocalOutput(env.ArtifactStore, trimmedPath, canonicalTrimmedPath, artifacts.Ref{
Kind: "transcript_trimmed", Kind: "transcript_final_trimmed",
Category: "transcripts", Category: "transcripts",
SessionID: sessionID, SessionID: sessionID,
}) })
@@ -382,7 +382,7 @@ func resolveTrimmedOutputPath(paths artifacts.SessionPaths, cfg *config.TrimConf
configured = strings.TrimSpace(cfg.OutputPath) configured = strings.TrimSpace(cfg.OutputPath)
} }
if configured == "" { if configured == "" {
return filepath.Join(paths.TranscriptsDir, "trimmed.json"), nil return filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), nil
} }
return resolveScriptoriumOutputPath(paths, configured) return resolveScriptoriumOutputPath(paths, configured)
} }

View File

@@ -19,10 +19,10 @@ import (
func TestTrimStageConsumesNormalizedAndProducesTrimmedTranscript(t *testing.T) { func TestTrimStageConsumesNormalizedAndProducesTrimmedTranscript(t *testing.T) {
env, m, scr, ser := setupTrimEnv(t) env, m, scr, ser := setupTrimEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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}]}`) writeFile(t, normalized, `{"segments":[{"id":10},{"id":868}]}`)
m.MarkStageSucceeded("normalize", time.Now().UTC(), []manifest.ArtifactRecord{ 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":[]}` 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" { if trimReq.KeepSelector != "10-868" {
t.Fatalf("keep selector = %q, want %q", 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) t.Fatalf("trim output = %q", trimReq.OutputTrimmedPath)
} }
@@ -54,13 +54,13 @@ func TestTrimStageConsumesNormalizedAndProducesTrimmedTranscript(t *testing.T) {
if len(result.Outputs) < 2 { if len(result.Outputs) < 2 {
t.Fatalf("outputs = %#v, want trimmed+bounds outputs", result.Outputs) t.Fatalf("outputs = %#v, want trimmed+bounds outputs", result.Outputs)
} }
if result.Outputs[0].Kind != "transcript_trimmed" { if result.Outputs[0].Kind != "transcript_final_trimmed" {
t.Fatalf("output[0] kind = %q, want transcript_trimmed", result.Outputs[0].Kind) t.Fatalf("output[0] kind = %q, want transcript_final_trimmed", result.Outputs[0].Kind)
} }
if result.Outputs[1].Kind != "session_bounds" { if result.Outputs[1].Kind != "session_bounds" {
t.Fatalf("output[1] kind = %q, want session_bounds", result.Outputs[1].Kind) 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"]) t.Fatalf("metadata trimmed_output_path = %#v", result.Metadata["trimmed_output_path"])
} }
if result.Metadata["keep_selector"] != "10-868" { if result.Metadata["keep_selector"] != "10-868" {
@@ -71,7 +71,7 @@ func TestTrimStageConsumesNormalizedAndProducesTrimmedTranscript(t *testing.T) {
func TestTrimStageUsesConfiguredScriptoriumInputName(t *testing.T) { func TestTrimStageUsesConfiguredScriptoriumInputName(t *testing.T) {
env, m, scr, _ := setupTrimEnv(t) env, m, scr, _ := setupTrimEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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}]}`) writeFile(t, normalized, `{"segments":[{"id":10},{"id":11}]}`)
scr.BoundsBody = `{"trim_action":"trim","start_segment_id":10,"end_segment_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) { func TestTrimStageRecordsLogAndGeneratedConfigRefs(t *testing.T) {
env, m, scr, _ := setupTrimEnv(t) env, m, scr, _ := setupTrimEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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}` scr.BoundsBody = `{"trim_action":"trim","start_segment_id":1,"end_segment_id":2}`
result, err := (trimStage{}).Run(context.Background(), env, m) result, err := (trimStage{}).Run(context.Background(), env, m)
@@ -115,7 +115,7 @@ func TestTrimStageRecordsLogAndGeneratedConfigRefs(t *testing.T) {
func TestTrimStageRenderDebugDiagnosticsAreNotStageOutputs(t *testing.T) { func TestTrimStageRenderDebugDiagnosticsAreNotStageOutputs(t *testing.T) {
env, m, scr, _ := setupTrimEnv(t) env, m, scr, _ := setupTrimEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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}` scr.BoundsBody = `{"trim_action":"trim","start_segment_id":1,"end_segment_id":2}`
cfg := *env.Config.Pipeline.Trim cfg := *env.Config.Pipeline.Trim
@@ -153,7 +153,7 @@ func TestTrimStageFailsWhenNormalizedTranscriptMissing(t *testing.T) {
func TestTrimStageDoesNotFallBackToProcessedTranscript(t *testing.T) { func TestTrimStageDoesNotFallBackToProcessedTranscript(t *testing.T) {
env, m, scr, ser := setupTrimEnv(t) env, m, scr, ser := setupTrimEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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) _, err := (trimStage{}).Run(context.Background(), env, m)
if err == nil { if err == nil {
@@ -173,7 +173,7 @@ func TestTrimStageDoesNotFallBackToProcessedTranscript(t *testing.T) {
func TestTrimStageFailsWhenNormalizedTranscriptInvalidJSON(t *testing.T) { func TestTrimStageFailsWhenNormalizedTranscriptInvalidJSON(t *testing.T) {
env, m, _, _ := setupTrimEnv(t) env, m, _, _ := setupTrimEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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) _, err := (trimStage{}).Run(context.Background(), env, m)
if err == nil { if err == nil {
t.Fatal("expected error, got nil") t.Fatal("expected error, got nil")
@@ -186,7 +186,7 @@ func TestTrimStageFailsWhenNormalizedTranscriptInvalidJSON(t *testing.T) {
func TestTrimStageFailsWhenNormalizedTranscriptMissingSegmentsArray(t *testing.T) { func TestTrimStageFailsWhenNormalizedTranscriptMissingSegmentsArray(t *testing.T) {
env, m, _, _ := setupTrimEnv(t) env, m, _, _ := setupTrimEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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) _, err := (trimStage{}).Run(context.Background(), env, m)
if err == nil { if err == nil {
t.Fatal("expected error, got nil") t.Fatal("expected error, got nil")
@@ -199,7 +199,7 @@ func TestTrimStageFailsWhenNormalizedTranscriptMissingSegmentsArray(t *testing.T
func TestTrimStageFailsWhenBoundsOutputInvalidJSON(t *testing.T) { func TestTrimStageFailsWhenBoundsOutputInvalidJSON(t *testing.T) {
env, m, scr, _ := setupTrimEnv(t) env, m, scr, _ := setupTrimEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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" scr.BoundsBody = "not-json"
_, err := (trimStage{}).Run(context.Background(), env, m) _, err := (trimStage{}).Run(context.Background(), env, m)
if err == nil { if err == nil {
@@ -213,7 +213,7 @@ func TestTrimStageFailsWhenBoundsOutputInvalidJSON(t *testing.T) {
func TestTrimStageFailsWhenBoundsRangeIsDescending(t *testing.T) { func TestTrimStageFailsWhenBoundsRangeIsDescending(t *testing.T) {
env, m, scr, _ := setupTrimEnv(t) env, m, scr, _ := setupTrimEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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}` scr.BoundsBody = `{"trim_action":"trim","start_segment_id":2,"end_segment_id":1}`
_, err := (trimStage{}).Run(context.Background(), env, m) _, err := (trimStage{}).Run(context.Background(), env, m)
if err == nil { if err == nil {
@@ -227,7 +227,7 @@ func TestTrimStageFailsWhenBoundsRangeIsDescending(t *testing.T) {
func TestTrimStageFailsWhenBoundsIDsMissingFromTranscript(t *testing.T) { func TestTrimStageFailsWhenBoundsIDsMissingFromTranscript(t *testing.T) {
env, m, scr, _ := setupTrimEnv(t) env, m, scr, _ := setupTrimEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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}` scr.BoundsBody = `{"trim_action":"trim","start_segment_id":10,"end_segment_id":21}`
_, err := (trimStage{}).Run(context.Background(), env, m) _, err := (trimStage{}).Run(context.Background(), env, m)
if err == nil { if err == nil {
@@ -241,7 +241,7 @@ func TestTrimStageFailsWhenBoundsIDsMissingFromTranscript(t *testing.T) {
func TestTrimStageFailsWhenScriptoriumAdapterFails(t *testing.T) { func TestTrimStageFailsWhenScriptoriumAdapterFails(t *testing.T) {
env, m, scr, _ := setupTrimEnv(t) env, m, scr, _ := setupTrimEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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") scr.RunErr = errors.New("bounds failed")
_, err := (trimStage{}).Run(context.Background(), env, m) _, err := (trimStage{}).Run(context.Background(), env, m)
if err == nil { if err == nil {
@@ -255,7 +255,7 @@ func TestTrimStageFailsWhenScriptoriumAdapterFails(t *testing.T) {
func TestTrimStageFailsWhenSeriatimTrimAdapterFails(t *testing.T) { func TestTrimStageFailsWhenSeriatimTrimAdapterFails(t *testing.T) {
env, m, scr, ser := setupTrimEnv(t) env, m, scr, ser := setupTrimEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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}` scr.BoundsBody = `{"trim_action":"trim","start_segment_id":1,"end_segment_id":2}`
ser.TrimErr = errors.New("trim failed") ser.TrimErr = errors.New("trim failed")
_, err := (trimStage{}).Run(context.Background(), env, m) _, err := (trimStage{}).Run(context.Background(), env, m)
@@ -270,7 +270,7 @@ func TestTrimStageFailsWhenSeriatimTrimAdapterFails(t *testing.T) {
func TestTrimStageDisabledCopiesNormalizedTranscript(t *testing.T) { func TestTrimStageDisabledCopiesNormalizedTranscript(t *testing.T) {
env, m, scr, ser := setupTrimEnv(t) env, m, scr, ser := setupTrimEnv(t)
paths := sessionPathsForEnv(env, m.SessionID) 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"}]}` normalizedBody := `{"segments":[{"id":1,"text":"alpha"},{"id":2,"text":"beta"}]}`
writeFile(t, normalized, normalizedBody) writeFile(t, normalized, normalizedBody)
@@ -288,7 +288,7 @@ func TestTrimStageDisabledCopiesNormalizedTranscript(t *testing.T) {
if result.Metadata["trim_action"] != "copy_disabled" { if result.Metadata["trim_action"] != "copy_disabled" {
t.Fatalf("metadata trim_action = %#v, want copy_disabled", result.Metadata["trim_action"]) 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) data, err := os.ReadFile(trimmedPath)
if err != nil { if err != nil {
t.Fatalf("read trimmed transcript: %v", err) t.Fatalf("read trimmed transcript: %v", err)
@@ -304,7 +304,7 @@ func TestTrimStageUsesRunLocalPathsAndPromotesCanonical(t *testing.T) {
m.Campaign = "sample-campaign" m.Campaign = "sample-campaign"
m.RunID = "20260518T010203Z-abcdef12" m.RunID = "20260518T010203Z-abcdef12"
paths := sessionPathsForEnv(env, m.SessionID) 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}]}`) writeFile(t, normalized, `{"segments":[{"id":10},{"id":11}]}`)
scr.BoundsBody = `{"trim_action":"trim","start_segment_id":10,"end_segment_id":11,"warnings":[]}` 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{ Trim: &config.TrimConfig{
Enabled: true, Enabled: true,
OutputPath: "transcripts/trimmed.json", OutputPath: "transcripts/final.trimmed.json",
Bounds: config.TrimBoundsConfig{ Bounds: config.TrimBoundsConfig{
PromptID: "dnd_session.bounds", PromptID: "dnd_session.bounds",
ProfileID: "", ProfileID: "",