Updated transcript artifact names and canonical paths to use a consistent, role-based nomenclature
This commit is contained in:
@@ -361,8 +361,8 @@ Examples:
|
||||
|
||||
```bash
|
||||
narratio locks --session-id 2026-04-04
|
||||
narratio locks add --session-id 2026-04-04 --reason "manual transcript review" narratio.transcript.trimmed
|
||||
narratio locks remove --session-id 2026-04-04 narratio.transcript.trimmed
|
||||
narratio locks add --session-id 2026-04-04 --reason "manual transcript review" narratio.transcript.final_trimmed
|
||||
narratio locks remove --session-id 2026-04-04 narratio.transcript.final_trimmed
|
||||
```
|
||||
|
||||
### `run-stage`
|
||||
|
||||
@@ -185,8 +185,8 @@ archive:
|
||||
enabled: true
|
||||
upload_run: true
|
||||
promote_artifacts:
|
||||
- source: narratio.transcript.trimmed
|
||||
dest: transcripts/trimmed.json
|
||||
- source: narratio.transcript.final_trimmed
|
||||
dest: transcripts/final.trimmed.json
|
||||
required: true
|
||||
- source: narratio.artifact.session_recap
|
||||
dest: artifacts/session_recap.md
|
||||
@@ -206,7 +206,7 @@ scriptorium:
|
||||
output_path: artifacts/session_recap.md
|
||||
inputs:
|
||||
transcript:
|
||||
source: narratio.transcript.trimmed
|
||||
source: narratio.transcript.final_trimmed
|
||||
required: true
|
||||
previous_recap:
|
||||
source: narratio.previous_session.artifact.session_recap
|
||||
@@ -280,7 +280,7 @@ Operational notes:
|
||||
| `pipeline.cache.s3_audio` | bool | No | `true` |
|
||||
| `pipeline.archive.enabled` | bool | No | `true` |
|
||||
| `pipeline.archive.upload_run` | bool | No | `true` |
|
||||
| `pipeline.archive.promote_artifacts[]` | list | No | trimmed transcript rule |
|
||||
| `pipeline.archive.promote_artifacts[]` | list | No | final-trimmed transcript rule |
|
||||
| `pipeline.archive.promote_artifacts[].source` | string | Yes (per rule) | none |
|
||||
| `pipeline.archive.promote_artifacts[].dest` | string | No | derived from source |
|
||||
| `pipeline.archive.promote_artifacts[].required` | bool | No | `true` |
|
||||
@@ -317,7 +317,7 @@ Operational notes:
|
||||
| `pipeline.audita.output_schema` | string | No | empty |
|
||||
| `pipeline.audita.work_dir_retention` | string | No | empty |
|
||||
| `pipeline.audita.report` | bool | No | `true` |
|
||||
| `pipeline.normalize.output_path` | string | No | `transcripts/normalized.json` |
|
||||
| `pipeline.normalize.output_path` | string | No | `transcripts/final.json` |
|
||||
| `pipeline.normalize.output_schema` | string | No | `seriatim-intermediate` |
|
||||
| `pipeline.normalize.report` | bool | No | `true` |
|
||||
| `pipeline.trim.enabled` | bool | No | `false` |
|
||||
@@ -366,19 +366,19 @@ Scriptorium artifact-key and dependency rules:
|
||||
Allowed `pipeline.scriptorium.artifacts.<name>.inputs.<key>.source` values:
|
||||
|
||||
- `narratio.previous_session.artifact.<configured_artifact_key>`
|
||||
- `narratio.transcript.merged`
|
||||
- `narratio.transcript.base`
|
||||
- `narratio.transcript.polished`
|
||||
- `narratio.transcript.full`
|
||||
- `narratio.transcript.trimmed`
|
||||
- `narratio.transcript.final`
|
||||
- `narratio.transcript.final_trimmed`
|
||||
- `narratio.bounds.session`
|
||||
- `narratio.artifact.<configured_artifact_key>`
|
||||
|
||||
`pipeline.archive.promote_artifacts[].source` values:
|
||||
|
||||
- `narratio.transcript.merged`
|
||||
- `narratio.transcript.base`
|
||||
- `narratio.transcript.polished`
|
||||
- `narratio.transcript.full`
|
||||
- `narratio.transcript.trimmed`
|
||||
- `narratio.transcript.final`
|
||||
- `narratio.transcript.final_trimmed`
|
||||
- `narratio.bounds.session`
|
||||
- `narratio.artifact.<configured_artifact_key>`
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ Define Narratio's adapter contract for transcript polishing via Audita CLI subpr
|
||||
|
||||
## Inputs and Outputs
|
||||
Inputs (`audita.PolishRequest`):
|
||||
- merged transcript path
|
||||
- base transcript path
|
||||
- glossary path
|
||||
- output processed transcript path
|
||||
- output polished transcript path
|
||||
- optional report path (required when report enabled)
|
||||
- work dir
|
||||
- generated config path
|
||||
@@ -15,7 +15,7 @@ Inputs (`audita.PolishRequest`):
|
||||
- optional module/model/base URL and concurrency knobs
|
||||
|
||||
Outputs (`audita.PolishResult`):
|
||||
- processed transcript path
|
||||
- polished transcript path
|
||||
- optional report path
|
||||
- generated config path
|
||||
- stdout/stderr log paths
|
||||
@@ -27,7 +27,7 @@ Owns:
|
||||
- Deterministic CLI argument construction for `audita process`
|
||||
- Environment bridging for API credentials
|
||||
- Invocation config emission
|
||||
- Output validation for processed transcript and report
|
||||
- Output validation for polished transcript and report
|
||||
|
||||
Does not own:
|
||||
- Upstream/downstream stage orchestration
|
||||
@@ -52,7 +52,7 @@ Via `pipeline.audita.*` mapped in app/stage wiring:
|
||||
|
||||
## Failure Behavior
|
||||
- Constructor validation fails on invalid binary/timeout/schema/concurrency/URL values.
|
||||
- Run fails on missing required paths, missing required credential env var, subprocess errors, invalid processed JSON shape, or invalid report JSON.
|
||||
- Run fails on missing required paths, missing required credential env var, subprocess errors, invalid polished JSON shape, or invalid report JSON.
|
||||
- Failures preserve stdout/stderr paths in returned result metadata.
|
||||
|
||||
## Tests to Inspect Before Changing
|
||||
@@ -61,6 +61,6 @@ Via `pipeline.audita.*` mapped in app/stage wiring:
|
||||
- `internal/stage/polish_test.go`
|
||||
|
||||
## Architectural Invariants
|
||||
- Processed output must be valid JSON with top-level `segments` array.
|
||||
- Polished output must be valid JSON with top-level `segments` array.
|
||||
- When report is enabled, report output must be valid JSON.
|
||||
- If `llm_api_key_env` is configured, credential must be present in environment.
|
||||
|
||||
@@ -5,7 +5,7 @@ Define Narratio's adapter contract for merge, normalize, and trim subprocess inv
|
||||
|
||||
## Inputs and Outputs
|
||||
Inputs:
|
||||
- `MergeRequest`: raw/normalized transcript inputs, output path, optional report, speaker/autocorrect paths, logs/config
|
||||
- `MergeRequest`: raw/per-speaker normalized transcript inputs, base output path, optional report, speaker/autocorrect paths, logs/config
|
||||
- `NormalizeRequest`: input transcript, output path, schema, optional report, timeout/log/config
|
||||
- `TrimRequest`: input transcript, output path, keep selector, timeout/log/config
|
||||
|
||||
@@ -44,8 +44,8 @@ Via `pipeline.seriatim.*` mapped in app/stage wiring:
|
||||
## Failure Behavior
|
||||
- Constructor fails for invalid binary/timeout/output-schema/coalesce-gap.
|
||||
- Merge fails on missing output path/inputs/report path (if enabled), subprocess errors, invalid merged output JSON, invalid report JSON.
|
||||
- Normalize fails on missing input/output, invalid schema, subprocess errors, invalid normalized output JSON shape, invalid report JSON.
|
||||
- Trim fails on missing input/output/keep selector, subprocess errors, invalid trimmed output JSON shape.
|
||||
- Normalize fails on missing input/output, invalid schema, subprocess errors, invalid final output JSON shape, invalid report JSON.
|
||||
- Trim fails on missing input/output/keep selector, subprocess errors, invalid final-trimmed output JSON shape.
|
||||
|
||||
## Tests to Inspect Before Changing
|
||||
- `internal/adapters/seriatim/subprocess_test.go`
|
||||
@@ -56,5 +56,5 @@ Via `pipeline.seriatim.*` mapped in app/stage wiring:
|
||||
|
||||
## Architectural Invariants
|
||||
- Supported output schemas are limited to `seriatim-minimal`, `seriatim-intermediate`, `seriatim-full`.
|
||||
- Normalize/trim outputs must include `segments` arrays.
|
||||
- Final and final-trimmed outputs must include `segments` arrays.
|
||||
- Merge/normalize/trim all route through deterministic subprocess invocation.
|
||||
|
||||
@@ -33,10 +33,10 @@ Does not own:
|
||||
## Built-in IDs
|
||||
| Artifact ID | Canonical file | Producer stage | Output kind |
|
||||
| --- | --- | --- | --- |
|
||||
| `narratio.transcript.merged` | `transcripts/merged.json` | `merge` | `transcript_merged` |
|
||||
| `narratio.transcript.polished` | `transcripts/processed.json` | `polish` | `transcript_processed` |
|
||||
| `narratio.transcript.full` | `transcripts/normalized.json` | `normalize` | `transcript_normalized` |
|
||||
| `narratio.transcript.trimmed` | `transcripts/trimmed.json` | `trim` | `transcript_trimmed` |
|
||||
| `narratio.transcript.base` | `transcripts/base.json` | `merge` | `transcript_base` |
|
||||
| `narratio.transcript.polished` | `transcripts/polished.json` | `polish` | `transcript_polished` |
|
||||
| `narratio.transcript.final` | `transcripts/final.json` | `normalize` | `transcript_final` |
|
||||
| `narratio.transcript.final_trimmed` | `transcripts/final.trimmed.json` | `trim` | `transcript_final_trimmed` |
|
||||
| `narratio.bounds.session` | `artifacts/session_bounds.json` | `trim` | `session_bounds` |
|
||||
|
||||
## Source families
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Stage: merge
|
||||
|
||||
## Purpose
|
||||
Normalize per-speaker raw transcripts and merge them into one merged transcript via Seriatim.
|
||||
Normalize per-speaker raw transcripts and merge them into the base transcript via Seriatim.
|
||||
|
||||
## Inputs and Outputs
|
||||
Inputs:
|
||||
@@ -10,7 +10,7 @@ Inputs:
|
||||
- `inputs/autocorrect.yml`
|
||||
|
||||
Outputs:
|
||||
- `transcripts/merged.json`
|
||||
- `transcripts/base.json`
|
||||
- optional `artifacts/seriatim.report.json` (when report enabled)
|
||||
|
||||
## Boundaries
|
||||
@@ -19,7 +19,7 @@ Owns:
|
||||
- Per-input normalize calls to Seriatim
|
||||
- Final merge call to Seriatim
|
||||
- Run-local log/config/report path wiring
|
||||
- Promotion of merged/report outputs to canonical paths
|
||||
- Promotion of base/report outputs to canonical paths
|
||||
|
||||
Does not own:
|
||||
- Transcript polishing or downstream artifact generation
|
||||
@@ -43,7 +43,7 @@ Does not own:
|
||||
## State and Manifest Behavior
|
||||
- Reads transcript inputs from transcribe stage outputs in manifest when present; falls back to canonical raw directory.
|
||||
- Writes run-local outputs/logs/config under `runs/{run_id}/merge/...` when enabled.
|
||||
- Promotes canonical merged transcript and optional report.
|
||||
- Promotes canonical base transcript and optional report.
|
||||
- Records normalized-input provenance and adapter metadata in stage metadata.
|
||||
|
||||
## Skip and Resume Behavior
|
||||
@@ -51,7 +51,7 @@ Does not own:
|
||||
- Forced rerun of this or upstream stages can stale downstream succeeded stages via runner invalidation.
|
||||
|
||||
## Failure Behavior
|
||||
- Fails on missing/invalid raw transcripts, missing speakers/autocorrect files, normalize failure, merge failure, invalid merged output JSON, or invalid report JSON when enabled.
|
||||
- Fails on missing/invalid raw transcripts, missing speakers/autocorrect files, normalize failure, merge failure, invalid base output JSON, or invalid report JSON when enabled.
|
||||
|
||||
## Tests to Inspect Before Changing
|
||||
- `internal/stage/merge_test.go`
|
||||
@@ -59,5 +59,5 @@ Does not own:
|
||||
|
||||
## Architectural Invariants
|
||||
- Merge consumes normalized forms of each raw transcript.
|
||||
- Merged transcript must validate before promotion.
|
||||
- Base transcript must validate before promotion.
|
||||
- Report output is optional and gated by config.
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
# Stage: normalize
|
||||
|
||||
## Purpose
|
||||
Normalize the processed transcript into a deterministic intermediate schema for trim and optionally emit a normalize report.
|
||||
Normalize the polished transcript into the full final transcript and optionally emit a normalize report.
|
||||
|
||||
## Inputs and Outputs
|
||||
Inputs:
|
||||
- `transcripts/processed.json`
|
||||
- `transcripts/polished.json`
|
||||
|
||||
Outputs:
|
||||
- `transcripts/normalized.json` (or configured normalize output path)
|
||||
- `transcripts/final.json` (or configured normalize output path)
|
||||
- optional `artifacts/seriatim.normalize.report.json`
|
||||
|
||||
## Boundaries
|
||||
Owns:
|
||||
- Processed transcript discovery/validation
|
||||
- Polished transcript discovery/validation
|
||||
- Normalize request construction and invocation
|
||||
- Optional normalize report wiring
|
||||
- Promotion of normalized transcript and optional report
|
||||
- Promotion of final transcript and optional report
|
||||
|
||||
Does not own:
|
||||
- Bounds detection or segment trimming
|
||||
@@ -35,9 +35,9 @@ Does not own:
|
||||
- Seriatim adapter (`Normalize`).
|
||||
|
||||
## State and Manifest Behavior
|
||||
- Reads processed transcript from polish outputs in manifest when present; falls back to canonical path.
|
||||
- Reads polished transcript from polish outputs in manifest when present; falls back to canonical path.
|
||||
- Uses run-local output/report/log/config paths when run layout is enabled.
|
||||
- Promotes canonical normalized transcript and optional normalize report.
|
||||
- Promotes canonical final transcript and optional normalize report.
|
||||
- Records adapter/result metadata including source path selection.
|
||||
|
||||
## Skip and Resume Behavior
|
||||
@@ -45,12 +45,12 @@ Does not own:
|
||||
- Forced reruns can stale downstream succeeded stages.
|
||||
|
||||
## Failure Behavior
|
||||
- Fails on missing/invalid processed transcript, adapter error, invalid normalized output, or invalid report output when report enabled.
|
||||
- Fails on missing/invalid polished transcript, adapter error, invalid final output, or invalid report output when report enabled.
|
||||
|
||||
## Tests to Inspect Before Changing
|
||||
- `internal/stage/normalize_test.go`
|
||||
- `internal/adapters/seriatim/subprocess_test.go`
|
||||
|
||||
## Architectural Invariants
|
||||
- Normalized output must validate as processed-transcript-compatible JSON (`segments` array required).
|
||||
- Final output must validate as transcript-compatible JSON (`segments` array required).
|
||||
- Default normalize config is applied when `pipeline.normalize` is unset.
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
# Stage: polish
|
||||
|
||||
## Purpose
|
||||
Polish merged transcript with Audita and produce a processed transcript for downstream normalization/analyze.
|
||||
Polish the base transcript with Audita and produce a polished transcript for downstream normalization/analyze.
|
||||
|
||||
## Inputs and Outputs
|
||||
Inputs:
|
||||
- `transcripts/merged.json`
|
||||
- `transcripts/base.json`
|
||||
- `inputs/glossary.yml`
|
||||
|
||||
Outputs:
|
||||
- `transcripts/processed.json`
|
||||
- `transcripts/polished.json`
|
||||
- optional `artifacts/audita.report.json` (when report enabled)
|
||||
|
||||
## Boundaries
|
||||
Owns:
|
||||
- Merged transcript discovery/validation
|
||||
- Base transcript discovery/validation
|
||||
- Audita invocation request construction
|
||||
- Run-local logs/config/work-dir/report wiring
|
||||
- Promotion of processed transcript and optional report
|
||||
- Promotion of polished transcript and optional report
|
||||
|
||||
Does not own:
|
||||
- Upstream merge normalization
|
||||
@@ -47,9 +47,9 @@ Does not own:
|
||||
- Audita adapter (`env.Audita.Run`).
|
||||
|
||||
## State and Manifest Behavior
|
||||
- Reads merged transcript from merge manifest outputs when available; falls back to canonical merged path.
|
||||
- Reads base transcript from merge manifest outputs when available; falls back to canonical base path.
|
||||
- Uses run-local output/report/log/config/scratch paths when run layout is enabled.
|
||||
- Promotes canonical `transcripts/processed.json` and optional report.
|
||||
- Promotes canonical `transcripts/polished.json` and optional report.
|
||||
- Records adapter invocation metadata, credential presence signal, and output provenance in stage metadata.
|
||||
|
||||
## Skip and Resume Behavior
|
||||
@@ -57,13 +57,13 @@ Does not own:
|
||||
- Forced rerun can stale downstream succeeded stages via runner invalidation.
|
||||
|
||||
## Failure Behavior
|
||||
- Fails on missing/invalid merged transcript, missing glossary, adapter error, invalid processed output shape (`segments` array required), or invalid report JSON when enabled.
|
||||
- Fails on missing/invalid base transcript, missing glossary, adapter error, invalid polished output shape (`segments` array required), or invalid report JSON when enabled.
|
||||
|
||||
## Tests to Inspect Before Changing
|
||||
- `internal/stage/polish_test.go`
|
||||
- `internal/adapters/audita/subprocess_test.go`
|
||||
|
||||
## Architectural Invariants
|
||||
- Processed transcript must contain a top-level `segments` array.
|
||||
- Polished transcript must contain a top-level `segments` array.
|
||||
- Report behavior is strictly config-gated.
|
||||
- Stage output canonicalization always ends at `transcripts/processed.json`.
|
||||
- Stage output canonicalization always ends at `transcripts/polished.json`.
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# Stage: trim
|
||||
|
||||
## Purpose
|
||||
Optionally trim the normalized transcript to session bounds; always produce a durable trimmed transcript.
|
||||
Optionally trim the final transcript to session bounds; always produce a durable final-trimmed transcript.
|
||||
|
||||
## Inputs and Outputs
|
||||
Inputs:
|
||||
- `transcripts/normalized.json`
|
||||
- `transcripts/final.json`
|
||||
|
||||
Outputs:
|
||||
- `transcripts/trimmed.json` (or configured trim output path)
|
||||
- `transcripts/final.trimmed.json` (or configured trim output path)
|
||||
- when trim enabled: `artifacts/session_bounds.json`
|
||||
|
||||
## Boundaries
|
||||
Owns:
|
||||
- Trim-enabled switch behavior
|
||||
- Bounds generation via Scriptorium artifact run
|
||||
- Bounds validation against normalized transcript
|
||||
- Bounds validation against final transcript
|
||||
- Keep-selector derivation and Seriatim trim invocation
|
||||
- Copy-through behavior when disabled or bounds indicate unchanged transcript
|
||||
|
||||
@@ -50,19 +50,19 @@ Does not own:
|
||||
- `Trim` when bounds indicate trimming is required
|
||||
|
||||
## State and Manifest Behavior
|
||||
- Reads normalized transcript from normalize manifest outputs when available; falls back to canonical path.
|
||||
- Reads final transcript from normalize manifest outputs when available; falls back to canonical path.
|
||||
- Uses run-local outputs/logs/reports/config/scratch paths when run layout is enabled.
|
||||
- Promotes canonical trimmed transcript; promotes session bounds when trim enabled.
|
||||
- Promotes canonical final-trimmed transcript; promotes session bounds when trim enabled.
|
||||
- Records bounds diagnostics, trim action, keep selector, and adapter metadata.
|
||||
|
||||
## Skip and Resume Behavior
|
||||
- Runner-level skip applies when already succeeded and not forced.
|
||||
- Forced reruns can stale downstream succeeded stages.
|
||||
- When `trim.enabled=false`, stage still succeeds by copying normalized to trimmed output.
|
||||
- When `trim.enabled=false`, stage still succeeds by copying final to final-trimmed output.
|
||||
|
||||
## Failure Behavior
|
||||
- Fails on missing/invalid normalized transcript.
|
||||
- With trim enabled, fails on missing adapters/config, bounds generation/validation errors, invalid bounds JSON, invalid range/segment ids, trim adapter failures, or invalid trimmed output.
|
||||
- Fails on missing/invalid final transcript.
|
||||
- With trim enabled, fails on missing adapters/config, bounds generation/validation errors, invalid bounds JSON, invalid range/segment ids, trim adapter failures, or invalid final-trimmed output.
|
||||
|
||||
## Tests to Inspect Before Changing
|
||||
- `internal/stage/trim_test.go`
|
||||
@@ -70,6 +70,6 @@ Does not own:
|
||||
- `internal/adapters/seriatim/subprocess_test.go`
|
||||
|
||||
## Architectural Invariants
|
||||
- Trim never falls back to processed transcript; normalized transcript is required input.
|
||||
- Trim never falls back to polished transcript; final transcript is required input.
|
||||
- `session_bounds` output exists only for enabled trim path.
|
||||
- Render-debug artifacts are diagnostics and not declared stage outputs.
|
||||
|
||||
210
docs/roadmap/transcripts.md
Normal file
210
docs/roadmap/transcripts.md
Normal 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`.
|
||||
@@ -39,8 +39,8 @@ archive:
|
||||
upload_run: true
|
||||
# Optional promotion rules; sources use Narratio artifact source IDs.
|
||||
promote_artifacts:
|
||||
- source: narratio.transcript.trimmed
|
||||
dest: transcripts/trimmed.json
|
||||
- source: narratio.transcript.final_trimmed
|
||||
dest: transcripts/final.trimmed.json
|
||||
required: true
|
||||
- source: narratio.artifact.session_recap
|
||||
dest: artifacts/session_recap.md
|
||||
@@ -93,14 +93,14 @@ audita:
|
||||
|
||||
normalize:
|
||||
# Optional; defaults shown explicitly.
|
||||
output_path: transcripts/normalized.json
|
||||
output_path: transcripts/final.json
|
||||
output_schema: seriatim-intermediate
|
||||
report: true
|
||||
|
||||
trim:
|
||||
# Keep disabled unless bounds prompt integration is configured.
|
||||
enabled: false
|
||||
output_path: transcripts/trimmed.json
|
||||
output_path: transcripts/final.trimmed.json
|
||||
bounds:
|
||||
prompt_id: dnd.session_bounds
|
||||
profile_id: local-fast
|
||||
@@ -127,7 +127,7 @@ scriptorium:
|
||||
timeout: 10m
|
||||
inputs:
|
||||
transcript:
|
||||
source: narratio.transcript.trimmed
|
||||
source: narratio.transcript.final_trimmed
|
||||
required: true
|
||||
previous_recap:
|
||||
source: narratio.previous_session.artifact.session_recap
|
||||
@@ -155,7 +155,7 @@ scriptorium:
|
||||
source: narratio.artifact.session_recap
|
||||
required: true
|
||||
transcript:
|
||||
source: narratio.transcript.trimmed
|
||||
source: narratio.transcript.final_trimmed
|
||||
required: true
|
||||
vars:
|
||||
session_id: true
|
||||
|
||||
@@ -19,8 +19,8 @@ archive:
|
||||
enabled: true
|
||||
upload_run: true
|
||||
promote_artifacts:
|
||||
- source: narratio.transcript.trimmed
|
||||
dest: transcripts/trimmed.json
|
||||
- source: narratio.transcript.final_trimmed
|
||||
dest: transcripts/final.trimmed.json
|
||||
required: true
|
||||
- source: narratio.artifact.session_recap
|
||||
dest: artifacts/session_recap.md
|
||||
@@ -57,7 +57,7 @@ audita:
|
||||
report: true
|
||||
|
||||
normalize:
|
||||
output_path: transcripts/normalized.json
|
||||
output_path: transcripts/final.json
|
||||
output_schema: seriatim-intermediate
|
||||
report: true
|
||||
|
||||
@@ -78,7 +78,7 @@ scriptorium:
|
||||
timeout: 10m
|
||||
inputs:
|
||||
transcript:
|
||||
source: narratio.transcript.trimmed
|
||||
source: narratio.transcript.final_trimmed
|
||||
required: true
|
||||
previous_recap:
|
||||
source: narratio.previous_session.artifact.session_recap
|
||||
@@ -102,7 +102,7 @@ scriptorium:
|
||||
source: narratio.artifact.session_recap
|
||||
required: true
|
||||
transcript:
|
||||
source: narratio.transcript.trimmed
|
||||
source: narratio.transcript.final_trimmed
|
||||
required: true
|
||||
vars:
|
||||
session_id: true
|
||||
|
||||
@@ -14,7 +14,7 @@ func TestFakeRunnerCapturesRequestAndReturnsPath(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
req := PolishRequest{
|
||||
GeneratedConfigPath: filepath.Join(dir, "config", "audita.yml"),
|
||||
OutputProcessedPath: filepath.Join(dir, "transcripts", "processed.json"),
|
||||
OutputProcessedPath: filepath.Join(dir, "transcripts", "polished.json"),
|
||||
StdoutLogPath: filepath.Join(dir, "logs", "audita.stdout.log"),
|
||||
StderrLogPath: filepath.Join(dir, "logs", "audita.stderr.log"),
|
||||
}
|
||||
|
||||
@@ -52,9 +52,9 @@ func TestSubprocessRunnerSuccessArgsEnvAndValidation(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
req := PolishRequest{
|
||||
GeneratedConfigPath: filepath.Join(dir, "audita.generated.yml"),
|
||||
MergedTranscriptPath: filepath.Join(dir, "merged.json"),
|
||||
MergedTranscriptPath: filepath.Join(dir, "base.json"),
|
||||
GlossaryPath: filepath.Join(dir, "glossary.yml"),
|
||||
OutputProcessedPath: filepath.Join(dir, "processed.json"),
|
||||
OutputProcessedPath: filepath.Join(dir, "polished.json"),
|
||||
ReportPath: filepath.Join(dir, "audita.report.json"),
|
||||
WorkDir: filepath.Join(dir, "artifacts", "audita-work"),
|
||||
StdoutLogPath: filepath.Join(dir, "audita.stdout.log"),
|
||||
@@ -571,7 +571,7 @@ func mustAuditaRunner(t *testing.T, cfg SubprocessRunnerConfig) *SubprocessRunne
|
||||
func auditaReqForTest(t *testing.T, withReport bool) PolishRequest {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
merged := filepath.Join(dir, "merged.json")
|
||||
merged := filepath.Join(dir, "base.json")
|
||||
glossary := filepath.Join(dir, "glossary.yml")
|
||||
writeAuditaTestFile(t, merged, `{"segments":[]}`)
|
||||
writeAuditaTestFile(t, glossary, "terms: []\n")
|
||||
@@ -579,7 +579,7 @@ func auditaReqForTest(t *testing.T, withReport bool) PolishRequest {
|
||||
GeneratedConfigPath: filepath.Join(dir, "audita.generated.yml"),
|
||||
MergedTranscriptPath: merged,
|
||||
GlossaryPath: glossary,
|
||||
OutputProcessedPath: filepath.Join(dir, "processed.json"),
|
||||
OutputProcessedPath: filepath.Join(dir, "polished.json"),
|
||||
WorkDir: filepath.Join(dir, "artifacts", "audita-work"),
|
||||
StdoutLogPath: filepath.Join(dir, "audita.stdout.log"),
|
||||
StderrLogPath: filepath.Join(dir, "audita.stderr.log"),
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestSubprocessRunnerRunSuccessBuildsDeterministicArgsAndCapturesLogs(t *tes
|
||||
ConfigPath: "/etc/scriptorium/config.yml",
|
||||
PromptID: "dnd.session_recap",
|
||||
ProfileID: "local-quality",
|
||||
InputPaths: map[string]string{"transcript": filepath.Join(dir, "processed.json"), "other": filepath.Join(dir, "other.md")},
|
||||
InputPaths: map[string]string{"transcript": filepath.Join(dir, "polished.json"), "other": filepath.Join(dir, "other.md")},
|
||||
Vars: map[string]string{"session_id": "2026-05-03", "campaign_name": "Icewind Dale"},
|
||||
OutputPath: filepath.Join(dir, "artifacts", "session_recap.md"),
|
||||
StdoutLogPath: filepath.Join(dir, "logs", "scriptorium.run.stdout.log"),
|
||||
@@ -180,7 +180,7 @@ func TestSubprocessRunnerRenderSuccess(t *testing.T) {
|
||||
req := RenderArtifactRequest{
|
||||
Binary: wrapper,
|
||||
PromptID: "dnd.session_recap",
|
||||
InputPaths: map[string]string{"transcript": filepath.Join(dir, "processed.json")},
|
||||
InputPaths: map[string]string{"transcript": filepath.Join(dir, "polished.json")},
|
||||
OutputPath: filepath.Join(dir, "artifacts", "session_recap.render.json"),
|
||||
StdoutLogPath: filepath.Join(dir, "logs", "scriptorium.render.stdout.log"),
|
||||
StderrLogPath: filepath.Join(dir, "logs", "scriptorium.render.stderr.log"),
|
||||
@@ -285,7 +285,7 @@ type scriptoriumHelperRecord struct {
|
||||
func runReqForTest(t *testing.T, binary string) RunArtifactRequest {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
transcriptPath := filepath.Join(dir, "processed.json")
|
||||
transcriptPath := filepath.Join(dir, "polished.json")
|
||||
writeScriptoriumFile(t, transcriptPath, `{"segments":[]}`)
|
||||
return RunArtifactRequest{
|
||||
Binary: binary,
|
||||
|
||||
@@ -14,7 +14,7 @@ func TestFakeRunnerCapturesRequestAndReturnsPath(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
req := MergeRequest{
|
||||
GeneratedConfigPath: filepath.Join(dir, "config", "seriatim.yml"),
|
||||
OutputMergedTranscriptPath: filepath.Join(dir, "transcripts", "merged.json"),
|
||||
OutputMergedTranscriptPath: filepath.Join(dir, "transcripts", "base.json"),
|
||||
StdoutLogPath: filepath.Join(dir, "logs", "seriatim.stdout.log"),
|
||||
StderrLogPath: filepath.Join(dir, "logs", "seriatim.stderr.log"),
|
||||
}
|
||||
@@ -57,8 +57,8 @@ func TestFakeRunnerTrimCapturesRequestAndReturnsPath(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
req := TrimRequest{
|
||||
GeneratedConfigPath: filepath.Join(dir, "config", "seriatim.trim.yml"),
|
||||
InputTranscriptPath: filepath.Join(dir, "transcripts", "processed.json"),
|
||||
OutputTrimmedPath: filepath.Join(dir, "transcripts", "trimmed.json"),
|
||||
InputTranscriptPath: filepath.Join(dir, "transcripts", "polished.json"),
|
||||
OutputTrimmedPath: filepath.Join(dir, "transcripts", "final.trimmed.json"),
|
||||
KeepSelector: "1-10",
|
||||
StdoutLogPath: filepath.Join(dir, "logs", "seriatim.trim.stdout.log"),
|
||||
StderrLogPath: filepath.Join(dir, "logs", "seriatim.trim.stderr.log"),
|
||||
@@ -105,8 +105,8 @@ func TestFakeRunnerNormalizeCapturesRequestAndReturnsPath(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
req := NormalizeRequest{
|
||||
GeneratedConfigPath: filepath.Join(dir, "config", "seriatim.normalize.yml"),
|
||||
InputTranscriptPath: filepath.Join(dir, "transcripts", "processed.json"),
|
||||
OutputNormalizedPath: filepath.Join(dir, "transcripts", "normalized.json"),
|
||||
InputTranscriptPath: filepath.Join(dir, "transcripts", "polished.json"),
|
||||
OutputNormalizedPath: filepath.Join(dir, "transcripts", "final.json"),
|
||||
OutputSchema: "seriatim-intermediate",
|
||||
ReportPath: filepath.Join(dir, "artifacts", "seriatim.normalize.report.json"),
|
||||
StdoutLogPath: filepath.Join(dir, "logs", "seriatim.normalize.stdout.log"),
|
||||
|
||||
@@ -50,7 +50,7 @@ func TestSubprocessRunnerSuccessWithReportArgsAndEnv(t *testing.T) {
|
||||
req := MergeRequest{
|
||||
GeneratedConfigPath: filepath.Join(dir, "seriatim.generated.yml"),
|
||||
InputTranscriptPaths: []string{filepath.Join(dir, "a.json"), filepath.Join(dir, "b.json")},
|
||||
OutputMergedTranscriptPath: filepath.Join(dir, "merged.json"),
|
||||
OutputMergedTranscriptPath: filepath.Join(dir, "base.json"),
|
||||
ReportPath: filepath.Join(dir, "seriatim.report.json"),
|
||||
SpeakersPath: filepath.Join(dir, "speakers.yml"),
|
||||
AutocorrectPath: filepath.Join(dir, "autocorrect.yml"),
|
||||
@@ -732,7 +732,7 @@ func mergeReqForTest(t *testing.T, withReport bool) MergeRequest {
|
||||
req := MergeRequest{
|
||||
GeneratedConfigPath: filepath.Join(dir, "seriatim.generated.yml"),
|
||||
InputTranscriptPaths: []string{in1, in2},
|
||||
OutputMergedTranscriptPath: filepath.Join(dir, "merged.json"),
|
||||
OutputMergedTranscriptPath: filepath.Join(dir, "base.json"),
|
||||
StdoutLogPath: filepath.Join(dir, "seriatim.stdout.log"),
|
||||
StderrLogPath: filepath.Join(dir, "seriatim.stderr.log"),
|
||||
}
|
||||
@@ -745,11 +745,11 @@ func mergeReqForTest(t *testing.T, withReport bool) MergeRequest {
|
||||
func trimReqForTest(t *testing.T) TrimRequest {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
input := filepath.Join(dir, "processed.json")
|
||||
input := filepath.Join(dir, "polished.json")
|
||||
writeSeriatimFile(t, input, `{"schema":"seriatim.intermediate.v1","segments":[]}`)
|
||||
return TrimRequest{
|
||||
InputTranscriptPath: input,
|
||||
OutputTrimmedPath: filepath.Join(dir, "trimmed.json"),
|
||||
OutputTrimmedPath: filepath.Join(dir, "final.trimmed.json"),
|
||||
KeepSelector: "5-12",
|
||||
GeneratedConfigPath: filepath.Join(dir, "seriatim.trim.generated.yml"),
|
||||
StdoutLogPath: filepath.Join(dir, "seriatim.trim.stdout.log"),
|
||||
@@ -760,12 +760,12 @@ func trimReqForTest(t *testing.T) TrimRequest {
|
||||
func normalizeReqForTest(t *testing.T, withReport bool) NormalizeRequest {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
input := filepath.Join(dir, "processed.json")
|
||||
input := filepath.Join(dir, "polished.json")
|
||||
writeSeriatimFile(t, input, `{"schema":"audita.processed.v1","segments":[]}`)
|
||||
|
||||
req := NormalizeRequest{
|
||||
InputTranscriptPath: input,
|
||||
OutputNormalizedPath: filepath.Join(dir, "normalized.json"),
|
||||
OutputNormalizedPath: filepath.Join(dir, "final.json"),
|
||||
OutputSchema: "seriatim-intermediate",
|
||||
GeneratedConfigPath: filepath.Join(dir, "seriatim.normalize.generated.yml"),
|
||||
StdoutLogPath: filepath.Join(dir, "seriatim.normalize.stdout.log"),
|
||||
|
||||
@@ -112,7 +112,7 @@ func TestExecuteRunStageNormalizeIsAccepted(t *testing.T) {
|
||||
workspaceRoot := t.TempDir()
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot, "https://example.com/transcribe")
|
||||
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "processed.json"), `{"segments":[{"id":1}]}`)
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "polished.json"), `{"segments":[{"id":1}]}`)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
@@ -233,7 +233,7 @@ inputs:
|
||||
})
|
||||
|
||||
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", sessionID)
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "merged.json"), `{"schema":"seriatim-intermediate","segments":[]}`)
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "base.json"), `{"schema":"seriatim-intermediate","segments":[]}`)
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "glossary.yml"), "[]\n")
|
||||
|
||||
var stdout bytes.Buffer
|
||||
|
||||
@@ -894,10 +894,10 @@ func writeArtifactList(out io.Writer, cfg *config.Config, catalog *artifacts.Art
|
||||
lockSet := lockSourceSet(locks.All)
|
||||
fmt.Fprintln(out, "Built-in:")
|
||||
for _, id := range []string{
|
||||
artifacts.ArtifactTranscriptMerged,
|
||||
artifacts.ArtifactTranscriptBase,
|
||||
artifacts.ArtifactTranscriptPolished,
|
||||
artifacts.ArtifactTranscriptFull,
|
||||
artifacts.ArtifactTranscriptTrimmed,
|
||||
artifacts.ArtifactTranscriptFinal,
|
||||
artifacts.ArtifactTranscriptFinalTrimmed,
|
||||
artifacts.ArtifactBoundsSession,
|
||||
} {
|
||||
writeArtifactLine(out, id, lockSet)
|
||||
|
||||
@@ -476,7 +476,7 @@ func TestExecuteLocksAddListAndRemoveUseRemoteLockStore(t *testing.T) {
|
||||
"--session", sessionPath,
|
||||
"--session-id", "2026-05-03",
|
||||
"--reason", "manual edit",
|
||||
"narratio.transcript.trimmed",
|
||||
"narratio.transcript.final_trimmed",
|
||||
}, &stdout, &stderr)
|
||||
if code != 0 {
|
||||
t.Fatalf("locks add exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
@@ -486,7 +486,7 @@ func TestExecuteLocksAddListAndRemoveUseRemoteLockStore(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatalf("remote locks key %q not uploaded", key)
|
||||
}
|
||||
if !strings.Contains(string(obj.Data), "source: narratio.transcript.trimmed") || !strings.Contains(string(obj.Data), "reason: manual edit") {
|
||||
if !strings.Contains(string(obj.Data), "source: narratio.transcript.final_trimmed") || !strings.Contains(string(obj.Data), "reason: manual edit") {
|
||||
t.Fatalf("lock store data = %q", string(obj.Data))
|
||||
}
|
||||
|
||||
@@ -502,7 +502,7 @@ func TestExecuteLocksAddListAndRemoveUseRemoteLockStore(t *testing.T) {
|
||||
if code != 0 {
|
||||
t.Fatalf("locks list exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "- narratio.transcript.trimmed origin=remote") {
|
||||
if !strings.Contains(stdout.String(), "- narratio.transcript.final_trimmed origin=remote") {
|
||||
t.Fatalf("stdout = %q, want remote lock", stdout.String())
|
||||
}
|
||||
|
||||
@@ -514,7 +514,7 @@ func TestExecuteLocksAddListAndRemoveUseRemoteLockStore(t *testing.T) {
|
||||
"--campaign", campaignPath,
|
||||
"--session", sessionPath,
|
||||
"--session-id", "2026-05-03",
|
||||
"narratio.transcript.trimmed",
|
||||
"narratio.transcript.final_trimmed",
|
||||
}, &stdout, &stderr)
|
||||
if code != 0 {
|
||||
t.Fatalf("locks remove exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
@@ -547,7 +547,7 @@ func TestExecuteLocksAddDuplicateRequiresForce(t *testing.T) {
|
||||
"--session", sessionPath,
|
||||
"--session-id", "2026-05-03",
|
||||
"--reason", "first",
|
||||
"narratio.transcript.trimmed",
|
||||
"narratio.transcript.final_trimmed",
|
||||
}, &stdout, &stderr)
|
||||
if code != 0 {
|
||||
t.Fatalf("initial locks add exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
@@ -562,7 +562,7 @@ func TestExecuteLocksAddDuplicateRequiresForce(t *testing.T) {
|
||||
"--session", sessionPath,
|
||||
"--session-id", "2026-05-03",
|
||||
"--reason", "second",
|
||||
"narratio.transcript.trimmed",
|
||||
"narratio.transcript.final_trimmed",
|
||||
}, &stdout, &stderr)
|
||||
if code == 0 {
|
||||
t.Fatal("duplicate locks add exit code = 0, want non-zero")
|
||||
@@ -581,7 +581,7 @@ func TestExecuteLocksAddDuplicateRequiresForce(t *testing.T) {
|
||||
"--session-id", "2026-05-03",
|
||||
"--reason", "second",
|
||||
"--force",
|
||||
"narratio.transcript.trimmed",
|
||||
"narratio.transcript.final_trimmed",
|
||||
}, &stdout, &stderr)
|
||||
if code != 0 {
|
||||
t.Fatalf("forced locks add exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
@@ -599,8 +599,8 @@ func TestExecuteLocksRequireSessionID(t *testing.T) {
|
||||
want string
|
||||
}{
|
||||
{"list", []string{"locks"}, "locks: --session-id is required"},
|
||||
{"add", []string{"locks", "add", "narratio.transcript.trimmed"}, "locks add: --session-id is required"},
|
||||
{"remove", []string{"locks", "remove", "narratio.transcript.trimmed"}, "locks remove: --session-id is required"},
|
||||
{"add", []string{"locks", "add", "narratio.transcript.final_trimmed"}, "locks add: --session-id is required"},
|
||||
{"remove", []string{"locks", "remove", "narratio.transcript.final_trimmed"}, "locks remove: --session-id is required"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -620,7 +620,7 @@ func TestExecuteLocksRequireSessionID(t *testing.T) {
|
||||
func TestExecuteLocksCannotModifyStaticLocks(t *testing.T) {
|
||||
workspaceRoot := t.TempDir()
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
addStaticArchiveLockToPipelineConfig(t, pipelinePath, "narratio.transcript.trimmed")
|
||||
addStaticArchiveLockToPipelineConfig(t, pipelinePath, "narratio.transcript.final_trimmed")
|
||||
fake := &storage.FakeBackend{}
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
||||
@@ -633,7 +633,7 @@ func TestExecuteLocksCannotModifyStaticLocks(t *testing.T) {
|
||||
"--campaign", campaignPath,
|
||||
"--session", sessionPath,
|
||||
"--session-id", "2026-05-03",
|
||||
"narratio.transcript.trimmed",
|
||||
"narratio.transcript.final_trimmed",
|
||||
}, &stdout, &stderr)
|
||||
if code == 0 {
|
||||
t.Fatal("locks add static lock exit code = 0, want non-zero")
|
||||
@@ -650,7 +650,7 @@ func TestExecuteLocksCannotModifyStaticLocks(t *testing.T) {
|
||||
"--campaign", campaignPath,
|
||||
"--session", sessionPath,
|
||||
"--session-id", "2026-05-03",
|
||||
"narratio.transcript.trimmed",
|
||||
"narratio.transcript.final_trimmed",
|
||||
}, &stdout, &stderr)
|
||||
if code == 0 {
|
||||
t.Fatal("locks remove static lock exit code = 0, want non-zero")
|
||||
@@ -666,7 +666,7 @@ func TestExecuteTopLevelLockAndUnlockAreRemoved(t *testing.T) {
|
||||
t.Run(cmd, func(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
code := Execute([]string{cmd, "narratio.transcript.trimmed"}, &stdout, &stderr)
|
||||
code := Execute([]string{cmd, "narratio.transcript.final_trimmed"}, &stdout, &stderr)
|
||||
if code == 0 {
|
||||
t.Fatal("exit code = 0, want non-zero")
|
||||
}
|
||||
@@ -718,14 +718,14 @@ func TestExecuteArtifactsListRemoteReportsPromotedAvailability(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
addArchivePromotionsToPipeline(t, pipelinePath, `
|
||||
promote_artifacts:
|
||||
- source: narratio.transcript.trimmed
|
||||
dest: transcripts/trimmed.json
|
||||
- source: narratio.transcript.final_trimmed
|
||||
dest: transcripts/final.trimmed.json
|
||||
required: true
|
||||
`)
|
||||
fake := &storage.FakeBackend{}
|
||||
trimmedKey := artifacts.S3PromotedArtifactKey(
|
||||
artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"),
|
||||
"transcripts/trimmed.json",
|
||||
"transcripts/final.trimmed.json",
|
||||
)
|
||||
fake.SeedObject(storage.FakeObject{Key: trimmedKey, Data: []byte(`{"segments":[]}`)})
|
||||
var storeInitCalls int
|
||||
@@ -743,7 +743,7 @@ func TestExecuteArtifactsListRemoteReportsPromotedAvailability(t *testing.T) {
|
||||
if code != 0 {
|
||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "narratio.transcript.trimmed remote=promoted") {
|
||||
if !strings.Contains(stdout.String(), "narratio.transcript.final_trimmed remote=promoted") {
|
||||
t.Fatalf("stdout = %q, want promoted remote availability", stdout.String())
|
||||
}
|
||||
}
|
||||
@@ -753,7 +753,7 @@ func TestExecuteArtifactsListRemoteUsesPromotionDestinations(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
addArchivePromotionsToPipeline(t, pipelinePath, `
|
||||
promote_artifacts:
|
||||
- source: narratio.transcript.full
|
||||
- source: narratio.transcript.final
|
||||
dest: transcripts/full.json
|
||||
required: true
|
||||
- source: narratio.bounds.session
|
||||
@@ -781,7 +781,7 @@ func TestExecuteArtifactsListRemoteUsesPromotionDestinations(t *testing.T) {
|
||||
}
|
||||
out := stdout.String()
|
||||
for _, unwanted := range []string{
|
||||
"narratio.transcript.full remote=missing",
|
||||
"narratio.transcript.final remote=missing",
|
||||
"narratio.bounds.session remote=missing",
|
||||
} {
|
||||
if strings.Contains(out, unwanted) {
|
||||
@@ -789,7 +789,7 @@ func TestExecuteArtifactsListRemoteUsesPromotionDestinations(t *testing.T) {
|
||||
}
|
||||
}
|
||||
for _, want := range []string{
|
||||
"narratio.transcript.full dest=transcripts/full.json remote=promoted",
|
||||
"narratio.transcript.final dest=transcripts/full.json remote=promoted",
|
||||
"narratio.bounds.session dest=transcripts/bounds.json remote=promoted",
|
||||
} {
|
||||
if !strings.Contains(out, want) {
|
||||
@@ -803,24 +803,24 @@ func TestExecuteStatusReportsRemoteArtifactCatalog(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
addArchivePromotionsToPipeline(t, pipelinePath, `
|
||||
promote_artifacts:
|
||||
- source: narratio.transcript.trimmed
|
||||
dest: transcripts/trimmed.json
|
||||
- source: narratio.transcript.final_trimmed
|
||||
dest: transcripts/final.trimmed.json
|
||||
required: true
|
||||
- source: narratio.transcript.full
|
||||
- source: narratio.transcript.final
|
||||
dest: transcripts/full.json
|
||||
required: true
|
||||
`)
|
||||
fake := &storage.FakeBackend{}
|
||||
sessionPrefix := artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03")
|
||||
manifestKey, runIDKey := artifacts.ResolveArchiveCurrentStateKeys(sessionPrefix)
|
||||
trimmedKey := artifacts.S3PromotedArtifactKey(sessionPrefix, "transcripts/trimmed.json")
|
||||
trimmedKey := artifacts.S3PromotedArtifactKey(sessionPrefix, "transcripts/final.trimmed.json")
|
||||
fullKey := artifacts.S3PromotedArtifactKey(sessionPrefix, "transcripts/full.json")
|
||||
lockKey := artifacts.S3SessionLocksKey(sessionPrefix)
|
||||
fake.SeedObject(storage.FakeObject{Key: runIDKey, Data: []byte("20260519T010203Z-a1b2c3d4\n")})
|
||||
fake.SeedObject(storage.FakeObject{Key: manifestKey, Data: restoreManifestJSON(t, "2026-05-03", "sample-campaign")})
|
||||
fake.SeedObject(storage.FakeObject{Key: trimmedKey, Data: []byte(`{"segments":[]}`)})
|
||||
fake.SeedObject(storage.FakeObject{Key: fullKey, Data: []byte(`{"segments":[]}`)})
|
||||
fake.SeedObject(storage.FakeObject{Key: lockKey, Data: []byte("locks:\n - source: narratio.transcript.trimmed\n reason: remote review\n")})
|
||||
fake.SeedObject(storage.FakeObject{Key: lockKey, Data: []byte("locks:\n - source: narratio.transcript.final_trimmed\n reason: remote review\n")})
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
||||
|
||||
@@ -843,15 +843,15 @@ func TestExecuteStatusReportsRemoteArtifactCatalog(t *testing.T) {
|
||||
"Configured:",
|
||||
"Previous-session:",
|
||||
"Promoted:",
|
||||
"narratio.transcript.trimmed locked",
|
||||
"narratio.transcript.trimmed locked remote=promoted",
|
||||
"narratio.transcript.full dest=transcripts/full.json remote=promoted",
|
||||
"narratio.transcript.final_trimmed locked",
|
||||
"narratio.transcript.final_trimmed locked remote=promoted",
|
||||
"narratio.transcript.final dest=transcripts/full.json remote=promoted",
|
||||
} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Fatalf("stdout = %q, want %q", out, want)
|
||||
}
|
||||
}
|
||||
if strings.Contains(out, "narratio.transcript.merged remote=missing") {
|
||||
if strings.Contains(out, "narratio.transcript.base remote=missing") {
|
||||
t.Fatalf("stdout = %q, did not want catalog remote marker", out)
|
||||
}
|
||||
}
|
||||
@@ -861,8 +861,8 @@ func TestExecuteStatusReportsRemoteArtifactCatalogErrorsWithoutFailing(t *testin
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
addArchivePromotionsToPipeline(t, pipelinePath, `
|
||||
promote_artifacts:
|
||||
- source: narratio.transcript.trimmed
|
||||
dest: transcripts/trimmed.json
|
||||
- source: narratio.transcript.final_trimmed
|
||||
dest: transcripts/final.trimmed.json
|
||||
required: true
|
||||
`)
|
||||
fake := &storage.FakeBackend{ExistsErr: fmt.Errorf("exists failed")}
|
||||
@@ -885,7 +885,7 @@ func TestExecuteStatusReportsRemoteArtifactCatalogErrorsWithoutFailing(t *testin
|
||||
if !strings.Contains(out, "Remote archive: missing or unavailable:") {
|
||||
t.Fatalf("stdout = %q, want remote archive unavailable state", out)
|
||||
}
|
||||
if !strings.Contains(out, "Remote outputs:") || !strings.Contains(out, "narratio.transcript.trimmed remote=error") {
|
||||
if !strings.Contains(out, "Remote outputs:") || !strings.Contains(out, "narratio.transcript.final_trimmed remote=error") {
|
||||
t.Fatalf("stdout = %q, want remote output error state", out)
|
||||
}
|
||||
if !strings.Contains(out, "Archive locks: error:") {
|
||||
@@ -898,7 +898,7 @@ func TestExecuteArchiveLoadsRemoteLocks(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeValidArchiveConfigFiles(t, workspaceRoot)
|
||||
fake := &storage.FakeBackend{}
|
||||
lockKey := artifacts.S3SessionLocksKey(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"))
|
||||
fake.SeedObject(storage.FakeObject{Key: lockKey, Data: []byte("locks:\n - source: narratio.transcript.trimmed\n reason: remote review\n")})
|
||||
fake.SeedObject(storage.FakeObject{Key: lockKey, Data: []byte("locks:\n - source: narratio.transcript.final_trimmed\n reason: remote review\n")})
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
||||
|
||||
@@ -907,7 +907,7 @@ func TestExecuteArchiveLoadsRemoteLocks(t *testing.T) {
|
||||
// The archive stage only checks the manifest statuses and source files.
|
||||
_ = stageName
|
||||
}
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "trimmed.json"), `{"segments":[]}`)
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "final.trimmed.json"), `{"segments":[]}`)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
@@ -915,7 +915,7 @@ func TestExecuteArchiveLoadsRemoteLocks(t *testing.T) {
|
||||
if code != 0 {
|
||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||
}
|
||||
promotedKey := artifacts.S3PromotedArtifactKey(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"), "transcripts/trimmed.json")
|
||||
promotedKey := artifacts.S3PromotedArtifactKey(artifacts.S3SessionPrefix("dnd", "sample-campaign", "2026-05-03"), "transcripts/final.trimmed.json")
|
||||
if _, ok := fake.Objects[promotedKey]; ok {
|
||||
t.Fatalf("locked promoted key %q was uploaded", promotedKey)
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ func TestPostArchiveCleanupNotRunWhenPromotionIsMissing(t *testing.T) {
|
||||
cfg.Pipeline.Spool.DeleteAudioAfterArchive = true
|
||||
cfg.Pipeline.Workspace.CleanupAfterArchive = true
|
||||
cfg.Pipeline.Archive.PromoteArtifacts = []config.ArchivePromotionRule{
|
||||
{Source: "narratio.transcript.merged", Dest: "transcripts/merged.json", Required: boolPtr(true)},
|
||||
{Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(true)},
|
||||
}
|
||||
|
||||
archiveStageImpl, err := stage.Select("archive")
|
||||
@@ -333,7 +333,7 @@ func archiveStageCleanupFixture(t *testing.T) (*config.Config, cleanupSeed, stri
|
||||
Enabled: boolPtr(true),
|
||||
UploadRun: boolPtr(true),
|
||||
PromoteArtifacts: []config.ArchivePromotionRule{
|
||||
{Source: "narratio.transcript.trimmed", Dest: "transcripts/trimmed.json", Required: boolPtr(true)},
|
||||
{Source: "narratio.transcript.final_trimmed", Dest: "transcripts/final.trimmed.json", Required: boolPtr(true)},
|
||||
{Source: "narratio.artifact.session_recap", Dest: "artifacts/session_recap.md", Required: boolPtr(true)},
|
||||
},
|
||||
}
|
||||
@@ -371,14 +371,14 @@ func writeArchiveFixtureRunFiles(t *testing.T, runWorkDir, sessionRoot string) {
|
||||
t.Helper()
|
||||
mustWriteFile(t, filepath.Join(runWorkDir, "prepare", "inputs", "session.yml"), "session_id: 2026-05-03\n")
|
||||
mustWriteFile(t, filepath.Join(runWorkDir, "transcribe", "outputs", "transcripts", "raw", "speaker.json"), "{}\n")
|
||||
mustWriteFile(t, filepath.Join(runWorkDir, "trim", "outputs", "transcripts", "trimmed.json"), "{\"segments\":[]}\n")
|
||||
mustWriteFile(t, filepath.Join(runWorkDir, "trim", "outputs", "transcripts", "final.trimmed.json"), "{\"segments\":[]}\n")
|
||||
mustWriteFile(t, filepath.Join(runWorkDir, "analyze", "outputs", "artifacts", "session_recap.md"), "# recap\n")
|
||||
mustWriteFile(t, filepath.Join(runWorkDir, "polish", "reports", "audita.report.json"), "{}\n")
|
||||
mustWriteFile(t, filepath.Join(runWorkDir, "merge", "config", "seriatim.generated.yml"), "key: value\n")
|
||||
mustWriteFile(t, filepath.Join(runWorkDir, "logs", "audita.stderr.log"), "stderr\n")
|
||||
mustWriteFile(t, filepath.Join(runWorkDir, "manifest.json"), "{}\n")
|
||||
|
||||
mustWriteFile(t, filepath.Join(sessionRoot, "transcripts", "trimmed.json"), "{\"segments\":[]}\n")
|
||||
mustWriteFile(t, filepath.Join(sessionRoot, "transcripts", "final.trimmed.json"), "{\"segments\":[]}\n")
|
||||
mustWriteFile(t, filepath.Join(sessionRoot, "artifacts", "session_recap.md"), "# recap\n")
|
||||
}
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ scriptorium:
|
||||
output_path: artifacts/session_recap.md
|
||||
inputs:
|
||||
transcript:
|
||||
source: narratio.transcript.trimmed
|
||||
source: narratio.transcript.final_trimmed
|
||||
required: true
|
||||
previous_recap:
|
||||
source: narratio.previous_session.artifact.session_recap
|
||||
@@ -184,7 +184,7 @@ previous_session_id: 2026-04-26
|
||||
cfg, sessionPrefix, manifestKey, runIDKey := seedRestoreCommittedState(t, fakeStore, pipelinePath, campaignPath, sessionPath)
|
||||
seedRestoreObject(fakeStore, runIDKey, []byte("20260519T010203Z-a1b2c3d4\n"))
|
||||
seedRestoreObject(fakeStore, manifestKey, restoreWorkflowManifestJSON(t, cfg.Session.SessionID, cfg.Session.Campaign))
|
||||
seedRestoreObject(fakeStore, sessionPrefix+"transcripts/trimmed.json", []byte(`{"segments":[]}`+"\n"))
|
||||
seedRestoreObject(fakeStore, sessionPrefix+"transcripts/final.trimmed.json", []byte(`{"segments":[]}`+"\n"))
|
||||
seedRestorePreviousCurrent(t, fakeStore, cfg, "# previous recap\n")
|
||||
|
||||
restoreWithStoreAndRealPhases(t, fakeStore)
|
||||
@@ -210,7 +210,7 @@ previous_session_id: 2026-04-26
|
||||
}
|
||||
|
||||
sessionRoot := artifacts.SessionWorkDirForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID)
|
||||
mustReadEquals(t, filepath.Join(sessionRoot, "transcripts", "trimmed.json"), `{"segments":[]}`+"\n")
|
||||
mustReadEquals(t, filepath.Join(sessionRoot, "transcripts", "final.trimmed.json"), `{"segments":[]}`+"\n")
|
||||
previousManifestBytes, err := os.ReadFile(filepath.Join(sessionRoot, "previous", "manifest.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("read restored previous manifest: %v", err)
|
||||
@@ -269,7 +269,7 @@ previous_session_id: 2026-04-26
|
||||
t.Fatalf("scriptorium run requests = %d, want 1", len(scriptoriumFake.RunRequests))
|
||||
}
|
||||
req := scriptoriumFake.RunRequests[0]
|
||||
if got := req.InputPaths["transcript"]; got != filepath.Join(sessionRoot, "transcripts", "trimmed.json") {
|
||||
if got := req.InputPaths["transcript"]; got != filepath.Join(sessionRoot, "transcripts", "final.trimmed.json") {
|
||||
t.Fatalf("transcript input = %q, want trimmed transcript path", got)
|
||||
}
|
||||
if got := req.InputPaths["previous_recap"]; got != filepath.Join(sessionRoot, "previous", "artifacts", "session_recap.md") {
|
||||
|
||||
@@ -107,7 +107,7 @@ func TestRunStageExecutesOnlySelectedStage(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
||||
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "merged.json"), `{"segments":[]}`)
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "base.json"), `{"segments":[]}`)
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "glossary.yml"), "terms: []\n")
|
||||
|
||||
var out bytes.Buffer
|
||||
@@ -137,7 +137,7 @@ func TestRunStageSkipAndForce(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
||||
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "merged.json"), `{"segments":[]}`)
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "base.json"), `{"segments":[]}`)
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "glossary.yml"), "terms: []\n")
|
||||
|
||||
store := &manifest.LocalStore{}
|
||||
@@ -171,7 +171,7 @@ func TestRunStageForceMarksDownstreamStaleAndResumeContinuesFromStale(t *testing
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
||||
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "merged.json"), `{"segments":[]}`)
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "base.json"), `{"segments":[]}`)
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "glossary.yml"), "terms: []\n")
|
||||
|
||||
store := &manifest.LocalStore{}
|
||||
@@ -217,7 +217,7 @@ func TestRunStageTrimExecutes(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
||||
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "normalized.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "final.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
|
||||
var out bytes.Buffer
|
||||
err := RunStage(context.Background(), []string{"--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "trim"}, &out)
|
||||
@@ -246,7 +246,7 @@ func TestRunStageNormalizeExecutes(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
||||
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "processed.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "polished.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
|
||||
var out bytes.Buffer
|
||||
err := RunStage(context.Background(), []string{"--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "normalize"}, &out)
|
||||
|
||||
@@ -761,7 +761,7 @@ func TestExecuteStagesRunLocalArtifactsAndCanonicalPromotion(t *testing.T) {
|
||||
filepath.Join(runRoot, "merge", "logs", "seriatim.stdout.log"),
|
||||
filepath.Join(runRoot, "polish", "config", "audita.generated.yml"),
|
||||
filepath.Join(runRoot, "normalize", "logs", "seriatim.normalize.stdout.log"),
|
||||
filepath.Join(runRoot, "trim", "outputs", "transcripts", "trimmed.json"),
|
||||
filepath.Join(runRoot, "trim", "outputs", "transcripts", "final.trimmed.json"),
|
||||
}
|
||||
for _, p := range runLocalChecks {
|
||||
if _, statErr := os.Stat(p); statErr != nil {
|
||||
@@ -771,10 +771,10 @@ func TestExecuteStagesRunLocalArtifactsAndCanonicalPromotion(t *testing.T) {
|
||||
|
||||
canonicalChecks := []string{
|
||||
filepath.Join(paths.TranscriptsRawDir, "alice.json"),
|
||||
filepath.Join(paths.TranscriptsDir, "merged.json"),
|
||||
filepath.Join(paths.TranscriptsDir, "processed.json"),
|
||||
filepath.Join(paths.TranscriptsDir, "normalized.json"),
|
||||
filepath.Join(paths.TranscriptsDir, "trimmed.json"),
|
||||
filepath.Join(paths.TranscriptsDir, "base.json"),
|
||||
filepath.Join(paths.TranscriptsDir, "polished.json"),
|
||||
filepath.Join(paths.TranscriptsDir, "final.json"),
|
||||
filepath.Join(paths.TranscriptsDir, "final.trimmed.json"),
|
||||
}
|
||||
for _, p := range canonicalChecks {
|
||||
if _, statErr := os.Stat(p); statErr != nil {
|
||||
@@ -919,7 +919,7 @@ func TestAdapterBackedStageFailureMarksManifestFailed(t *testing.T) {
|
||||
if ensureErr != nil {
|
||||
t.Fatalf("EnsureLayout() error = %v", ensureErr)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(paths.TranscriptsDir, "merged.json"), []byte(`{"segments":[]}`), 0o644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(paths.TranscriptsDir, "base.json"), []byte(`{"segments":[]}`), 0o644); err != nil {
|
||||
t.Fatalf("write merged transcript: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(paths.InputsDir, "glossary.yml"), []byte("terms: []\n"), 0o644); err != nil {
|
||||
@@ -931,7 +931,7 @@ func TestAdapterBackedStageFailureMarksManifestFailed(t *testing.T) {
|
||||
if ensureErr != nil {
|
||||
t.Fatalf("EnsureLayout() error = %v", ensureErr)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(paths.TranscriptsDir, "processed.json"), []byte(`{"segments":[]}`), 0o644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(paths.TranscriptsDir, "polished.json"), []byte(`{"segments":[]}`), 0o644); err != nil {
|
||||
t.Fatalf("write processed transcript: %v", err)
|
||||
}
|
||||
cfg.Pipeline.Scriptorium = &config.ScriptoriumConfig{
|
||||
|
||||
84
internal/artifactmodel/transcripts.go
Normal file
84
internal/artifactmodel/transcripts.go
Normal 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
|
||||
}
|
||||
@@ -9,20 +9,35 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||
)
|
||||
|
||||
const (
|
||||
ArtifactTranscriptMerged = "narratio.transcript.merged"
|
||||
ArtifactTranscriptPolished = "narratio.transcript.polished"
|
||||
ArtifactTranscriptFull = "narratio.transcript.full"
|
||||
ArtifactTranscriptTrimmed = "narratio.transcript.trimmed"
|
||||
ArtifactBoundsSession = "narratio.bounds.session"
|
||||
ArtifactTranscriptBase = artifactmodel.SourceTranscriptBase
|
||||
ArtifactTranscriptPolished = artifactmodel.SourceTranscriptPolished
|
||||
ArtifactTranscriptFinal = artifactmodel.SourceTranscriptFinal
|
||||
ArtifactTranscriptFinalTrimmed = artifactmodel.SourceTranscriptFinalTrimmed
|
||||
ArtifactBoundsSession = "narratio.bounds.session"
|
||||
|
||||
ArtifactProvenancePreviousCacheManifestInput = "manifest.inputs.previous_cache"
|
||||
ArtifactProvenancePreviousCacheFilesystem = "current_session.previous_cache"
|
||||
)
|
||||
|
||||
const (
|
||||
TranscriptPathBase = artifactmodel.TranscriptPathBase
|
||||
TranscriptPathPolished = artifactmodel.TranscriptPathPolished
|
||||
TranscriptPathFinal = artifactmodel.TranscriptPathFinal
|
||||
TranscriptPathFinalTrimmed = artifactmodel.TranscriptPathFinalTrimmed
|
||||
)
|
||||
|
||||
const (
|
||||
TranscriptOutputKindBase = artifactmodel.TranscriptOutputKindBase
|
||||
TranscriptOutputKindPolished = artifactmodel.TranscriptOutputKindPolished
|
||||
TranscriptOutputKindFinal = artifactmodel.TranscriptOutputKindFinal
|
||||
TranscriptOutputKindFinalTrimmed = artifactmodel.TranscriptOutputKindFinalTrimmed
|
||||
)
|
||||
|
||||
// ErrSessionArtifactNotFound is returned when no readable artifact exists for a known ID.
|
||||
var ErrSessionArtifactNotFound = errors.New("session artifact not found")
|
||||
var configuredArtifactSourceRE = regexp.MustCompile(`^narratio\.artifact\.([a-z][a-z0-9_]*)$`)
|
||||
@@ -44,42 +59,27 @@ type artifactSpec struct {
|
||||
ContentKind artifactContentKind
|
||||
}
|
||||
|
||||
var artifactRegistry = map[string]artifactSpec{
|
||||
ArtifactTranscriptMerged: {
|
||||
ID: ArtifactTranscriptMerged,
|
||||
CanonicalRelPath: "transcripts/merged.json",
|
||||
ProducerStage: "merge",
|
||||
OutputKind: "transcript_merged",
|
||||
ContentKind: contentTranscriptJSON,
|
||||
},
|
||||
ArtifactTranscriptPolished: {
|
||||
ID: ArtifactTranscriptPolished,
|
||||
CanonicalRelPath: "transcripts/processed.json",
|
||||
ProducerStage: "polish",
|
||||
OutputKind: "transcript_processed",
|
||||
ContentKind: contentTranscriptJSON,
|
||||
},
|
||||
ArtifactTranscriptFull: {
|
||||
ID: ArtifactTranscriptFull,
|
||||
CanonicalRelPath: "transcripts/normalized.json",
|
||||
ProducerStage: "normalize",
|
||||
OutputKind: "transcript_normalized",
|
||||
ContentKind: contentTranscriptJSON,
|
||||
},
|
||||
ArtifactTranscriptTrimmed: {
|
||||
ID: ArtifactTranscriptTrimmed,
|
||||
CanonicalRelPath: "transcripts/trimmed.json",
|
||||
ProducerStage: "trim",
|
||||
OutputKind: "transcript_trimmed",
|
||||
ContentKind: contentTranscriptJSON,
|
||||
},
|
||||
ArtifactBoundsSession: {
|
||||
var artifactRegistry = buildArtifactRegistry()
|
||||
|
||||
func buildArtifactRegistry() map[string]artifactSpec {
|
||||
registry := map[string]artifactSpec{}
|
||||
for _, transcript := range RuntimeTranscriptArtifacts() {
|
||||
registry[transcript.SourceID] = artifactSpec{
|
||||
ID: transcript.SourceID,
|
||||
CanonicalRelPath: transcript.CanonicalRelPath,
|
||||
ProducerStage: transcript.ProducerStage,
|
||||
OutputKind: transcript.OutputKind,
|
||||
ContentKind: contentTranscriptJSON,
|
||||
}
|
||||
}
|
||||
registry[ArtifactBoundsSession] = artifactSpec{
|
||||
ID: ArtifactBoundsSession,
|
||||
CanonicalRelPath: "artifacts/session_bounds.json",
|
||||
ProducerStage: "trim",
|
||||
OutputKind: "session_bounds",
|
||||
ContentKind: contentJSON,
|
||||
},
|
||||
}
|
||||
return registry
|
||||
}
|
||||
|
||||
// ResolvedSessionArtifact describes one session-level artifact lookup result.
|
||||
|
||||
@@ -23,7 +23,7 @@ func TestNormalizeSessionArtifactSource(t *testing.T) {
|
||||
{name: "legacy alias normalized unsupported", source: "normalized_transcript", wantErr: "unsupported artifact source"},
|
||||
{name: "legacy alias trimmed unsupported", source: "trimmed_transcript", wantErr: "unsupported artifact source"},
|
||||
{name: "configured source unsupported in built-in normalization", source: "narratio.artifact.session_recap", wantErr: "unsupported artifact source"},
|
||||
{name: "canonical", source: ArtifactTranscriptTrimmed, wantID: ArtifactTranscriptTrimmed},
|
||||
{name: "canonical", source: ArtifactTranscriptFinalTrimmed, wantID: ArtifactTranscriptFinalTrimmed},
|
||||
{name: "unsupported", source: "narratio.unknown", wantErr: "unsupported artifact source"},
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ func TestConfiguredArtifactSourceHelpers(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "built-in",
|
||||
source: ArtifactTranscriptMerged,
|
||||
source: ArtifactTranscriptBase,
|
||||
wantMatch: false,
|
||||
},
|
||||
}
|
||||
@@ -160,7 +160,7 @@ func TestResolveSessionArtifactPrefersManifestOutput(t *testing.T) {
|
||||
if err := os.WriteFile(manifestPath, []byte(`{"segments":[]}`), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile() error = %v", err)
|
||||
}
|
||||
canonicalPath := filepath.Join(paths.TranscriptsDir, "normalized.json")
|
||||
canonicalPath := filepath.Join(paths.TranscriptsDir, "final.json")
|
||||
if err := os.MkdirAll(filepath.Dir(canonicalPath), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll() error = %v", err)
|
||||
}
|
||||
@@ -170,10 +170,10 @@ func TestResolveSessionArtifactPrefersManifestOutput(t *testing.T) {
|
||||
|
||||
m := manifest.New("session", time.Now().UTC())
|
||||
m.MarkStageSucceeded("normalize", time.Now().UTC(), []manifest.ArtifactRecord{
|
||||
{Kind: "transcript_normalized", LocalPath: manifestPath, ProducerRunID: "run-123"},
|
||||
{Kind: "transcript_final", LocalPath: manifestPath, ProducerRunID: "run-123"},
|
||||
})
|
||||
|
||||
resolved, err := ResolveSessionArtifact(paths, m, ArtifactTranscriptFull)
|
||||
resolved, err := ResolveSessionArtifact(paths, m, ArtifactTranscriptFinal)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveSessionArtifact() error = %v", err)
|
||||
}
|
||||
@@ -191,7 +191,7 @@ func TestResolveSessionArtifactPrefersManifestOutput(t *testing.T) {
|
||||
func TestResolveSessionArtifactFallsBackToCanonicalPath(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
paths := buildSessionPaths(workspace, "campaign", "session")
|
||||
canonicalPath := filepath.Join(paths.TranscriptsDir, "trimmed.json")
|
||||
canonicalPath := filepath.Join(paths.TranscriptsDir, "final.trimmed.json")
|
||||
if err := os.MkdirAll(filepath.Dir(canonicalPath), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll() error = %v", err)
|
||||
}
|
||||
@@ -199,7 +199,7 @@ func TestResolveSessionArtifactFallsBackToCanonicalPath(t *testing.T) {
|
||||
t.Fatalf("WriteFile() error = %v", err)
|
||||
}
|
||||
|
||||
resolved, err := ResolveSessionArtifact(paths, nil, ArtifactTranscriptTrimmed)
|
||||
resolved, err := ResolveSessionArtifact(paths, nil, ArtifactTranscriptFinalTrimmed)
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveSessionArtifact() error = %v", err)
|
||||
}
|
||||
@@ -215,7 +215,7 @@ func TestResolveSessionArtifactMissingReturnsTypedError(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
paths := buildSessionPaths(workspace, "campaign", "session")
|
||||
|
||||
_, err := ResolveSessionArtifact(paths, nil, ArtifactTranscriptTrimmed)
|
||||
_, err := ResolveSessionArtifact(paths, nil, ArtifactTranscriptFinalTrimmed)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
@@ -227,7 +227,7 @@ func TestResolveSessionArtifactMissingReturnsTypedError(t *testing.T) {
|
||||
func TestResolveSessionArtifactValidatesTranscriptShape(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
paths := buildSessionPaths(workspace, "campaign", "session")
|
||||
canonicalPath := filepath.Join(paths.TranscriptsDir, "processed.json")
|
||||
canonicalPath := filepath.Join(paths.TranscriptsDir, "polished.json")
|
||||
if err := os.MkdirAll(filepath.Dir(canonicalPath), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll() error = %v", err)
|
||||
}
|
||||
@@ -247,7 +247,7 @@ func TestResolveSessionArtifactValidatesTranscriptShape(t *testing.T) {
|
||||
func TestResolveSessionArtifactWithCatalogBuiltInBehaviorUnchanged(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
paths := buildSessionPaths(workspace, "campaign", "session")
|
||||
canonicalPath := filepath.Join(paths.TranscriptsDir, "trimmed.json")
|
||||
canonicalPath := filepath.Join(paths.TranscriptsDir, "final.trimmed.json")
|
||||
if err := os.MkdirAll(filepath.Dir(canonicalPath), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll() error = %v", err)
|
||||
}
|
||||
@@ -255,7 +255,7 @@ func TestResolveSessionArtifactWithCatalogBuiltInBehaviorUnchanged(t *testing.T)
|
||||
t.Fatalf("WriteFile() error = %v", err)
|
||||
}
|
||||
|
||||
resolved, err := ResolveSessionArtifactWithCatalog(paths, nil, ArtifactTranscriptTrimmed, NewArtifactCatalog())
|
||||
resolved, err := ResolveSessionArtifactWithCatalog(paths, nil, ArtifactTranscriptFinalTrimmed, NewArtifactCatalog())
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveSessionArtifactWithCatalog() error = %v", err)
|
||||
}
|
||||
|
||||
@@ -215,10 +215,10 @@ func (c *ArtifactCatalog) addEntry(entry CatalogEntry) error {
|
||||
|
||||
func runtimeBuiltInArtifactIDs() []string {
|
||||
return []string{
|
||||
ArtifactTranscriptMerged,
|
||||
ArtifactTranscriptBase,
|
||||
ArtifactTranscriptPolished,
|
||||
ArtifactTranscriptFull,
|
||||
ArtifactTranscriptTrimmed,
|
||||
ArtifactTranscriptFinal,
|
||||
ArtifactTranscriptFinalTrimmed,
|
||||
ArtifactBoundsSession,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ func TestArtifactCatalogRegisterBuiltInsAndLookup(t *testing.T) {
|
||||
t.Fatalf("RegisterBuiltIns() error = %v", err)
|
||||
}
|
||||
|
||||
entry, ok := catalog.Lookup(ArtifactTranscriptFull)
|
||||
entry, ok := catalog.Lookup(ArtifactTranscriptFinal)
|
||||
if !ok {
|
||||
t.Fatalf("Lookup(%q) ok = false, want true", ArtifactTranscriptFull)
|
||||
t.Fatalf("Lookup(%q) ok = false, want true", ArtifactTranscriptFinal)
|
||||
}
|
||||
if !entry.Planned {
|
||||
t.Fatalf("entry.Planned = false, want true")
|
||||
@@ -18,8 +18,8 @@ func TestArtifactCatalogRegisterBuiltInsAndLookup(t *testing.T) {
|
||||
if entry.Executable {
|
||||
t.Fatalf("entry.Executable = true, want false")
|
||||
}
|
||||
if entry.CanonicalRelPath != "transcripts/normalized.json" {
|
||||
t.Fatalf("entry.CanonicalRelPath = %q, want transcripts/normalized.json", entry.CanonicalRelPath)
|
||||
if entry.CanonicalRelPath != "transcripts/final.json" {
|
||||
t.Fatalf("entry.CanonicalRelPath = %q, want transcripts/final.json", entry.CanonicalRelPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestCollectPreviousArtifactRequirements(t *testing.T) {
|
||||
"session_recap": {
|
||||
Enabled: true,
|
||||
Inputs: map[string]config.ScriptoriumInputConfig{
|
||||
"transcript": {Source: "narratio.transcript.trimmed", Required: true},
|
||||
"transcript": {Source: "narratio.transcript.final_trimmed", Required: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -43,8 +43,8 @@ func TestS3KeyConstruction(t *testing.T) {
|
||||
t.Fatalf("manifest key = %q", manifestKey)
|
||||
}
|
||||
|
||||
promoted := S3PromotedArtifactKey(sessionPrefix, "transcripts/trimmed.json")
|
||||
if promoted != "dnd/campaigns/forsaken/sessions/2026-04-19/transcripts/trimmed.json" {
|
||||
promoted := S3PromotedArtifactKey(sessionPrefix, "transcripts/final.trimmed.json")
|
||||
if promoted != "dnd/campaigns/forsaken/sessions/2026-04-19/transcripts/final.trimmed.json" {
|
||||
t.Fatalf("promoted key = %q", promoted)
|
||||
}
|
||||
|
||||
|
||||
25
internal/artifacts/transcripts.go
Normal file
25
internal/artifacts/transcripts.go
Normal 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)
|
||||
}
|
||||
123
internal/artifacts/transcripts_test.go
Normal file
123
internal/artifacts/transcripts_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package config
|
||||
|
||||
import "gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
|
||||
|
||||
// Default filesystem locations for config lookup when config path flags are
|
||||
// omitted. Order is highest to lowest precedence.
|
||||
const (
|
||||
@@ -40,32 +42,32 @@ const (
|
||||
DefaultTrimBoundsTimeout = "10m"
|
||||
DefaultTrimSeriatimReport = false
|
||||
|
||||
DefaultNormalizeOutputPath = "transcripts/normalized.json"
|
||||
DefaultNormalizeOutputPath = artifactmodel.TranscriptPathFinal
|
||||
DefaultNormalizeOutputSchema = "seriatim-intermediate"
|
||||
DefaultNormalizeReport = true
|
||||
|
||||
DefaultArchiveEnabled = true
|
||||
DefaultArchiveUploadRun = true
|
||||
|
||||
PathWorkDirSegment = "work"
|
||||
PathInputsDirSegment = "inputs"
|
||||
PathAudioDirSegment = "audio"
|
||||
PathTranscriptsSegment = "transcripts"
|
||||
PathTranscriptsRaw = "transcripts/raw"
|
||||
PathTranscriptsTrimmed = "transcripts/trimmed"
|
||||
PathArtifactsDirSegment = "artifacts"
|
||||
PathReportsDirSegment = "reports"
|
||||
PathConfigDirSegment = "config"
|
||||
PathLogsDirSegment = "logs"
|
||||
PathCurrentDirSegment = "current"
|
||||
PathRunsDirSegment = "runs"
|
||||
PathPreviousDirSegment = "previous"
|
||||
PathManifestFile = "manifest.json"
|
||||
PathLockFile = ".lock"
|
||||
PathTranscriptMerged = "transcripts/merged.json"
|
||||
PathTranscriptProcessed = "transcripts/processed.json"
|
||||
PathTranscriptNormalized = "transcripts/normalized.json"
|
||||
PathTranscriptTrimmed = "transcripts/trimmed.json"
|
||||
PathWorkDirSegment = "work"
|
||||
PathInputsDirSegment = "inputs"
|
||||
PathAudioDirSegment = "audio"
|
||||
PathTranscriptsSegment = "transcripts"
|
||||
PathTranscriptsRaw = "transcripts/raw"
|
||||
PathTranscriptsTrimmed = "transcripts/trimmed"
|
||||
PathArtifactsDirSegment = "artifacts"
|
||||
PathReportsDirSegment = "reports"
|
||||
PathConfigDirSegment = "config"
|
||||
PathLogsDirSegment = "logs"
|
||||
PathCurrentDirSegment = "current"
|
||||
PathRunsDirSegment = "runs"
|
||||
PathPreviousDirSegment = "previous"
|
||||
PathManifestFile = "manifest.json"
|
||||
PathLockFile = ".lock"
|
||||
PathTranscriptBase = artifactmodel.TranscriptPathBase
|
||||
PathTranscriptPolished = artifactmodel.TranscriptPathPolished
|
||||
PathTranscriptFinal = artifactmodel.TranscriptPathFinal
|
||||
PathTranscriptFinalTrimmed = artifactmodel.TranscriptPathFinalTrimmed
|
||||
|
||||
S3CampaignsSegment = "campaigns"
|
||||
S3SessionsSegment = "sessions"
|
||||
@@ -78,7 +80,7 @@ const (
|
||||
// DefaultArchivePromoteArtifacts defines the default archive promotion rules.
|
||||
// Callers should copy this slice before mutating.
|
||||
var DefaultArchivePromoteArtifacts = []ArchivePromotionRule{
|
||||
{Source: "narratio.transcript.trimmed", Dest: PathTranscriptTrimmed},
|
||||
{Source: artifactmodel.SourceTranscriptFinalTrimmed, Dest: PathTranscriptFinalTrimmed},
|
||||
}
|
||||
|
||||
// DefaultPipelineConfigSearchPaths defines the default search order for
|
||||
|
||||
@@ -853,8 +853,8 @@ inputs:
|
||||
if cfg.Pipeline.Normalize == nil {
|
||||
t.Fatal("normalize config should be present via defaults")
|
||||
}
|
||||
if cfg.Pipeline.Normalize.OutputPath != "transcripts/normalized.json" {
|
||||
t.Fatalf("normalize.output_path = %q, want %q", cfg.Pipeline.Normalize.OutputPath, "transcripts/normalized.json")
|
||||
if cfg.Pipeline.Normalize.OutputPath != "transcripts/final.json" {
|
||||
t.Fatalf("normalize.output_path = %q, want %q", cfg.Pipeline.Normalize.OutputPath, "transcripts/final.json")
|
||||
}
|
||||
if cfg.Pipeline.Normalize.OutputSchema != "seriatim-intermediate" {
|
||||
t.Fatalf("normalize.output_schema = %q, want %q", cfg.Pipeline.Normalize.OutputSchema, "seriatim-intermediate")
|
||||
|
||||
@@ -21,8 +21,8 @@ func TestNormalizeLoadAndValidate(t *testing.T) {
|
||||
if cfg.Pipeline.Normalize == nil {
|
||||
t.Fatal("normalize config should be present via defaults")
|
||||
}
|
||||
if cfg.Pipeline.Normalize.OutputPath != "transcripts/normalized.json" {
|
||||
t.Fatalf("normalize.output_path = %q, want %q", cfg.Pipeline.Normalize.OutputPath, "transcripts/normalized.json")
|
||||
if cfg.Pipeline.Normalize.OutputPath != "transcripts/final.json" {
|
||||
t.Fatalf("normalize.output_path = %q, want %q", cfg.Pipeline.Normalize.OutputPath, "transcripts/final.json")
|
||||
}
|
||||
if cfg.Pipeline.Normalize.OutputSchema != "seriatim-intermediate" {
|
||||
t.Fatalf("normalize.output_schema = %q, want %q", cfg.Pipeline.Normalize.OutputSchema, "seriatim-intermediate")
|
||||
@@ -58,7 +58,7 @@ func TestNormalizeLoadAndValidate(t *testing.T) {
|
||||
{
|
||||
name: "invalid normalize output schema fails",
|
||||
normalizeYAML: `normalize:
|
||||
output_path: transcripts/normalized.json
|
||||
output_path: transcripts/final.json
|
||||
output_schema: not-a-schema
|
||||
report: true
|
||||
`,
|
||||
@@ -76,7 +76,7 @@ func TestNormalizeLoadAndValidate(t *testing.T) {
|
||||
{
|
||||
name: "unknown normalize field fails strict decoding",
|
||||
normalizeYAML: `normalize:
|
||||
output_path: transcripts/normalized.json
|
||||
output_path: transcripts/final.json
|
||||
output_schema: seriatim-intermediate
|
||||
report: true
|
||||
bogus: true
|
||||
|
||||
@@ -198,7 +198,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
|
||||
output_path: artifacts/session_recap.md
|
||||
inputs:
|
||||
transcript:
|
||||
source: narratio.transcript.trimmed
|
||||
source: narratio.transcript.final_trimmed
|
||||
required: true
|
||||
`,
|
||||
},
|
||||
@@ -287,7 +287,7 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
|
||||
output_path: artifacts/session_recap.md
|
||||
inputs:
|
||||
transcript:
|
||||
source: narratio.transcript.trimmed
|
||||
source: narratio.transcript.final_trimmed
|
||||
required: true
|
||||
player_handout:
|
||||
enabled: true
|
||||
@@ -507,6 +507,42 @@ func TestScriptoriumLoadAndValidate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestScriptoriumLegacyTranscriptSourcesRejected(t *testing.T) {
|
||||
legacyTranscriptSources := []string{
|
||||
"narratio.transcript." + "merged",
|
||||
"narratio.transcript." + "full",
|
||||
"narratio.transcript." + "trimmed",
|
||||
}
|
||||
|
||||
for _, source := range legacyTranscriptSources {
|
||||
t.Run(source, func(t *testing.T) {
|
||||
pipelineYAML := testPipelineBaseYAML + `
|
||||
scriptorium:
|
||||
binary: scriptorium
|
||||
artifacts:
|
||||
session_recap:
|
||||
enabled: true
|
||||
prompt_id: dnd.session_recap
|
||||
output_path: artifacts/session_recap.md
|
||||
inputs:
|
||||
transcript:
|
||||
source: ` + source + `
|
||||
required: true
|
||||
`
|
||||
pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML)
|
||||
cfg, err := Load(pipelinePath, sessionPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
err = Validate(cfg)
|
||||
wantErr := `pipeline.scriptorium.artifacts.session_recap.inputs.transcript.source "` + source + `" is unsupported`
|
||||
if err == nil || !strings.Contains(err.Error(), wantErr) {
|
||||
t.Fatalf("Validate() error = %v, want to contain %q", err, wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const testPipelineBaseYAML = `workspace:
|
||||
root: /tmp/narratio
|
||||
whisperx:
|
||||
|
||||
@@ -186,11 +186,11 @@ func TestSpoolAndArchiveDefaults(t *testing.T) {
|
||||
if item.Required == nil || !*item.Required {
|
||||
t.Fatalf("archive.promote_artifacts[0].required = %#v, want true", item.Required)
|
||||
}
|
||||
if item.Source != "narratio.transcript.trimmed" {
|
||||
t.Fatalf("archive.promote_artifacts[0].source = %q, want narratio.transcript.trimmed", item.Source)
|
||||
if item.Source != "narratio.transcript.final_trimmed" {
|
||||
t.Fatalf("archive.promote_artifacts[0].source = %q, want narratio.transcript.final_trimmed", item.Source)
|
||||
}
|
||||
if item.Dest != "transcripts/trimmed.json" {
|
||||
t.Fatalf("archive.promote_artifacts[0].dest = %q, want transcripts/trimmed.json", item.Dest)
|
||||
if item.Dest != "transcripts/final.trimmed.json" {
|
||||
t.Fatalf("archive.promote_artifacts[0].dest = %q, want transcripts/final.trimmed.json", item.Dest)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,8 +204,8 @@ func TestArchivePromotionValidation(t *testing.T) {
|
||||
name: "absolute dest path rejected",
|
||||
ruleYML: `archive:
|
||||
promote_artifacts:
|
||||
- source: "narratio.transcript.trimmed"
|
||||
dest: "/transcripts/trimmed.json"
|
||||
- source: "narratio.transcript.final_trimmed"
|
||||
dest: "/transcripts/final.trimmed.json"
|
||||
`,
|
||||
wantErr: "must be a relative path",
|
||||
},
|
||||
@@ -213,7 +213,7 @@ func TestArchivePromotionValidation(t *testing.T) {
|
||||
name: "traversal dest path rejected",
|
||||
ruleYML: `archive:
|
||||
promote_artifacts:
|
||||
- source: "narratio.transcript.trimmed"
|
||||
- source: "narratio.transcript.final_trimmed"
|
||||
dest: "../trimmed.json"
|
||||
`,
|
||||
wantErr: "must not contain path traversal",
|
||||
@@ -223,7 +223,7 @@ func TestArchivePromotionValidation(t *testing.T) {
|
||||
ruleYML: `archive:
|
||||
promote_artifacts:
|
||||
- source: "narratio.unknown"
|
||||
dest: "transcripts/trimmed.json"
|
||||
dest: "transcripts/final.trimmed.json"
|
||||
`,
|
||||
wantErr: "source \"narratio.unknown\" is unsupported",
|
||||
},
|
||||
@@ -231,9 +231,9 @@ func TestArchivePromotionValidation(t *testing.T) {
|
||||
name: "duplicate destination rejected",
|
||||
ruleYML: `archive:
|
||||
promote_artifacts:
|
||||
- source: "narratio.transcript.trimmed"
|
||||
- source: "narratio.transcript.final_trimmed"
|
||||
dest: "artifacts/shared.md"
|
||||
- source: "narratio.transcript.full"
|
||||
- source: "narratio.transcript.final"
|
||||
dest: "artifacts/shared.md"
|
||||
`,
|
||||
wantErr: "duplicates another archive promotion destination",
|
||||
@@ -278,6 +278,35 @@ archive:
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchivePromotionLegacyTranscriptSourcesRejected(t *testing.T) {
|
||||
legacyTranscriptSources := []string{
|
||||
"narratio.transcript." + "merged",
|
||||
"narratio.transcript." + "full",
|
||||
"narratio.transcript." + "trimmed",
|
||||
}
|
||||
|
||||
for _, source := range legacyTranscriptSources {
|
||||
t.Run(source, func(t *testing.T) {
|
||||
pipelineYAML := testPipelineBaseYAML + `
|
||||
archive:
|
||||
promote_artifacts:
|
||||
- source: ` + source + `
|
||||
dest: transcripts/final.trimmed.json
|
||||
`
|
||||
pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML)
|
||||
cfg, err := Load(pipelinePath, sessionPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
err = Validate(cfg)
|
||||
wantErr := `source "` + source + `" is unsupported`
|
||||
if err == nil || !strings.Contains(err.Error(), wantErr) {
|
||||
t.Fatalf("Validate() error = %v, want to contain %q", err, wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchivePromotionDerivesDestinationWhenOmitted(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -289,9 +318,9 @@ func TestArchivePromotionDerivesDestinationWhenOmitted(t *testing.T) {
|
||||
pipelineYML: testPipelineBaseYAML + `
|
||||
archive:
|
||||
promote_artifacts:
|
||||
- source: narratio.transcript.full
|
||||
- source: narratio.transcript.final
|
||||
`,
|
||||
wantDest: "transcripts/normalized.json",
|
||||
wantDest: "transcripts/final.json",
|
||||
},
|
||||
{
|
||||
name: "configured source derives configured output path",
|
||||
@@ -341,7 +370,7 @@ func TestArchiveLockValidation(t *testing.T) {
|
||||
pipelineYML: testPipelineBaseYAML + `
|
||||
archive:
|
||||
locks:
|
||||
- source: narratio.transcript.trimmed
|
||||
- source: narratio.transcript.final_trimmed
|
||||
reason: reviewed transcript
|
||||
`,
|
||||
},
|
||||
@@ -382,8 +411,8 @@ archive:
|
||||
pipelineYML: testPipelineBaseYAML + `
|
||||
archive:
|
||||
locks:
|
||||
- source: narratio.transcript.trimmed
|
||||
- source: " narratio.transcript.trimmed "
|
||||
- source: narratio.transcript.final_trimmed
|
||||
- source: " narratio.transcript.final_trimmed "
|
||||
`,
|
||||
wantErr: "duplicates another archive lock source",
|
||||
},
|
||||
@@ -410,12 +439,40 @@ archive:
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveLockLegacyTranscriptSourcesRejected(t *testing.T) {
|
||||
legacyTranscriptSources := []string{
|
||||
"narratio.transcript." + "merged",
|
||||
"narratio.transcript." + "full",
|
||||
"narratio.transcript." + "trimmed",
|
||||
}
|
||||
|
||||
for _, source := range legacyTranscriptSources {
|
||||
t.Run(source, func(t *testing.T) {
|
||||
pipelineYAML := testPipelineBaseYAML + `
|
||||
archive:
|
||||
locks:
|
||||
- source: ` + source + `
|
||||
`
|
||||
pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML)
|
||||
cfg, err := Load(pipelinePath, sessionPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
err = Validate(cfg)
|
||||
wantErr := `source "` + source + `" is unsupported`
|
||||
if err == nil || !strings.Contains(err.Error(), wantErr) {
|
||||
t.Fatalf("Validate() error = %v, want to contain %q", err, wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveLockUnknownFieldFailsStrictDecode(t *testing.T) {
|
||||
pipelineYAML := testPipelineBaseYAML + `
|
||||
archive:
|
||||
locks:
|
||||
- source: narratio.transcript.trimmed
|
||||
dest: transcripts/trimmed.json
|
||||
- source: narratio.transcript.final_trimmed
|
||||
dest: transcripts/final.trimmed.json
|
||||
`
|
||||
pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML)
|
||||
_, err := Load(pipelinePath, sessionPath)
|
||||
@@ -428,8 +485,8 @@ func TestArchiveLegacyFromToFailsStrictDecode(t *testing.T) {
|
||||
pipelineYAML := testPipelineBaseYAML + `
|
||||
archive:
|
||||
promote_artifacts:
|
||||
- from: transcripts/trimmed.json
|
||||
to: transcripts/trimmed.json
|
||||
- from: transcripts/final.trimmed.json
|
||||
to: transcripts/final.trimmed.json
|
||||
`
|
||||
pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, testSessionBaseYAML)
|
||||
_, err := Load(pipelinePath, sessionPath)
|
||||
@@ -440,27 +497,27 @@ archive:
|
||||
|
||||
func TestArchiveLockStoreBytesStrictDecodeAndValidation(t *testing.T) {
|
||||
store, err := LoadArchiveLockStoreBytes("locks.yml", []byte(`locks:
|
||||
- source: narratio.transcript.trimmed
|
||||
- source: narratio.transcript.final_trimmed
|
||||
reason: reviewed
|
||||
`), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadArchiveLockStoreBytes() error = %v", err)
|
||||
}
|
||||
if len(store.Locks) != 1 || store.Locks[0].Source != "narratio.transcript.trimmed" || store.Locks[0].Reason != "reviewed" {
|
||||
if len(store.Locks) != 1 || store.Locks[0].Source != "narratio.transcript.final_trimmed" || store.Locks[0].Reason != "reviewed" {
|
||||
t.Fatalf("locks = %#v", store.Locks)
|
||||
}
|
||||
|
||||
_, err = LoadArchiveLockStoreBytes("locks.yml", []byte(`locks:
|
||||
- source: narratio.transcript.trimmed
|
||||
dest: transcripts/trimmed.json
|
||||
- source: narratio.transcript.final_trimmed
|
||||
dest: transcripts/final.trimmed.json
|
||||
`), nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "strict decode failed") {
|
||||
t.Fatalf("unknown field error = %v, want strict decode failed", err)
|
||||
}
|
||||
|
||||
_, err = LoadArchiveLockStoreBytes("locks.yml", []byte(`locks:
|
||||
- source: narratio.transcript.trimmed
|
||||
- source: narratio.transcript.trimmed
|
||||
- source: narratio.transcript.final_trimmed
|
||||
- source: narratio.transcript.final_trimmed
|
||||
`), nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "duplicates another archive lock source") {
|
||||
t.Fatalf("duplicate error = %v", err)
|
||||
@@ -469,19 +526,19 @@ func TestArchiveLockStoreBytesStrictDecodeAndValidation(t *testing.T) {
|
||||
|
||||
func TestMergeArchiveLockRulesStaticWins(t *testing.T) {
|
||||
merged := MergeArchiveLockRules(
|
||||
[]ArchiveLockRule{{Source: "narratio.transcript.trimmed", Reason: "static"}},
|
||||
[]ArchiveLockRule{{Source: "narratio.transcript.final_trimmed", Reason: "static"}},
|
||||
[]ArchiveLockRule{
|
||||
{Source: "narratio.transcript.trimmed", Reason: "remote"},
|
||||
{Source: "narratio.transcript.full", Reason: "remote full"},
|
||||
{Source: "narratio.transcript.final_trimmed", Reason: "remote"},
|
||||
{Source: "narratio.transcript.final", Reason: "remote full"},
|
||||
},
|
||||
)
|
||||
if len(merged) != 2 {
|
||||
t.Fatalf("merged len = %d, want 2: %#v", len(merged), merged)
|
||||
}
|
||||
if merged[0].Source != "narratio.transcript.trimmed" || merged[0].Reason != "static" {
|
||||
if merged[0].Source != "narratio.transcript.final_trimmed" || merged[0].Reason != "static" {
|
||||
t.Fatalf("merged[0] = %#v, want static lock", merged[0])
|
||||
}
|
||||
if merged[1].Source != "narratio.transcript.full" {
|
||||
if merged[1].Source != "narratio.transcript.final" {
|
||||
t.Fatalf("merged[1] = %#v, want remote full lock", merged[1])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ func TestTrimLoadAndValidate(t *testing.T) {
|
||||
name: "valid trim config",
|
||||
trimYAML: `trim:
|
||||
enabled: true
|
||||
output_path: transcripts/trimmed.json
|
||||
output_path: transcripts/final.trimmed.json
|
||||
bounds:
|
||||
prompt_id: dnd_session.bounds
|
||||
profile_id: ""
|
||||
@@ -45,7 +45,7 @@ func TestTrimLoadAndValidate(t *testing.T) {
|
||||
{
|
||||
name: "enabled omitted defaults disabled",
|
||||
trimYAML: `trim:
|
||||
output_path: transcripts/trimmed.json
|
||||
output_path: transcripts/final.trimmed.json
|
||||
bounds:
|
||||
prompt_id: dnd_session.bounds
|
||||
transcript_input_name: transcript
|
||||
@@ -65,7 +65,7 @@ func TestTrimLoadAndValidate(t *testing.T) {
|
||||
name: "missing prompt id fails when enabled",
|
||||
trimYAML: `trim:
|
||||
enabled: true
|
||||
output_path: transcripts/trimmed.json
|
||||
output_path: transcripts/final.trimmed.json
|
||||
bounds:
|
||||
transcript_input_name: transcript
|
||||
output_path: artifacts/session_bounds.json
|
||||
@@ -76,7 +76,7 @@ func TestTrimLoadAndValidate(t *testing.T) {
|
||||
name: "missing transcript input name fails when enabled",
|
||||
trimYAML: `trim:
|
||||
enabled: true
|
||||
output_path: transcripts/trimmed.json
|
||||
output_path: transcripts/final.trimmed.json
|
||||
bounds:
|
||||
prompt_id: dnd_session.bounds
|
||||
output_path: artifacts/session_bounds.json
|
||||
@@ -87,7 +87,7 @@ func TestTrimLoadAndValidate(t *testing.T) {
|
||||
name: "missing bounds output path fails when enabled",
|
||||
trimYAML: `trim:
|
||||
enabled: true
|
||||
output_path: transcripts/trimmed.json
|
||||
output_path: transcripts/final.trimmed.json
|
||||
bounds:
|
||||
prompt_id: dnd_session.bounds
|
||||
transcript_input_name: transcript
|
||||
@@ -109,7 +109,7 @@ func TestTrimLoadAndValidate(t *testing.T) {
|
||||
name: "invalid timeout fails",
|
||||
trimYAML: `trim:
|
||||
enabled: true
|
||||
output_path: transcripts/trimmed.json
|
||||
output_path: transcripts/final.trimmed.json
|
||||
bounds:
|
||||
prompt_id: dnd_session.bounds
|
||||
transcript_input_name: transcript
|
||||
@@ -122,7 +122,7 @@ func TestTrimLoadAndValidate(t *testing.T) {
|
||||
name: "render debug true requires render output path",
|
||||
trimYAML: `trim:
|
||||
enabled: true
|
||||
output_path: transcripts/trimmed.json
|
||||
output_path: transcripts/final.trimmed.json
|
||||
bounds:
|
||||
prompt_id: dnd_session.bounds
|
||||
transcript_input_name: transcript
|
||||
@@ -135,7 +135,7 @@ func TestTrimLoadAndValidate(t *testing.T) {
|
||||
name: "unknown trim field fails strict decoding",
|
||||
trimYAML: `trim:
|
||||
enabled: true
|
||||
output_path: transcripts/trimmed.json
|
||||
output_path: transcripts/final.trimmed.json
|
||||
bounds:
|
||||
prompt_id: dnd_session.bounds
|
||||
transcript_input_name: transcript
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
|
||||
)
|
||||
|
||||
// Validate checks resolved configuration for required fields and parseable durations.
|
||||
@@ -224,12 +226,11 @@ func MergeArchiveLockRules(staticLocks, remoteLocks []ArchiveLockRule) []Archive
|
||||
|
||||
func archiveSourceKnown(source string, scriptorium *ScriptoriumConfig) (string, error) {
|
||||
trimmed := strings.TrimSpace(source)
|
||||
if _, ok := artifactmodel.LookupRuntimeTranscriptArtifact(trimmed); ok {
|
||||
return "", nil
|
||||
}
|
||||
switch trimmed {
|
||||
case "narratio.transcript.merged",
|
||||
"narratio.transcript.polished",
|
||||
"narratio.transcript.full",
|
||||
"narratio.transcript.trimmed",
|
||||
"narratio.bounds.session":
|
||||
case "narratio.bounds.session":
|
||||
return "", nil
|
||||
}
|
||||
matches := narratioArtifactSourceRE.FindStringSubmatch(trimmed)
|
||||
@@ -248,15 +249,10 @@ func archiveSourceKnown(source string, scriptorium *ScriptoriumConfig) (string,
|
||||
|
||||
func deriveArchivePromotionDest(source string, scriptorium *ScriptoriumConfig) (string, error) {
|
||||
trimmed := strings.TrimSpace(source)
|
||||
if spec, ok := artifactmodel.LookupRuntimeTranscriptArtifact(trimmed); ok {
|
||||
return spec.CanonicalRelPath, nil
|
||||
}
|
||||
switch trimmed {
|
||||
case "narratio.transcript.merged":
|
||||
return PathTranscriptMerged, nil
|
||||
case "narratio.transcript.polished":
|
||||
return PathTranscriptProcessed, nil
|
||||
case "narratio.transcript.full":
|
||||
return PathTranscriptNormalized, nil
|
||||
case "narratio.transcript.trimmed":
|
||||
return PathTranscriptTrimmed, nil
|
||||
case "narratio.bounds.session":
|
||||
return filepath.ToSlash(filepath.Join(PathArtifactsDirSegment, "session_bounds.json")), nil
|
||||
}
|
||||
@@ -716,15 +712,10 @@ func validateScriptoriumInputSource(artifactName, inputName, source string, conf
|
||||
}
|
||||
|
||||
func isStaticSupportedScriptoriumInputSource(source string) bool {
|
||||
if _, ok := artifactmodel.LookupRuntimeTranscriptArtifact(source); ok {
|
||||
return true
|
||||
}
|
||||
switch source {
|
||||
case "narratio.transcript.merged":
|
||||
return true
|
||||
case "narratio.transcript.polished":
|
||||
return true
|
||||
case "narratio.transcript.full":
|
||||
return true
|
||||
case "narratio.transcript.trimmed":
|
||||
return true
|
||||
case "narratio.bounds.session":
|
||||
return true
|
||||
default:
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
func TestValidBoundsOutputProducesKeepSelector(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
boundsPath := filepath.Join(dir, "bounds.json")
|
||||
transcriptPath := filepath.Join(dir, "processed.json")
|
||||
transcriptPath := filepath.Join(dir, "polished.json")
|
||||
|
||||
writeBoundsTestFile(t, boundsPath, `{
|
||||
"confidence":"high",
|
||||
@@ -103,7 +103,7 @@ func TestBoundsNonExistentEndIDFails(t *testing.T) {
|
||||
|
||||
func TestInvalidTranscriptJSONFails(t *testing.T) {
|
||||
bounds := SessionBounds{TrimAction: "trim", StartSegmentID: intPtr(1), EndSegmentID: intPtr(2)}
|
||||
path := filepath.Join(t.TempDir(), "processed.json")
|
||||
path := filepath.Join(t.TempDir(), "polished.json")
|
||||
writeBoundsTestFile(t, path, "not-json")
|
||||
|
||||
err := ValidateSessionBoundsAgainstTranscript(bounds, path)
|
||||
@@ -117,7 +117,7 @@ func TestInvalidTranscriptJSONFails(t *testing.T) {
|
||||
|
||||
func TestTranscriptWithoutSegmentsFails(t *testing.T) {
|
||||
bounds := SessionBounds{TrimAction: "trim", StartSegmentID: intPtr(1), EndSegmentID: intPtr(2)}
|
||||
path := filepath.Join(t.TempDir(), "processed.json")
|
||||
path := filepath.Join(t.TempDir(), "polished.json")
|
||||
writeBoundsTestFile(t, path, `{"schema":"audita.processed.v1"}`)
|
||||
|
||||
err := ValidateSessionBoundsAgainstTranscript(bounds, path)
|
||||
@@ -170,7 +170,7 @@ func TestNoTrimActionCopyIsSupported(t *testing.T) {
|
||||
|
||||
func writeTranscriptWithIDs(t *testing.T, ids ...int) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "processed.json")
|
||||
path := filepath.Join(t.TempDir(), "polished.json")
|
||||
if len(ids) == 0 {
|
||||
writeBoundsTestFile(t, path, `{"segments":[]}`)
|
||||
return path
|
||||
|
||||
@@ -23,7 +23,7 @@ func TestStageMarkHelpers(t *testing.T) {
|
||||
}
|
||||
|
||||
succeededAt := runningAt.Add(2 * time.Minute)
|
||||
outputs := []ArtifactRecord{{Kind: "transcript_processed", LocalPath: "transcripts/processed.json"}}
|
||||
outputs := []ArtifactRecord{{Kind: "transcript_polished", LocalPath: "transcripts/polished.json"}}
|
||||
m.MarkStageSucceeded("transcribe", succeededAt, outputs)
|
||||
if stage.Status != StatusSucceeded {
|
||||
t.Fatalf("status = %q, want %q", stage.Status, StatusSucceeded)
|
||||
@@ -35,7 +35,7 @@ func TestStageMarkHelpers(t *testing.T) {
|
||||
t.Fatalf("outputs len = %d, want 1", len(stage.Outputs))
|
||||
}
|
||||
outputs[0].LocalPath = "mutated.json"
|
||||
if stage.Outputs[0].LocalPath != "transcripts/processed.json" {
|
||||
if stage.Outputs[0].LocalPath != "transcripts/polished.json" {
|
||||
t.Fatalf("stage outputs should be copied, got %#v", stage.Outputs)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ func TestLocalStoreCreateSaveLoadRoundTrip(t *testing.T) {
|
||||
|
||||
now := time.Date(2026, 5, 3, 12, 0, 0, 0, time.UTC)
|
||||
m.MarkStageRunning("prepare", now)
|
||||
m.MarkStageSucceeded("prepare", now.Add(2*time.Second), []ArtifactRecord{{Kind: "transcript", LocalPath: "transcripts/merged.json"}})
|
||||
m.MarkStageSucceeded("prepare", now.Add(2*time.Second), []ArtifactRecord{{Kind: "transcript", LocalPath: "transcripts/base.json"}})
|
||||
m.Campaign = "forsaken"
|
||||
m.RunID = "20260515T031522Z-a1b2c3d4"
|
||||
m.LocalWorkDir = "/var/lib/narratio/work/forsaken/2026-05-03/20260515T031522Z-a1b2c3d4"
|
||||
|
||||
@@ -24,9 +24,9 @@ func (analyzeStage) Name() string { return "analyze" }
|
||||
func (analyzeStage) Declares() IODecl {
|
||||
return IODecl{
|
||||
Inputs: []artifacts.Ref{
|
||||
{Kind: "transcript_processed", Category: "transcripts", RelativePath: "transcripts/processed.json"},
|
||||
{Kind: "transcript_normalized", Category: "transcripts", RelativePath: "transcripts/normalized.json"},
|
||||
{Kind: "transcript_trimmed", Category: "transcripts", RelativePath: "transcripts/trimmed.json"},
|
||||
{Kind: "transcript_polished", Category: "transcripts", RelativePath: "transcripts/polished.json"},
|
||||
{Kind: "transcript_final", Category: "transcripts", RelativePath: "transcripts/final.json"},
|
||||
{Kind: "transcript_final_trimmed", Category: "transcripts", RelativePath: "transcripts/final.trimmed.json"},
|
||||
},
|
||||
Outputs: nil,
|
||||
}
|
||||
@@ -562,7 +562,7 @@ func discoverProcessedTranscript(m *manifest.Manifest, paths artifacts.SessionPa
|
||||
if m != nil && m.Stages != nil {
|
||||
if sr := m.Stages["polish"]; sr != nil {
|
||||
for _, out := range sr.Outputs {
|
||||
if out.Kind != "transcript_processed" {
|
||||
if out.Kind != "transcript_polished" {
|
||||
continue
|
||||
}
|
||||
p := strings.TrimSpace(out.LocalPath)
|
||||
@@ -581,7 +581,7 @@ func discoverProcessedTranscript(m *manifest.Manifest, paths artifacts.SessionPa
|
||||
}
|
||||
}
|
||||
|
||||
fallback := filepath.Join(paths.TranscriptsDir, "processed.json")
|
||||
fallback := filepath.Join(paths.TranscriptsDir, "polished.json")
|
||||
if info, err := os.Stat(fallback); err == nil && !info.IsDir() {
|
||||
return filepath.Clean(fallback), "fallback.transcripts_dir", nil
|
||||
}
|
||||
@@ -602,8 +602,8 @@ type analyzeTranscriptInputs struct {
|
||||
|
||||
func discoverAnalyzeTranscriptRefs(m *manifest.Manifest, paths artifacts.SessionPaths) analyzeTranscriptInputs {
|
||||
processedPath, processedSource := discoverAnalyzeArtifactRef(m, paths, artifacts.ArtifactTranscriptPolished)
|
||||
normalizedPath, normalizedSource := discoverAnalyzeArtifactRef(m, paths, artifacts.ArtifactTranscriptFull)
|
||||
trimmedPath, trimmedSource := discoverAnalyzeArtifactRef(m, paths, artifacts.ArtifactTranscriptTrimmed)
|
||||
normalizedPath, normalizedSource := discoverAnalyzeArtifactRef(m, paths, artifacts.ArtifactTranscriptFinal)
|
||||
trimmedPath, trimmedSource := discoverAnalyzeArtifactRef(m, paths, artifacts.ArtifactTranscriptFinalTrimmed)
|
||||
return analyzeTranscriptInputs{
|
||||
ProcessedPath: processedPath,
|
||||
ProcessedSource: processedSource,
|
||||
@@ -669,9 +669,9 @@ func resolveScriptoriumInput(
|
||||
switch normalized {
|
||||
case artifacts.ArtifactTranscriptPolished:
|
||||
return "", false, nil, nil
|
||||
case artifacts.ArtifactTranscriptFull:
|
||||
case artifacts.ArtifactTranscriptFinal:
|
||||
return "", false, nil, fmt.Errorf("normalized transcript input is unavailable; run normalize stage first")
|
||||
case artifacts.ArtifactTranscriptTrimmed:
|
||||
case artifacts.ArtifactTranscriptFinalTrimmed:
|
||||
return "", false, nil, fmt.Errorf("trimmed transcript input is unavailable; run trim stage first")
|
||||
default:
|
||||
return "", false, nil, nil
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
func TestAnalyzeGeneratesSessionRecapFromTrimmedTranscript(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
|
||||
result, err := (analyzeStage{}).Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
@@ -36,7 +36,7 @@ func TestAnalyzeGeneratesSessionRecapFromTrimmedTranscript(t *testing.T) {
|
||||
if req.ProfileID != "local-quality" {
|
||||
t.Fatalf("profile id = %q, want local-quality", req.ProfileID)
|
||||
}
|
||||
if req.InputPaths["transcript"] != filepath.Join(paths.TranscriptsDir, "trimmed.json") {
|
||||
if req.InputPaths["transcript"] != filepath.Join(paths.TranscriptsDir, "final.trimmed.json") {
|
||||
t.Fatalf("transcript input = %q, want trimmed transcript path", req.InputPaths["transcript"])
|
||||
}
|
||||
if req.OutputPath != filepath.Join(paths.ArtifactsDir, "session_recap.md") {
|
||||
@@ -72,7 +72,7 @@ func TestAnalyzeGeneratesSessionRecapFromTrimmedTranscript(t *testing.T) {
|
||||
func TestAnalyzeRenderDebugFalseDoesNotCallRenderArtifact(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
|
||||
env.Config.Pipeline.Scriptorium.RenderDebug = false
|
||||
|
||||
@@ -88,7 +88,7 @@ func TestAnalyzeRenderDebugFalseDoesNotCallRenderArtifact(t *testing.T) {
|
||||
func TestAnalyzeRenderDebugArtifactOverrideFalseWinsOverGlobalTrue(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
|
||||
env.Config.Pipeline.Scriptorium.RenderDebug = true
|
||||
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
@@ -111,7 +111,7 @@ func TestAnalyzeRenderDebugArtifactOverrideFalseWinsOverGlobalTrue(t *testing.T)
|
||||
func TestAnalyzeRenderDebugTrueCallsRenderBeforeRun(t *testing.T) {
|
||||
env, m, _ := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
|
||||
env.Config.Pipeline.Scriptorium.RenderDebug = true
|
||||
runner := &orderedScriptoriumRunner{
|
||||
@@ -132,7 +132,7 @@ func TestAnalyzeRenderDebugTrueCallsRenderBeforeRun(t *testing.T) {
|
||||
func TestAnalyzeRenderOutputPathIsRecorded(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
env.Config.Pipeline.Scriptorium.RenderDebug = true
|
||||
|
||||
result, err := (analyzeStage{}).Run(context.Background(), env, m)
|
||||
@@ -156,7 +156,7 @@ func TestAnalyzeRenderOutputPathIsRecorded(t *testing.T) {
|
||||
func TestAnalyzeRenderFailurePreventsRun(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
env.Config.Pipeline.Scriptorium.RenderDebug = true
|
||||
fake.RenderErr = errors.New("render boom")
|
||||
|
||||
@@ -178,7 +178,7 @@ func TestAnalyzeRenderFailurePreventsRun(t *testing.T) {
|
||||
func TestAnalyzeRenderInvalidJSONFailsClearly(t *testing.T) {
|
||||
env, m, _ := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
env.Config.Pipeline.Scriptorium.RenderDebug = true
|
||||
runner := &orderedScriptoriumRunner{
|
||||
RenderBody: `not-json`,
|
||||
@@ -201,7 +201,7 @@ func TestAnalyzeRenderInvalidJSONFailsClearly(t *testing.T) {
|
||||
func TestAnalyzeRunStillSucceedsWhenRenderSucceeds(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
env.Config.Pipeline.Scriptorium.RenderDebug = true
|
||||
|
||||
result, err := (analyzeStage{}).Run(context.Background(), env, m)
|
||||
@@ -222,7 +222,7 @@ func TestAnalyzeRunStillSucceedsWhenRenderSucceeds(t *testing.T) {
|
||||
func TestAnalyzeOmitsOptionalCanonicalPreviousRecapWhenUnavailable(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[]}`)
|
||||
|
||||
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = config.ScriptoriumArtifactConfig{
|
||||
Enabled: true,
|
||||
@@ -261,7 +261,7 @@ func TestAnalyzeUsesRunLocalPathsAndPromotesCanonical(t *testing.T) {
|
||||
m.Campaign = "sample-campaign"
|
||||
m.RunID = "20260518T010203Z-abcdef12"
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
|
||||
result, err := (analyzeStage{}).Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
@@ -344,7 +344,7 @@ func (r *orderedScriptoriumRunner) RunArtifact(_ context.Context, req scriptoriu
|
||||
func TestAnalyzeIncludesCanonicalPreviousRecapWhenPreparedCacheExists(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[]}`)
|
||||
|
||||
previousRecapPath := artifacts.SessionPreviousArtifactPath(paths, "artifacts/session_recap.md")
|
||||
writeAnalyzeFile(t, previousRecapPath, "previous recap\n")
|
||||
@@ -383,7 +383,7 @@ func TestAnalyzeIncludesCanonicalPreviousRecapWhenPreparedCacheExists(t *testing
|
||||
func TestAnalyzeFailsWhenRequiredCanonicalPreviousRecapMissing(t *testing.T) {
|
||||
env, m, _ := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[]}`)
|
||||
|
||||
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = config.ScriptoriumArtifactConfig{
|
||||
Enabled: true,
|
||||
@@ -413,7 +413,7 @@ func TestAnalyzeFailsWhenRequiredCanonicalPreviousRecapMissing(t *testing.T) {
|
||||
func TestAnalyzeResolvesConfiguredArtifactInputFromDisabledArtifactOutput(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
playerHandoutPath := filepath.Join(paths.ArtifactsDir, "player_handout.md")
|
||||
writeAnalyzeFile(t, playerHandoutPath, "handout\n")
|
||||
|
||||
@@ -443,7 +443,7 @@ func TestAnalyzeResolvesConfiguredArtifactInputFromDisabledArtifactOutput(t *tes
|
||||
func TestAnalyzeMetadataIncludesGeneratedAndReusedArtifacts(t *testing.T) {
|
||||
env, m, _ := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
playerHandoutPath := filepath.Join(paths.ArtifactsDir, "player_handout.md")
|
||||
writeAnalyzeFile(t, playerHandoutPath, "handout\n")
|
||||
|
||||
@@ -512,7 +512,7 @@ func TestAnalyzeMetadataIncludesGeneratedAndReusedArtifacts(t *testing.T) {
|
||||
func TestAnalyzeRunsMultipleIndependentArtifactsInDeterministicOrder(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
|
||||
env.Config.Pipeline.Scriptorium.Artifacts["player_handout"] = config.ScriptoriumArtifactConfig{
|
||||
Enabled: true,
|
||||
@@ -521,7 +521,7 @@ func TestAnalyzeRunsMultipleIndependentArtifactsInDeterministicOrder(t *testing.
|
||||
OutputPath: "artifacts/player_handout.md",
|
||||
Inputs: map[string]config.ScriptoriumInputConfig{
|
||||
"transcript": {
|
||||
Source: "narratio.transcript.trimmed",
|
||||
Source: "narratio.transcript.final_trimmed",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
@@ -553,7 +553,7 @@ func TestAnalyzeRunsMultipleIndependentArtifactsInDeterministicOrder(t *testing.
|
||||
func TestAnalyzeRunsDependenciesBeforeDependents(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
|
||||
env.Config.Pipeline.Scriptorium.Artifacts["player_handout"] = config.ScriptoriumArtifactConfig{
|
||||
Enabled: true,
|
||||
@@ -567,7 +567,7 @@ func TestAnalyzeRunsDependenciesBeforeDependents(t *testing.T) {
|
||||
Required: true,
|
||||
},
|
||||
"transcript": {
|
||||
Source: "narratio.transcript.trimmed",
|
||||
Source: "narratio.transcript.final_trimmed",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
@@ -594,7 +594,7 @@ func TestAnalyzeRunsDependenciesBeforeDependents(t *testing.T) {
|
||||
func TestAnalyzeAppliesSelectedArtifactsFilter(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
|
||||
env.Config.Pipeline.Scriptorium.Artifacts["player_handout"] = config.ScriptoriumArtifactConfig{
|
||||
Enabled: true,
|
||||
@@ -603,7 +603,7 @@ func TestAnalyzeAppliesSelectedArtifactsFilter(t *testing.T) {
|
||||
OutputPath: "artifacts/player_handout.md",
|
||||
Inputs: map[string]config.ScriptoriumInputConfig{
|
||||
"transcript": {
|
||||
Source: "narratio.transcript.trimmed",
|
||||
Source: "narratio.transcript.final_trimmed",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
@@ -628,7 +628,7 @@ func TestAnalyzeAppliesSelectedArtifactsFilter(t *testing.T) {
|
||||
func TestAnalyzeMetadataIncludesMultipleGeneratedArtifacts(t *testing.T) {
|
||||
env, m, _ := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
|
||||
env.Config.Pipeline.Scriptorium.Artifacts["player_handout"] = config.ScriptoriumArtifactConfig{
|
||||
Enabled: true,
|
||||
@@ -637,7 +637,7 @@ func TestAnalyzeMetadataIncludesMultipleGeneratedArtifacts(t *testing.T) {
|
||||
OutputPath: "artifacts/player_handout.md",
|
||||
Inputs: map[string]config.ScriptoriumInputConfig{
|
||||
"transcript": {
|
||||
Source: "narratio.transcript.trimmed",
|
||||
Source: "narratio.transcript.final_trimmed",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
@@ -663,7 +663,7 @@ func TestAnalyzeMetadataIncludesMultipleGeneratedArtifacts(t *testing.T) {
|
||||
func TestAnalyzeFailsWhenRequiredConfiguredArtifactMissing(t *testing.T) {
|
||||
env, m, _ := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
|
||||
sessionRecap := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
sessionRecap.Inputs["recap"] = config.ScriptoriumInputConfig{
|
||||
@@ -688,7 +688,7 @@ func TestAnalyzeFailsWhenRequiredConfiguredArtifactMissing(t *testing.T) {
|
||||
func TestAnalyzeOmitsOptionalMissingConfiguredArtifactInput(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
|
||||
sessionRecap := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
sessionRecap.Inputs["recap"] = config.ScriptoriumInputConfig{
|
||||
@@ -735,7 +735,7 @@ func TestAnalyzeRequiredPreviousSessionArtifactInputGuidesPrepareForce(t *testin
|
||||
func TestAnalyzeResolvesCanonicalPreviousSessionArtifactFromManifestInput(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
|
||||
previousPath := artifacts.SessionPreviousArtifactPath(paths, "artifacts/session_recap.md")
|
||||
writeAnalyzeFile(t, previousPath, "previous recap\n")
|
||||
@@ -766,7 +766,7 @@ func TestAnalyzeResolvesCanonicalPreviousSessionArtifactFromManifestInput(t *tes
|
||||
func TestAnalyzeOmitsOptionalMissingCanonicalPreviousSessionArtifact(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
|
||||
sessionRecap := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
sessionRecap.Inputs["previous_recap"] = config.ScriptoriumInputConfig{
|
||||
@@ -790,7 +790,7 @@ func TestAnalyzeOmitsOptionalMissingCanonicalPreviousSessionArtifact(t *testing.
|
||||
func TestAnalyzeRenderDebugWithCanonicalPreviousSessionInput(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
env.Config.Pipeline.Scriptorium.RenderDebug = true
|
||||
|
||||
previousPath := artifacts.SessionPreviousArtifactPath(paths, "artifacts/session_recap.md")
|
||||
@@ -825,7 +825,7 @@ func TestAnalyzeRenderDebugWithCanonicalPreviousSessionInput(t *testing.T) {
|
||||
func TestAnalyzeDoesNotCallObjectStoreForCanonicalPreviousSessionInput(t *testing.T) {
|
||||
env, m, _ := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
|
||||
previousPath := artifacts.SessionPreviousArtifactPath(paths, "artifacts/session_recap.md")
|
||||
writeAnalyzeFile(t, previousPath, "previous recap\n")
|
||||
@@ -856,7 +856,7 @@ func TestAnalyzeDoesNotCallObjectStoreForCanonicalPreviousSessionInput(t *testin
|
||||
func TestAnalyzeFailsWhenOutputPathMissing(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
|
||||
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
artifact.OutputPath = ""
|
||||
@@ -891,7 +891,7 @@ func TestAnalyzeFailsWhenTrimmedTranscriptMissing(t *testing.T) {
|
||||
func TestAnalyzeSupportsProcessedTranscriptSourceWhenConfigured(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[]}`)
|
||||
|
||||
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
|
||||
@@ -907,7 +907,7 @@ func TestAnalyzeSupportsProcessedTranscriptSourceWhenConfigured(t *testing.T) {
|
||||
if len(fake.RunRequests) != 1 {
|
||||
t.Fatalf("run requests = %d, want 1", len(fake.RunRequests))
|
||||
}
|
||||
if fake.RunRequests[0].InputPaths["transcript"] != filepath.Join(paths.TranscriptsDir, "processed.json") {
|
||||
if fake.RunRequests[0].InputPaths["transcript"] != filepath.Join(paths.TranscriptsDir, "polished.json") {
|
||||
t.Fatalf("transcript input = %q, want processed transcript path", fake.RunRequests[0].InputPaths["transcript"])
|
||||
}
|
||||
}
|
||||
@@ -915,11 +915,11 @@ func TestAnalyzeSupportsProcessedTranscriptSourceWhenConfigured(t *testing.T) {
|
||||
func TestAnalyzeSupportsCanonicalTrimmedTranscriptSourceWhenConfigured(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
|
||||
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
|
||||
Source: "narratio.transcript.trimmed",
|
||||
Source: "narratio.transcript.final_trimmed",
|
||||
Required: true,
|
||||
}
|
||||
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact
|
||||
@@ -931,7 +931,7 @@ func TestAnalyzeSupportsCanonicalTrimmedTranscriptSourceWhenConfigured(t *testin
|
||||
if len(fake.RunRequests) != 1 {
|
||||
t.Fatalf("run requests = %d, want 1", len(fake.RunRequests))
|
||||
}
|
||||
if fake.RunRequests[0].InputPaths["transcript"] != filepath.Join(paths.TranscriptsDir, "trimmed.json") {
|
||||
if fake.RunRequests[0].InputPaths["transcript"] != filepath.Join(paths.TranscriptsDir, "final.trimmed.json") {
|
||||
t.Fatalf("transcript input = %q, want trimmed transcript path", fake.RunRequests[0].InputPaths["transcript"])
|
||||
}
|
||||
}
|
||||
@@ -939,12 +939,12 @@ func TestAnalyzeSupportsCanonicalTrimmedTranscriptSourceWhenConfigured(t *testin
|
||||
func TestAnalyzeSupportsNormalizedTranscriptSourceWhenConfigured(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
normalizedPath := filepath.Join(paths.TranscriptsDir, "normalized.json")
|
||||
normalizedPath := filepath.Join(paths.TranscriptsDir, "final.json")
|
||||
writeAnalyzeFile(t, normalizedPath, `{"segments":[{"id":1}]}`)
|
||||
|
||||
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
|
||||
Source: "narratio.transcript.full",
|
||||
Source: "narratio.transcript.final",
|
||||
Required: true,
|
||||
}
|
||||
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact
|
||||
@@ -964,17 +964,17 @@ func TestAnalyzeSupportsNormalizedTranscriptSourceWhenConfigured(t *testing.T) {
|
||||
func TestAnalyzeSupportsCanonicalNormalizedTranscriptSourceFromManifestOutput(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
fallbackPath := filepath.Join(paths.TranscriptsDir, "normalized.json")
|
||||
fallbackPath := filepath.Join(paths.TranscriptsDir, "final.json")
|
||||
manifestPath := filepath.Join(paths.ArtifactsDir, "normalized.from-manifest.json")
|
||||
writeAnalyzeFile(t, fallbackPath, `{"segments":[{"id":999}]}`)
|
||||
writeAnalyzeFile(t, manifestPath, `{"segments":[{"id":10}]}`)
|
||||
m.MarkStageSucceeded("normalize", time.Now().UTC(), []manifest.ArtifactRecord{
|
||||
{Kind: "transcript_normalized", LocalPath: manifestPath},
|
||||
{Kind: "transcript_final", LocalPath: manifestPath},
|
||||
})
|
||||
|
||||
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
|
||||
Source: "narratio.transcript.full",
|
||||
Source: "narratio.transcript.final",
|
||||
Required: true,
|
||||
}
|
||||
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact
|
||||
@@ -994,17 +994,17 @@ func TestAnalyzeSupportsCanonicalNormalizedTranscriptSourceFromManifestOutput(t
|
||||
func TestAnalyzeSupportsNormalizedTranscriptSourceFromManifestOutput(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
fallbackPath := filepath.Join(paths.TranscriptsDir, "normalized.json")
|
||||
fallbackPath := filepath.Join(paths.TranscriptsDir, "final.json")
|
||||
manifestPath := filepath.Join(paths.ArtifactsDir, "normalized.from-manifest.json")
|
||||
writeAnalyzeFile(t, fallbackPath, `{"segments":[{"id":999}]}`)
|
||||
writeAnalyzeFile(t, manifestPath, `{"segments":[{"id":10}]}`)
|
||||
m.MarkStageSucceeded("normalize", time.Now().UTC(), []manifest.ArtifactRecord{
|
||||
{Kind: "transcript_normalized", LocalPath: manifestPath},
|
||||
{Kind: "transcript_final", LocalPath: manifestPath},
|
||||
})
|
||||
|
||||
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
|
||||
Source: "narratio.transcript.full",
|
||||
Source: "narratio.transcript.final",
|
||||
Required: true,
|
||||
}
|
||||
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact
|
||||
@@ -1025,7 +1025,7 @@ func TestAnalyzeFailsWhenNormalizedTranscriptMissing(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
artifact.Inputs["transcript"] = config.ScriptoriumInputConfig{
|
||||
Source: "narratio.transcript.full",
|
||||
Source: "narratio.transcript.final",
|
||||
Required: true,
|
||||
}
|
||||
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact
|
||||
@@ -1048,7 +1048,7 @@ func TestAnalyzeFailsWhenNormalizedTranscriptMissing(t *testing.T) {
|
||||
func TestAnalyzeFailsWhenProcessedTranscriptInvalidJSON(t *testing.T) {
|
||||
env, m, _ := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{not-json`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{not-json`)
|
||||
|
||||
_, err := (analyzeStage{}).Run(context.Background(), env, m)
|
||||
if err == nil {
|
||||
@@ -1062,7 +1062,7 @@ func TestAnalyzeFailsWhenProcessedTranscriptInvalidJSON(t *testing.T) {
|
||||
func TestAnalyzeFailsWhenProcessedTranscriptMissingSegmentsArray(t *testing.T) {
|
||||
env, m, _ := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"not_segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"not_segments":[]}`)
|
||||
|
||||
_, err := (analyzeStage{}).Run(context.Background(), env, m)
|
||||
if err == nil {
|
||||
@@ -1076,7 +1076,7 @@ func TestAnalyzeFailsWhenProcessedTranscriptMissingSegmentsArray(t *testing.T) {
|
||||
func TestAnalyzeRecordsRefsAndMetadata(t *testing.T) {
|
||||
env, m, _ := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
|
||||
result, err := (analyzeStage{}).Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
@@ -1105,7 +1105,7 @@ func TestAnalyzeRecordsRefsAndMetadata(t *testing.T) {
|
||||
func TestAnalyzeHandlesAdapterError(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
fake.RunErr = errors.New("adapter boom")
|
||||
|
||||
_, err := (analyzeStage{}).Run(context.Background(), env, m)
|
||||
@@ -1120,7 +1120,7 @@ func TestAnalyzeHandlesAdapterError(t *testing.T) {
|
||||
func TestAnalyzeHandlesValidationFailedResultAsError(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
fake.RunResult = scriptorium.ArtifactResult{
|
||||
ValidationFailed: true,
|
||||
ExitCode: 2,
|
||||
@@ -1139,7 +1139,7 @@ func TestAnalyzeHandlesValidationFailedResultAsError(t *testing.T) {
|
||||
func TestAnalyzeSkipsWhenNoEnabledScriptoriumArtifactsConfigured(t *testing.T) {
|
||||
env, m, _ := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "trimmed.json"), `{"segments":[]}`)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
|
||||
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = config.ScriptoriumArtifactConfig{
|
||||
Enabled: false,
|
||||
@@ -1199,7 +1199,7 @@ func setupAnalyzeEnv(t *testing.T) (*Env, *manifest.Manifest, *scriptorium.FakeR
|
||||
Timeout: "2m",
|
||||
Inputs: map[string]config.ScriptoriumInputConfig{
|
||||
"transcript": {
|
||||
Source: "narratio.transcript.trimmed",
|
||||
Source: "narratio.transcript.final_trimmed",
|
||||
Required: true,
|
||||
},
|
||||
"previous_recap": {
|
||||
|
||||
@@ -82,7 +82,7 @@ func TestArchiveUploadsRunRecordPromotionsAndCurrentPointer(t *testing.T) {
|
||||
"manifest.json",
|
||||
"polish/reports/audita.report.json",
|
||||
"transcribe/outputs/transcripts/raw/speaker.json",
|
||||
"trim/outputs/transcripts/trimmed.json",
|
||||
"trim/outputs/transcripts/final.trimmed.json",
|
||||
}
|
||||
for _, rel := range wantRunUploads {
|
||||
key := runPrefix + rel
|
||||
@@ -91,7 +91,7 @@ func TestArchiveUploadsRunRecordPromotionsAndCurrentPointer(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
trimmedKey := sessionPrefix + "transcripts/trimmed.json"
|
||||
trimmedKey := sessionPrefix + "transcripts/final.trimmed.json"
|
||||
recapKey := sessionPrefix + "artifacts/session_recap.md"
|
||||
if _, ok := fake.Objects[trimmedKey]; !ok {
|
||||
t.Fatalf("missing promoted key %q", trimmedKey)
|
||||
@@ -180,7 +180,7 @@ func TestArchiveToleratesMissingPreviousCache(t *testing.T) {
|
||||
func TestArchiveUsesCustomPromotionRules(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
env.Config.Pipeline.Archive.PromoteArtifacts = []config.ArchivePromotionRule{
|
||||
{Source: "narratio.transcript.trimmed", Dest: "published/trimmed.json", Required: boolPtr(true)},
|
||||
{Source: "narratio.transcript.final_trimmed", Dest: "published/trimmed.json", Required: boolPtr(true)},
|
||||
{Source: "narratio.artifact.session_recap", Dest: "published/recap.md", Required: boolPtr(true)},
|
||||
}
|
||||
|
||||
@@ -201,8 +201,8 @@ func TestArchiveUsesCustomPromotionRules(t *testing.T) {
|
||||
func TestArchiveSkipsOptionalMissingPromotion(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
env.Config.Pipeline.Archive.PromoteArtifacts = []config.ArchivePromotionRule{
|
||||
{Source: "narratio.transcript.trimmed", Dest: "transcripts/trimmed.json", Required: boolPtr(true)},
|
||||
{Source: "narratio.transcript.merged", Dest: "transcripts/merged.json", Required: boolPtr(false)},
|
||||
{Source: "narratio.transcript.final_trimmed", Dest: "transcripts/final.trimmed.json", Required: boolPtr(true)},
|
||||
{Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(false)},
|
||||
}
|
||||
|
||||
result, err := archiveStage{}.Run(context.Background(), env, m)
|
||||
@@ -210,7 +210,7 @@ func TestArchiveSkipsOptionalMissingPromotion(t *testing.T) {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
got, _ := result.Metadata["skipped_optional_promotions"].([]string)
|
||||
want := []string{"transcripts/merged.json"}
|
||||
want := []string{"transcripts/base.json"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("skipped_optional_promotions = %#v, want %#v", got, want)
|
||||
}
|
||||
@@ -230,7 +230,7 @@ func TestArchiveSkipsRequiredUnselectedConfiguredPromotion(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
if _, ok := fake.Objects[m.S3SessionPrefix+"transcripts/trimmed.json"]; !ok {
|
||||
if _, ok := fake.Objects[m.S3SessionPrefix+"transcripts/final.trimmed.json"]; !ok {
|
||||
t.Fatalf("missing built-in promoted trimmed key")
|
||||
}
|
||||
if _, ok := fake.Objects[m.S3SessionPrefix+"artifacts/session_recap.md"]; ok {
|
||||
@@ -321,7 +321,7 @@ func TestArchiveLockedUnselectedConfiguredPromotionSkipsAsUnselected(t *testing.
|
||||
func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
env.Config.Pipeline.Archive.Locks = []config.ArchiveLockRule{
|
||||
{Source: "narratio.transcript.trimmed", Reason: "human reviewed"},
|
||||
{Source: "narratio.transcript.final_trimmed", Reason: "human reviewed"},
|
||||
}
|
||||
fake := env.ObjectStore.(*storage.FakeBackend)
|
||||
|
||||
@@ -330,7 +330,7 @@ func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
|
||||
trimmedKey := m.S3SessionPrefix + "transcripts/trimmed.json"
|
||||
trimmedKey := m.S3SessionPrefix + "transcripts/final.trimmed.json"
|
||||
if _, ok := fake.Objects[trimmedKey]; ok {
|
||||
t.Fatalf("locked promotion key %q should not be uploaded", trimmedKey)
|
||||
}
|
||||
@@ -338,7 +338,7 @@ func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) {
|
||||
if _, ok := fake.Objects[recapKey]; !ok {
|
||||
t.Fatalf("unlocked promotion key %q should be uploaded", recapKey)
|
||||
}
|
||||
runTrimmedKey := m.S3RunPrefix + "trim/outputs/transcripts/trimmed.json"
|
||||
runTrimmedKey := m.S3RunPrefix + "trim/outputs/transcripts/final.trimmed.json"
|
||||
if _, ok := fake.Objects[runTrimmedKey]; !ok {
|
||||
t.Fatalf("run-local locked source output %q should still be uploaded", runTrimmedKey)
|
||||
}
|
||||
@@ -358,8 +358,8 @@ func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) {
|
||||
if len(locked) != 1 {
|
||||
t.Fatalf("locked_promotions = %#v, want one item", locked)
|
||||
}
|
||||
if locked[0]["source"] != "narratio.transcript.trimmed" ||
|
||||
locked[0]["dest"] != "transcripts/trimmed.json" ||
|
||||
if locked[0]["source"] != "narratio.transcript.final_trimmed" ||
|
||||
locked[0]["dest"] != "transcripts/final.trimmed.json" ||
|
||||
locked[0]["remote_key"] != trimmedKey ||
|
||||
locked[0]["reason"] != "human reviewed" ||
|
||||
locked[0]["required"] != true ||
|
||||
@@ -388,10 +388,10 @@ func TestArchiveSkipsLockedRequiredPromotionAndCommits(t *testing.T) {
|
||||
func TestArchiveLockedRequiredMissingPromotionSucceeds(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
env.Config.Pipeline.Archive.PromoteArtifacts = []config.ArchivePromotionRule{
|
||||
{Source: "narratio.transcript.merged", Dest: "transcripts/merged.json", Required: boolPtr(true)},
|
||||
{Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(true)},
|
||||
}
|
||||
env.Config.Pipeline.Archive.Locks = []config.ArchiveLockRule{
|
||||
{Source: "narratio.transcript.merged", Reason: "manual merge is locked"},
|
||||
{Source: "narratio.transcript.base", Reason: "manual merge is locked"},
|
||||
}
|
||||
|
||||
result, err := archiveStage{}.Run(context.Background(), env, m)
|
||||
@@ -400,7 +400,7 @@ func TestArchiveLockedRequiredMissingPromotionSucceeds(t *testing.T) {
|
||||
}
|
||||
|
||||
fake := env.ObjectStore.(*storage.FakeBackend)
|
||||
mergedKey := m.S3SessionPrefix + "transcripts/merged.json"
|
||||
mergedKey := m.S3SessionPrefix + "transcripts/base.json"
|
||||
if _, ok := fake.Objects[mergedKey]; ok {
|
||||
t.Fatalf("locked missing promotion key %q should not be uploaded", mergedKey)
|
||||
}
|
||||
@@ -419,10 +419,10 @@ func TestArchiveLockedRequiredMissingPromotionSucceeds(t *testing.T) {
|
||||
func TestArchiveLockDoesNotOverwriteExistingPromotion(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
env.Config.Pipeline.Archive.Locks = []config.ArchiveLockRule{
|
||||
{Source: "narratio.transcript.trimmed", Reason: "already published"},
|
||||
{Source: "narratio.transcript.final_trimmed", Reason: "already published"},
|
||||
}
|
||||
fake := env.ObjectStore.(*storage.FakeBackend)
|
||||
trimmedKey := m.S3SessionPrefix + "transcripts/trimmed.json"
|
||||
trimmedKey := m.S3SessionPrefix + "transcripts/final.trimmed.json"
|
||||
fake.SeedObject(storage.FakeObject{Key: trimmedKey, Data: []byte("previously published\n")})
|
||||
|
||||
if _, err := (archiveStage{}).Run(context.Background(), env, m); err != nil {
|
||||
@@ -443,7 +443,7 @@ func TestArchiveLockDoesNotOverwriteExistingPromotion(t *testing.T) {
|
||||
func TestArchiveFailsWhenRequiredPromotionMissing(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
env.Config.Pipeline.Archive.PromoteArtifacts = []config.ArchivePromotionRule{
|
||||
{Source: "narratio.transcript.merged", Dest: "transcripts/merged.json", Required: boolPtr(true)},
|
||||
{Source: "narratio.transcript.base", Dest: "transcripts/base.json", Required: boolPtr(true)},
|
||||
}
|
||||
|
||||
_, err := archiveStage{}.Run(context.Background(), env, m)
|
||||
@@ -470,7 +470,7 @@ func TestArchiveFailsWhenCanonicalRunRootMissing(t *testing.T) {
|
||||
func TestArchiveDoesNotWriteCurrentPointerWhenPromotionUploadFails(t *testing.T) {
|
||||
env, m, _ := archiveFixture(t)
|
||||
fake := env.ObjectStore.(*storage.FakeBackend)
|
||||
trimmedKey := m.S3SessionPrefix + "transcripts/trimmed.json"
|
||||
trimmedKey := m.S3SessionPrefix + "transcripts/final.trimmed.json"
|
||||
|
||||
origUploadErr := fake.UploadErr
|
||||
fake.UploadErr = nil
|
||||
@@ -528,13 +528,13 @@ func archiveFixture(t *testing.T) (*Env, *manifest.Manifest, string) {
|
||||
sessionRoot := artifacts.SessionWorkDirForCampaign(root, campaign, sessionID)
|
||||
runRoot := artifacts.SessionRunRootForCampaign(root, campaign, sessionID, runID)
|
||||
|
||||
writeStageTestFile(t, filepath.Join(sessionRoot, "transcripts", "trimmed.json"), "{\"segments\":[]}\n")
|
||||
writeStageTestFile(t, filepath.Join(sessionRoot, "transcripts", "final.trimmed.json"), "{\"segments\":[]}\n")
|
||||
writeStageTestFile(t, filepath.Join(sessionRoot, "artifacts", "session_recap.md"), "# recap\n")
|
||||
|
||||
writeStageTestFile(t, filepath.Join(runRoot, "prepare", "inputs", "session.yml"), "session_id: 2026-04-19\n")
|
||||
writeStageTestFile(t, filepath.Join(runRoot, "prepare", "outputs", "audio", "speaker.flac"), "flac\n")
|
||||
writeStageTestFile(t, filepath.Join(runRoot, "transcribe", "outputs", "transcripts", "raw", "speaker.json"), "{}\n")
|
||||
writeStageTestFile(t, filepath.Join(runRoot, "trim", "outputs", "transcripts", "trimmed.json"), "{\"segments\":[]}\n")
|
||||
writeStageTestFile(t, filepath.Join(runRoot, "trim", "outputs", "transcripts", "final.trimmed.json"), "{\"segments\":[]}\n")
|
||||
writeStageTestFile(t, filepath.Join(runRoot, "analyze", "outputs", "artifacts", "session_recap.md"), "# recap\n")
|
||||
writeStageTestFile(t, filepath.Join(runRoot, "polish", "reports", "audita.report.json"), "{}\n")
|
||||
writeStageTestFile(t, filepath.Join(runRoot, "merge", "config", "seriatim.generated.yml"), "key: value\n")
|
||||
@@ -567,7 +567,7 @@ func archiveFixture(t *testing.T) (*Env, *manifest.Manifest, string) {
|
||||
Enabled: boolPtr(true),
|
||||
UploadRun: boolPtr(true),
|
||||
PromoteArtifacts: []config.ArchivePromotionRule{
|
||||
{Source: "narratio.transcript.trimmed", Dest: "transcripts/trimmed.json", Required: boolPtr(true)},
|
||||
{Source: "narratio.transcript.final_trimmed", Dest: "transcripts/final.trimmed.json", Required: boolPtr(true)},
|
||||
{Source: "narratio.artifact.session_recap", Dest: "artifacts/session_recap.md", Required: boolPtr(true)},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -26,7 +26,7 @@ func (mergeStage) Declares() IODecl {
|
||||
{Kind: "autocorrect", Category: "inputs", RelativePath: "inputs/autocorrect.yml"},
|
||||
},
|
||||
Outputs: []artifacts.Ref{
|
||||
{Kind: "transcript_merged", Category: "transcripts", RelativePath: "transcripts/merged.json"},
|
||||
{Kind: "transcript_base", Category: "transcripts", RelativePath: "transcripts/base.json"},
|
||||
{Kind: "seriatim_report", Category: "artifacts", RelativePath: "artifacts/seriatim.report.json"},
|
||||
},
|
||||
}
|
||||
@@ -85,7 +85,7 @@ func (mergeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Sta
|
||||
return nil, fmt.Errorf("merge: %w", err)
|
||||
}
|
||||
|
||||
canonicalMergedPath := filepath.Join(paths.TranscriptsDir, "merged.json")
|
||||
canonicalMergedPath := filepath.Join(paths.TranscriptsDir, "base.json")
|
||||
mergedPath, err := runLocalPathForCanonical(runLayout, paths, canonicalMergedPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("merge: resolve run-local merged transcript path: %w", err)
|
||||
@@ -151,7 +151,7 @@ func (mergeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Sta
|
||||
}
|
||||
|
||||
promotedMerged, err := promoteRunLocalOutput(env.ArtifactStore, finalMergedPath, canonicalMergedPath, artifacts.Ref{
|
||||
Kind: "transcript_merged",
|
||||
Kind: "transcript_base",
|
||||
Category: "transcripts",
|
||||
SessionID: sessionID,
|
||||
})
|
||||
|
||||
@@ -86,8 +86,8 @@ func TestMergeStageMergesRawTranscriptsAndRecordsMetadata(t *testing.T) {
|
||||
if len(result.Outputs) != 2 {
|
||||
t.Fatalf("outputs len = %d, want 2", len(result.Outputs))
|
||||
}
|
||||
if result.Outputs[0].Kind != "transcript_merged" {
|
||||
t.Fatalf("output[0] kind = %q, want transcript_merged", result.Outputs[0].Kind)
|
||||
if result.Outputs[0].Kind != "transcript_base" {
|
||||
t.Fatalf("output[0] kind = %q, want transcript_base", result.Outputs[0].Kind)
|
||||
}
|
||||
if result.Outputs[1].Kind != "seriatim_report" {
|
||||
t.Fatalf("output[1] kind = %q, want seriatim_report", result.Outputs[1].Kind)
|
||||
|
||||
@@ -20,10 +20,10 @@ func (normalizeStage) Name() string { return "normalize" }
|
||||
func (normalizeStage) Declares() IODecl {
|
||||
return IODecl{
|
||||
Inputs: []artifacts.Ref{
|
||||
{Kind: "transcript_processed", Category: "transcripts", RelativePath: "transcripts/processed.json"},
|
||||
{Kind: "transcript_polished", Category: "transcripts", RelativePath: "transcripts/polished.json"},
|
||||
},
|
||||
Outputs: []artifacts.Ref{
|
||||
{Kind: "transcript_normalized", Category: "transcripts", RelativePath: "transcripts/normalized.json"},
|
||||
{Kind: "transcript_final", Category: "transcripts", RelativePath: "transcripts/final.json"},
|
||||
{Kind: "seriatim_normalize_report", Category: "artifacts", RelativePath: "artifacts/seriatim.normalize.report.json"},
|
||||
},
|
||||
}
|
||||
@@ -134,7 +134,7 @@ func (normalizeStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (
|
||||
}
|
||||
|
||||
promotedNormalized, err := promoteRunLocalOutput(env.ArtifactStore, finalNormalizedPath, canonicalNormalizedPath, artifacts.Ref{
|
||||
Kind: "transcript_normalized",
|
||||
Kind: "transcript_final",
|
||||
Category: "transcripts",
|
||||
SessionID: sessionID,
|
||||
})
|
||||
@@ -202,7 +202,7 @@ func normalizeConfigOrDefault(cfg *config.NormalizeConfig) *config.NormalizeConf
|
||||
}
|
||||
report := true
|
||||
return &config.NormalizeConfig{
|
||||
OutputPath: "transcripts/normalized.json",
|
||||
OutputPath: "transcripts/final.json",
|
||||
OutputSchema: "seriatim-intermediate",
|
||||
Report: &report,
|
||||
}
|
||||
@@ -213,7 +213,7 @@ func discoverNormalizedTranscript(m *manifest.Manifest, paths artifacts.SessionP
|
||||
if m != nil && m.Stages != nil {
|
||||
if sr := m.Stages["normalize"]; sr != nil {
|
||||
for _, out := range sr.Outputs {
|
||||
if out.Kind != "transcript_normalized" {
|
||||
if out.Kind != "transcript_final" {
|
||||
continue
|
||||
}
|
||||
p := strings.TrimSpace(out.LocalPath)
|
||||
@@ -232,7 +232,7 @@ func discoverNormalizedTranscript(m *manifest.Manifest, paths artifacts.SessionP
|
||||
}
|
||||
}
|
||||
|
||||
fallback := filepath.Join(paths.TranscriptsDir, "normalized.json")
|
||||
fallback := filepath.Join(paths.TranscriptsDir, "final.json")
|
||||
if info, err := os.Stat(fallback); err == nil && !info.IsDir() {
|
||||
return filepath.Clean(fallback), "fallback.transcripts_dir", nil
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@ func TestNormalizeStageConsumesProcessedTranscriptFromManifest(t *testing.T) {
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
manifestProcessed := filepath.Join(paths.ArtifactsDir, "processed.from-manifest.json")
|
||||
writeFile(t, manifestProcessed, `{"segments":[{"id":10}]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":99}]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":99}]}`)
|
||||
m.MarkStageSucceeded("polish", time.Now().UTC(), []manifest.ArtifactRecord{{
|
||||
Kind: "transcript_processed",
|
||||
Kind: "transcript_polished",
|
||||
LocalPath: manifestProcessed,
|
||||
}})
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestNormalizeStageConsumesProcessedTranscriptFromManifest(t *testing.T) {
|
||||
func TestNormalizeStageFallsBackToProcessedTranscriptPath(t *testing.T) {
|
||||
env, m, ser := setupNormalizeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
fallback := filepath.Join(paths.TranscriptsDir, "processed.json")
|
||||
fallback := filepath.Join(paths.TranscriptsDir, "polished.json")
|
||||
writeFile(t, fallback, `{"segments":[{"id":1}]}`)
|
||||
|
||||
_, err := (normalizeStage{}).Run(context.Background(), env, m)
|
||||
@@ -73,7 +73,7 @@ func TestNormalizeStageFailsWhenProcessedTranscriptMissing(t *testing.T) {
|
||||
func TestNormalizeStageFailsWhenProcessedTranscriptInvalidJSON(t *testing.T) {
|
||||
env, m, _ := setupNormalizeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), "not-json")
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), "not-json")
|
||||
|
||||
_, err := (normalizeStage{}).Run(context.Background(), env, m)
|
||||
if err == nil {
|
||||
@@ -87,7 +87,7 @@ func TestNormalizeStageFailsWhenProcessedTranscriptInvalidJSON(t *testing.T) {
|
||||
func TestNormalizeStageFailsWhenProcessedTranscriptMissingSegments(t *testing.T) {
|
||||
env, m, _ := setupNormalizeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"schema":"audita.processed.v1"}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"schema":"audita.processed.v1"}`)
|
||||
|
||||
_, err := (normalizeStage{}).Run(context.Background(), env, m)
|
||||
if err == nil {
|
||||
@@ -101,7 +101,7 @@ func TestNormalizeStageFailsWhenProcessedTranscriptMissingSegments(t *testing.T)
|
||||
func TestNormalizeStagePassesConfiguredOutputSchemaToAdapter(t *testing.T) {
|
||||
env, m, ser := setupNormalizeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":1}]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":1}]}`)
|
||||
env.Config.Pipeline.Normalize.OutputSchema = "seriatim-full"
|
||||
|
||||
result, err := (normalizeStage{}).Run(context.Background(), env, m)
|
||||
@@ -122,24 +122,24 @@ func TestNormalizeStagePassesConfiguredOutputSchemaToAdapter(t *testing.T) {
|
||||
func TestNormalizeStageRecordsNormalizedTranscriptOutputKind(t *testing.T) {
|
||||
env, m, _ := setupNormalizeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":1}]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":1}]}`)
|
||||
|
||||
result, err := (normalizeStage{}).Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
t.Fatalf("normalize.Run() error = %v", err)
|
||||
}
|
||||
if len(result.Outputs) == 0 {
|
||||
t.Fatalf("outputs = %#v, want at least transcript_normalized", result.Outputs)
|
||||
t.Fatalf("outputs = %#v, want at least transcript_final", result.Outputs)
|
||||
}
|
||||
if result.Outputs[0].Kind != "transcript_normalized" {
|
||||
t.Fatalf("outputs[0].kind = %q, want transcript_normalized", result.Outputs[0].Kind)
|
||||
if result.Outputs[0].Kind != "transcript_final" {
|
||||
t.Fatalf("outputs[0].kind = %q, want transcript_final", result.Outputs[0].Kind)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeStageRecordsReportLogAndGeneratedConfigRefs(t *testing.T) {
|
||||
env, m, _ := setupNormalizeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":1}]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":1}]}`)
|
||||
report := true
|
||||
env.Config.Pipeline.Normalize.Report = &report
|
||||
|
||||
@@ -148,7 +148,7 @@ func TestNormalizeStageRecordsReportLogAndGeneratedConfigRefs(t *testing.T) {
|
||||
t.Fatalf("normalize.Run() error = %v", err)
|
||||
}
|
||||
if len(result.Outputs) < 2 {
|
||||
t.Fatalf("outputs = %#v, want transcript_normalized + report", result.Outputs)
|
||||
t.Fatalf("outputs = %#v, want transcript_final + report", result.Outputs)
|
||||
}
|
||||
if result.Outputs[1].Kind != "seriatim_normalize_report" {
|
||||
t.Fatalf("outputs[1].kind = %q, want seriatim_normalize_report", result.Outputs[1].Kind)
|
||||
@@ -165,7 +165,7 @@ func TestNormalizeStageRecordsReportLogAndGeneratedConfigRefs(t *testing.T) {
|
||||
func TestNormalizeStageFailsWhenAdapterReturnsError(t *testing.T) {
|
||||
env, m, ser := setupNormalizeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":1}]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":1}]}`)
|
||||
ser.NormalizeErr = errors.New("normalize failed")
|
||||
|
||||
_, err := (normalizeStage{}).Run(context.Background(), env, m)
|
||||
@@ -180,7 +180,7 @@ func TestNormalizeStageFailsWhenAdapterReturnsError(t *testing.T) {
|
||||
func TestNormalizeStageFailsWhenNormalizedOutputInvalid(t *testing.T) {
|
||||
env, m, ser := setupNormalizeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":1}]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":1}]}`)
|
||||
badOutput := filepath.Join(paths.TranscriptsDir, "normalized.bad.json")
|
||||
writeFile(t, badOutput, "not-json")
|
||||
ser.NormalizeResult = seriatim.NormalizeResult{OutputNormalizedPath: badOutput}
|
||||
@@ -197,7 +197,7 @@ func TestNormalizeStageFailsWhenNormalizedOutputInvalid(t *testing.T) {
|
||||
func TestNormalizeStageReportEnabledFailsWhenReportMissing(t *testing.T) {
|
||||
env, m, ser := setupNormalizeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":1}]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":1}]}`)
|
||||
report := true
|
||||
env.Config.Pipeline.Normalize.Report = &report
|
||||
ser.NormalizeResult = seriatim.NormalizeResult{ReportPath: filepath.Join(paths.ArtifactsDir, "missing-report.json")}
|
||||
@@ -217,7 +217,7 @@ func TestNormalizeStageUsesRunLocalPathsAndPromotesCanonical(t *testing.T) {
|
||||
m.Campaign = "sample-campaign"
|
||||
m.RunID = "20260518T010203Z-abcdef12"
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":1}]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":1}]}`)
|
||||
|
||||
result, err := (normalizeStage{}).Run(context.Background(), env, m)
|
||||
if err != nil {
|
||||
@@ -261,7 +261,7 @@ func setupNormalizeEnv(t *testing.T) (*Env, *manifest.Manifest, *seriatim.FakeRu
|
||||
Report: &report,
|
||||
},
|
||||
Normalize: &config.NormalizeConfig{
|
||||
OutputPath: "transcripts/normalized.json",
|
||||
OutputPath: "transcripts/final.json",
|
||||
OutputSchema: "seriatim-intermediate",
|
||||
Report: &report,
|
||||
},
|
||||
|
||||
@@ -163,8 +163,8 @@ func TestStagesReturnExpectedMetadata(t *testing.T) {
|
||||
if result.Metadata["output_schema"] == nil {
|
||||
t.Fatalf("normalize metadata = %#v, want output_schema metadata", result.Metadata)
|
||||
}
|
||||
if len(result.Outputs) == 0 || result.Outputs[0].Kind != "transcript_normalized" {
|
||||
t.Fatalf("normalize outputs = %#v, want transcript_normalized output", result.Outputs)
|
||||
if len(result.Outputs) == 0 || result.Outputs[0].Kind != "transcript_final" {
|
||||
t.Fatalf("normalize outputs = %#v, want transcript_final output", result.Outputs)
|
||||
}
|
||||
if len(result.Logs) != 2 {
|
||||
t.Fatalf("normalize logs = %#v, want stdout+stderr", result.Logs)
|
||||
@@ -190,8 +190,8 @@ func TestStagesReturnExpectedMetadata(t *testing.T) {
|
||||
if result.Metadata["trim_action"] != "copy_disabled" {
|
||||
t.Fatalf("trim metadata = %#v, want trim_action=copy_disabled", result.Metadata)
|
||||
}
|
||||
if len(result.Outputs) == 0 || result.Outputs[0].Kind != "transcript_trimmed" {
|
||||
t.Fatalf("trim outputs = %#v, want transcript_trimmed output", result.Outputs)
|
||||
if len(result.Outputs) == 0 || result.Outputs[0].Kind != "transcript_final_trimmed" {
|
||||
t.Fatalf("trim outputs = %#v, want transcript_final_trimmed output", result.Outputs)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@ func (polishStage) Name() string { return "polish" }
|
||||
func (polishStage) Declares() IODecl {
|
||||
return IODecl{
|
||||
Inputs: []artifacts.Ref{
|
||||
{Kind: "transcript_merged", Category: "transcripts", RelativePath: "transcripts/merged.json"},
|
||||
{Kind: "transcript_base", Category: "transcripts", RelativePath: "transcripts/base.json"},
|
||||
{Kind: "glossary", Category: "inputs", RelativePath: "inputs/glossary.yml"},
|
||||
},
|
||||
Outputs: []artifacts.Ref{
|
||||
{Kind: "transcript_processed", Category: "transcripts", RelativePath: "transcripts/processed.json"},
|
||||
{Kind: "transcript_polished", Category: "transcripts", RelativePath: "transcripts/polished.json"},
|
||||
{Kind: "audita_report", Category: "artifacts", RelativePath: "artifacts/audita.report.json"},
|
||||
},
|
||||
}
|
||||
@@ -76,7 +76,7 @@ func (polishStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*St
|
||||
return nil, fmt.Errorf("polish: %w", err)
|
||||
}
|
||||
|
||||
canonicalProcessedPath := filepath.Join(paths.TranscriptsDir, "processed.json")
|
||||
canonicalProcessedPath := filepath.Join(paths.TranscriptsDir, "polished.json")
|
||||
processedPath, err := runLocalPathForCanonical(runLayout, paths, canonicalProcessedPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("polish: resolve run-local processed transcript path: %w", err)
|
||||
@@ -150,7 +150,7 @@ func (polishStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*St
|
||||
}
|
||||
|
||||
promotedProcessed, err := promoteRunLocalOutput(env.ArtifactStore, finalProcessedPath, canonicalProcessedPath, artifacts.Ref{
|
||||
Kind: "transcript_processed",
|
||||
Kind: "transcript_polished",
|
||||
Category: "transcripts",
|
||||
SessionID: sessionID,
|
||||
})
|
||||
@@ -250,7 +250,7 @@ func discoverMergedTranscript(m *manifest.Manifest, paths artifacts.SessionPaths
|
||||
if m != nil && m.Stages != nil {
|
||||
if sr := m.Stages["merge"]; sr != nil {
|
||||
for _, out := range sr.Outputs {
|
||||
if out.Kind != "transcript_merged" {
|
||||
if out.Kind != "transcript_base" {
|
||||
continue
|
||||
}
|
||||
p := strings.TrimSpace(out.LocalPath)
|
||||
@@ -270,7 +270,7 @@ func discoverMergedTranscript(m *manifest.Manifest, paths artifacts.SessionPaths
|
||||
}
|
||||
}
|
||||
|
||||
fallback := filepath.Join(paths.TranscriptsDir, "merged.json")
|
||||
fallback := filepath.Join(paths.TranscriptsDir, "base.json")
|
||||
if info, err := os.Stat(fallback); err == nil && !info.IsDir() {
|
||||
return filepath.Clean(fallback), "fallback.transcripts_dir", nil
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ func TestPolishStagePolishesMergedTranscriptAndRecordsMetadata(t *testing.T) {
|
||||
env, m := setupPolishEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
|
||||
mergedPath := filepath.Join(paths.TranscriptsDir, "merged.json")
|
||||
mergedPath := filepath.Join(paths.TranscriptsDir, "base.json")
|
||||
writeFile(t, mergedPath, `{"segments":[]}`)
|
||||
writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n")
|
||||
m.MarkStageSucceeded("merge", time.Now().UTC(), []manifest.ArtifactRecord{
|
||||
{Kind: "transcript_merged", LocalPath: mergedPath},
|
||||
{Kind: "transcript_base", LocalPath: mergedPath},
|
||||
})
|
||||
|
||||
fake := &audita.FakeRunner{}
|
||||
@@ -85,8 +85,8 @@ func TestPolishStagePolishesMergedTranscriptAndRecordsMetadata(t *testing.T) {
|
||||
if len(result.Outputs) != 2 {
|
||||
t.Fatalf("outputs len = %d, want 2", len(result.Outputs))
|
||||
}
|
||||
if result.Outputs[0].Kind != "transcript_processed" {
|
||||
t.Fatalf("output[0] kind = %q, want transcript_processed", result.Outputs[0].Kind)
|
||||
if result.Outputs[0].Kind != "transcript_polished" {
|
||||
t.Fatalf("output[0] kind = %q, want transcript_polished", result.Outputs[0].Kind)
|
||||
}
|
||||
if result.Outputs[1].Kind != "audita_report" {
|
||||
t.Fatalf("output[1] kind = %q, want audita_report", result.Outputs[1].Kind)
|
||||
@@ -121,7 +121,7 @@ func TestPolishStagePolishesMergedTranscriptAndRecordsMetadata(t *testing.T) {
|
||||
func TestPolishStageFallsBackToMergedTranscriptPath(t *testing.T) {
|
||||
env, m := setupPolishEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
mergedPath := filepath.Join(paths.TranscriptsDir, "merged.json")
|
||||
mergedPath := filepath.Join(paths.TranscriptsDir, "base.json")
|
||||
writeFile(t, mergedPath, `{"segments":[]}`)
|
||||
writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n")
|
||||
|
||||
@@ -158,7 +158,7 @@ func TestPolishStageFailsWhenMergedTranscriptMissing(t *testing.T) {
|
||||
func TestPolishStageFailsWhenMergedTranscriptInvalidJSON(t *testing.T) {
|
||||
env, m := setupPolishEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "merged.json"), "not-json")
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "base.json"), "not-json")
|
||||
writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n")
|
||||
env.Audita = &audita.FakeRunner{}
|
||||
|
||||
@@ -174,7 +174,7 @@ func TestPolishStageFailsWhenMergedTranscriptInvalidJSON(t *testing.T) {
|
||||
func TestPolishStageFailsWhenGlossaryMissing(t *testing.T) {
|
||||
env, m := setupPolishEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "merged.json"), `{"segments":[]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "base.json"), `{"segments":[]}`)
|
||||
env.Audita = &audita.FakeRunner{}
|
||||
|
||||
_, err := (polishStage{}).Run(context.Background(), env, m)
|
||||
@@ -189,7 +189,7 @@ func TestPolishStageFailsWhenGlossaryMissing(t *testing.T) {
|
||||
func TestPolishStageFailsWhenAdapterFails(t *testing.T) {
|
||||
env, m := setupPolishEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "merged.json"), `{"segments":[]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "base.json"), `{"segments":[]}`)
|
||||
writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n")
|
||||
env.Audita = &audita.FakeRunner{Err: errors.New("audita failed")}
|
||||
|
||||
@@ -205,7 +205,7 @@ func TestPolishStageFailsWhenAdapterFails(t *testing.T) {
|
||||
func TestPolishStageFailsWhenProcessedOutputInvalid(t *testing.T) {
|
||||
env, m := setupPolishEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "merged.json"), `{"segments":[]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "base.json"), `{"segments":[]}`)
|
||||
writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n")
|
||||
badPath := filepath.Join(paths.TranscriptsDir, "processed.invalid.json")
|
||||
writeFile(t, badPath, `{"schema":"audita.processed.v1","segments":"wrong-type"}`)
|
||||
@@ -223,7 +223,7 @@ func TestPolishStageFailsWhenProcessedOutputInvalid(t *testing.T) {
|
||||
func TestPolishStageFailsWhenReportInvalid(t *testing.T) {
|
||||
env, m := setupPolishEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "merged.json"), `{"segments":[]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "base.json"), `{"segments":[]}`)
|
||||
writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n")
|
||||
badReport := filepath.Join(paths.ArtifactsDir, "bad.report.json")
|
||||
writeFile(t, badReport, "not-json")
|
||||
@@ -244,7 +244,7 @@ func TestPolishStageUsesRunLocalPathsAndPromotesCanonical(t *testing.T) {
|
||||
m.Campaign = "sample-campaign"
|
||||
m.RunID = "20260518T010203Z-abcdef12"
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "merged.json"), `{"segments":[]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "base.json"), `{"segments":[]}`)
|
||||
writeFile(t, filepath.Join(paths.InputsDir, "glossary.yml"), "terms: []\n")
|
||||
|
||||
fake := &audita.FakeRunner{}
|
||||
|
||||
@@ -19,13 +19,13 @@ func TestRunLocalPathForCanonicalCreatesParentDirectories(t *testing.T) {
|
||||
t.Fatalf("mkdir outputs dir: %v", err)
|
||||
}
|
||||
|
||||
canonical := filepath.Join(sessionRoot, "transcripts", "merged.json")
|
||||
canonical := filepath.Join(sessionRoot, "transcripts", "base.json")
|
||||
got, err := runLocalPathForCanonical(layout, artifacts.SessionPaths{Root: sessionRoot}, canonical)
|
||||
if err != nil {
|
||||
t.Fatalf("runLocalPathForCanonical() error = %v", err)
|
||||
}
|
||||
|
||||
want := filepath.Join(layout.OutputsDir, "transcripts", "merged.json")
|
||||
want := filepath.Join(layout.OutputsDir, "transcripts", "base.json")
|
||||
if got != want {
|
||||
t.Fatalf("runLocalPathForCanonical() = %q, want %q", got, want)
|
||||
}
|
||||
|
||||
@@ -23,10 +23,10 @@ func (trimStage) Name() string { return "trim" }
|
||||
func (trimStage) Declares() IODecl {
|
||||
return IODecl{
|
||||
Inputs: []artifacts.Ref{
|
||||
{Kind: "transcript_normalized", Category: "transcripts", RelativePath: "transcripts/normalized.json"},
|
||||
{Kind: "transcript_final", Category: "transcripts", RelativePath: "transcripts/final.json"},
|
||||
},
|
||||
Outputs: []artifacts.Ref{
|
||||
{Kind: "transcript_trimmed", Category: "transcripts", RelativePath: "transcripts/trimmed.json"},
|
||||
{Kind: "transcript_final_trimmed", Category: "transcripts", RelativePath: "transcripts/final.trimmed.json"},
|
||||
{Kind: "session_bounds", Category: "artifacts", RelativePath: "artifacts/session_bounds.json"},
|
||||
},
|
||||
}
|
||||
@@ -101,7 +101,7 @@ func (trimStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Stag
|
||||
return nil, fmt.Errorf("trim: copied trimmed transcript %q invalid: %w", trimmedPath, err)
|
||||
}
|
||||
promotedTrimmed, err := promoteRunLocalOutput(env.ArtifactStore, trimmedPath, canonicalTrimmedPath, artifacts.Ref{
|
||||
Kind: "transcript_trimmed",
|
||||
Kind: "transcript_final_trimmed",
|
||||
Category: "transcripts",
|
||||
SessionID: sessionID,
|
||||
})
|
||||
@@ -352,7 +352,7 @@ func (trimStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Stag
|
||||
}
|
||||
|
||||
promotedTrimmed, err := promoteRunLocalOutput(env.ArtifactStore, trimmedPath, canonicalTrimmedPath, artifacts.Ref{
|
||||
Kind: "transcript_trimmed",
|
||||
Kind: "transcript_final_trimmed",
|
||||
Category: "transcripts",
|
||||
SessionID: sessionID,
|
||||
})
|
||||
@@ -382,7 +382,7 @@ func resolveTrimmedOutputPath(paths artifacts.SessionPaths, cfg *config.TrimConf
|
||||
configured = strings.TrimSpace(cfg.OutputPath)
|
||||
}
|
||||
if configured == "" {
|
||||
return filepath.Join(paths.TranscriptsDir, "trimmed.json"), nil
|
||||
return filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), nil
|
||||
}
|
||||
return resolveScriptoriumOutputPath(paths, configured)
|
||||
}
|
||||
|
||||
@@ -19,10 +19,10 @@ import (
|
||||
func TestTrimStageConsumesNormalizedAndProducesTrimmedTranscript(t *testing.T) {
|
||||
env, m, scr, ser := setupTrimEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
normalized := filepath.Join(paths.TranscriptsDir, "normalized.json")
|
||||
normalized := filepath.Join(paths.TranscriptsDir, "final.json")
|
||||
writeFile(t, normalized, `{"segments":[{"id":10},{"id":868}]}`)
|
||||
m.MarkStageSucceeded("normalize", time.Now().UTC(), []manifest.ArtifactRecord{
|
||||
{Kind: "transcript_normalized", LocalPath: normalized},
|
||||
{Kind: "transcript_final", LocalPath: normalized},
|
||||
})
|
||||
scr.BoundsBody = `{"confidence":"high","trim_action":"trim","start_segment_id":10,"end_segment_id":868,"warnings":[]}`
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestTrimStageConsumesNormalizedAndProducesTrimmedTranscript(t *testing.T) {
|
||||
if trimReq.KeepSelector != "10-868" {
|
||||
t.Fatalf("keep selector = %q, want %q", trimReq.KeepSelector, "10-868")
|
||||
}
|
||||
if trimReq.OutputTrimmedPath != filepath.Join(paths.TranscriptsDir, "trimmed.json") {
|
||||
if trimReq.OutputTrimmedPath != filepath.Join(paths.TranscriptsDir, "final.trimmed.json") {
|
||||
t.Fatalf("trim output = %q", trimReq.OutputTrimmedPath)
|
||||
}
|
||||
|
||||
@@ -54,13 +54,13 @@ func TestTrimStageConsumesNormalizedAndProducesTrimmedTranscript(t *testing.T) {
|
||||
if len(result.Outputs) < 2 {
|
||||
t.Fatalf("outputs = %#v, want trimmed+bounds outputs", result.Outputs)
|
||||
}
|
||||
if result.Outputs[0].Kind != "transcript_trimmed" {
|
||||
t.Fatalf("output[0] kind = %q, want transcript_trimmed", result.Outputs[0].Kind)
|
||||
if result.Outputs[0].Kind != "transcript_final_trimmed" {
|
||||
t.Fatalf("output[0] kind = %q, want transcript_final_trimmed", result.Outputs[0].Kind)
|
||||
}
|
||||
if result.Outputs[1].Kind != "session_bounds" {
|
||||
t.Fatalf("output[1] kind = %q, want session_bounds", result.Outputs[1].Kind)
|
||||
}
|
||||
if result.Metadata["trimmed_output_path"] != filepath.Join(paths.TranscriptsDir, "trimmed.json") {
|
||||
if result.Metadata["trimmed_output_path"] != filepath.Join(paths.TranscriptsDir, "final.trimmed.json") {
|
||||
t.Fatalf("metadata trimmed_output_path = %#v", result.Metadata["trimmed_output_path"])
|
||||
}
|
||||
if result.Metadata["keep_selector"] != "10-868" {
|
||||
@@ -71,7 +71,7 @@ func TestTrimStageConsumesNormalizedAndProducesTrimmedTranscript(t *testing.T) {
|
||||
func TestTrimStageUsesConfiguredScriptoriumInputName(t *testing.T) {
|
||||
env, m, scr, _ := setupTrimEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
normalized := filepath.Join(paths.TranscriptsDir, "normalized.json")
|
||||
normalized := filepath.Join(paths.TranscriptsDir, "final.json")
|
||||
writeFile(t, normalized, `{"segments":[{"id":10},{"id":11}]}`)
|
||||
scr.BoundsBody = `{"trim_action":"trim","start_segment_id":10,"end_segment_id":11}`
|
||||
|
||||
@@ -95,7 +95,7 @@ func TestTrimStageUsesConfiguredScriptoriumInputName(t *testing.T) {
|
||||
func TestTrimStageRecordsLogAndGeneratedConfigRefs(t *testing.T) {
|
||||
env, m, scr, _ := setupTrimEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
scr.BoundsBody = `{"trim_action":"trim","start_segment_id":1,"end_segment_id":2}`
|
||||
|
||||
result, err := (trimStage{}).Run(context.Background(), env, m)
|
||||
@@ -115,7 +115,7 @@ func TestTrimStageRecordsLogAndGeneratedConfigRefs(t *testing.T) {
|
||||
func TestTrimStageRenderDebugDiagnosticsAreNotStageOutputs(t *testing.T) {
|
||||
env, m, scr, _ := setupTrimEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
scr.BoundsBody = `{"trim_action":"trim","start_segment_id":1,"end_segment_id":2}`
|
||||
|
||||
cfg := *env.Config.Pipeline.Trim
|
||||
@@ -153,7 +153,7 @@ func TestTrimStageFailsWhenNormalizedTranscriptMissing(t *testing.T) {
|
||||
func TestTrimStageDoesNotFallBackToProcessedTranscript(t *testing.T) {
|
||||
env, m, scr, ser := setupTrimEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "processed.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "polished.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
|
||||
_, err := (trimStage{}).Run(context.Background(), env, m)
|
||||
if err == nil {
|
||||
@@ -173,7 +173,7 @@ func TestTrimStageDoesNotFallBackToProcessedTranscript(t *testing.T) {
|
||||
func TestTrimStageFailsWhenNormalizedTranscriptInvalidJSON(t *testing.T) {
|
||||
env, m, _, _ := setupTrimEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), "not-json")
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), "not-json")
|
||||
_, err := (trimStage{}).Run(context.Background(), env, m)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
@@ -186,7 +186,7 @@ func TestTrimStageFailsWhenNormalizedTranscriptInvalidJSON(t *testing.T) {
|
||||
func TestTrimStageFailsWhenNormalizedTranscriptMissingSegmentsArray(t *testing.T) {
|
||||
env, m, _, _ := setupTrimEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), `{"schema":"audita.processed.v1"}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"schema":"audita.processed.v1"}`)
|
||||
_, err := (trimStage{}).Run(context.Background(), env, m)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
@@ -199,7 +199,7 @@ func TestTrimStageFailsWhenNormalizedTranscriptMissingSegmentsArray(t *testing.T
|
||||
func TestTrimStageFailsWhenBoundsOutputInvalidJSON(t *testing.T) {
|
||||
env, m, scr, _ := setupTrimEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
scr.BoundsBody = "not-json"
|
||||
_, err := (trimStage{}).Run(context.Background(), env, m)
|
||||
if err == nil {
|
||||
@@ -213,7 +213,7 @@ func TestTrimStageFailsWhenBoundsOutputInvalidJSON(t *testing.T) {
|
||||
func TestTrimStageFailsWhenBoundsRangeIsDescending(t *testing.T) {
|
||||
env, m, scr, _ := setupTrimEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
scr.BoundsBody = `{"trim_action":"trim","start_segment_id":2,"end_segment_id":1}`
|
||||
_, err := (trimStage{}).Run(context.Background(), env, m)
|
||||
if err == nil {
|
||||
@@ -227,7 +227,7 @@ func TestTrimStageFailsWhenBoundsRangeIsDescending(t *testing.T) {
|
||||
func TestTrimStageFailsWhenBoundsIDsMissingFromTranscript(t *testing.T) {
|
||||
env, m, scr, _ := setupTrimEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), `{"segments":[{"id":20},{"id":21}]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":20},{"id":21}]}`)
|
||||
scr.BoundsBody = `{"trim_action":"trim","start_segment_id":10,"end_segment_id":21}`
|
||||
_, err := (trimStage{}).Run(context.Background(), env, m)
|
||||
if err == nil {
|
||||
@@ -241,7 +241,7 @@ func TestTrimStageFailsWhenBoundsIDsMissingFromTranscript(t *testing.T) {
|
||||
func TestTrimStageFailsWhenScriptoriumAdapterFails(t *testing.T) {
|
||||
env, m, scr, _ := setupTrimEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
scr.RunErr = errors.New("bounds failed")
|
||||
_, err := (trimStage{}).Run(context.Background(), env, m)
|
||||
if err == nil {
|
||||
@@ -255,7 +255,7 @@ func TestTrimStageFailsWhenScriptoriumAdapterFails(t *testing.T) {
|
||||
func TestTrimStageFailsWhenSeriatimTrimAdapterFails(t *testing.T) {
|
||||
env, m, scr, ser := setupTrimEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "normalized.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
writeFile(t, filepath.Join(paths.TranscriptsDir, "final.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||
scr.BoundsBody = `{"trim_action":"trim","start_segment_id":1,"end_segment_id":2}`
|
||||
ser.TrimErr = errors.New("trim failed")
|
||||
_, err := (trimStage{}).Run(context.Background(), env, m)
|
||||
@@ -270,7 +270,7 @@ func TestTrimStageFailsWhenSeriatimTrimAdapterFails(t *testing.T) {
|
||||
func TestTrimStageDisabledCopiesNormalizedTranscript(t *testing.T) {
|
||||
env, m, scr, ser := setupTrimEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
normalized := filepath.Join(paths.TranscriptsDir, "normalized.json")
|
||||
normalized := filepath.Join(paths.TranscriptsDir, "final.json")
|
||||
normalizedBody := `{"segments":[{"id":1,"text":"alpha"},{"id":2,"text":"beta"}]}`
|
||||
writeFile(t, normalized, normalizedBody)
|
||||
|
||||
@@ -288,7 +288,7 @@ func TestTrimStageDisabledCopiesNormalizedTranscript(t *testing.T) {
|
||||
if result.Metadata["trim_action"] != "copy_disabled" {
|
||||
t.Fatalf("metadata trim_action = %#v, want copy_disabled", result.Metadata["trim_action"])
|
||||
}
|
||||
trimmedPath := filepath.Join(paths.TranscriptsDir, "trimmed.json")
|
||||
trimmedPath := filepath.Join(paths.TranscriptsDir, "final.trimmed.json")
|
||||
data, err := os.ReadFile(trimmedPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read trimmed transcript: %v", err)
|
||||
@@ -304,7 +304,7 @@ func TestTrimStageUsesRunLocalPathsAndPromotesCanonical(t *testing.T) {
|
||||
m.Campaign = "sample-campaign"
|
||||
m.RunID = "20260518T010203Z-abcdef12"
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
normalized := filepath.Join(paths.TranscriptsDir, "normalized.json")
|
||||
normalized := filepath.Join(paths.TranscriptsDir, "final.json")
|
||||
writeFile(t, normalized, `{"segments":[{"id":10},{"id":11}]}`)
|
||||
scr.BoundsBody = `{"trim_action":"trim","start_segment_id":10,"end_segment_id":11,"warnings":[]}`
|
||||
|
||||
@@ -438,7 +438,7 @@ func setupTrimEnv(t *testing.T) (*Env, *manifest.Manifest, *boundsScriptoriumRun
|
||||
},
|
||||
Trim: &config.TrimConfig{
|
||||
Enabled: true,
|
||||
OutputPath: "transcripts/trimmed.json",
|
||||
OutputPath: "transcripts/final.trimmed.json",
|
||||
Bounds: config.TrimBoundsConfig{
|
||||
PromptID: "dnd_session.bounds",
|
||||
ProfileID: "",
|
||||
|
||||
Reference in New Issue
Block a user