# Roadmap: Campaign Registry Status: Implemented ## Problem Narratio currently treats campaign configuration as one selected `campaign.yml` file: - command flags use `--campaign `; - default discovery searches fixed system file locations; - `campaign.yml` uses `campaign:` as the identity field. That model works for a single campaign, but it is awkward for installations that manage multiple campaigns. Operators need to pass file paths or maintain a single global campaign config, while the newer session-oriented CLI already uses concise positional session IDs and remote session lookup. The campaign selection model should become ID-based and pipeline-owned. Pipeline config should describe where campaigns live, commands should select a campaign by ID, and each campaign directory should contain its stable campaign materials. ## Target Model `pipeline.yml` owns the campaign registry: campaigns: root: /usr/local/share/narratio/campaigns default_campaign_id: dilfs Campaign files live at the conventional path: {campaigns.root}/{campaign_id}/campaign.yml The first implementation should use only the conventional path. Recursive discovery of every `campaign.yml` under `campaigns.root` is deferred to a future stage. Each campaign file uses `campaign_id` as the canonical identity field: campaign_id: dilfs session_template_file: ./session.template.yml inputs: speakers_file: ./speakers.yml autocorrect_file: ./autocorrect.yml glossary_file: ./glossary.yml Campaign-relative files continue to resolve relative to the selected `campaign.yml`, including stable input files and `session_template_file`. The public CLI changes from path-based campaign selection to ID-based campaign selection: - `--campaign ` selects a campaign ID. - `--campaign-file ` explicitly loads one campaign file for development, tests, and unusual local workflows. - `--campaign` and `--campaign-file` are mutually exclusive. If neither `--campaign` nor `--campaign-file` is passed, Narratio uses `pipeline.campaigns.default_campaign_id`. If no campaign can be selected, commands fail clearly before session loading or stage execution. Resolved campaign ID remains the campaign segment used for: - workspace paths; - spool paths; - S3 session prefixes; - remote `session.yml` lookup; - archive locks and promoted output keys; - session/campaign mismatch validation; - status, plan, restore, and helper output. ## Compatibility Policy This is a breaking public/config contract change. After the cutover: - `--campaign` no longer accepts a filesystem path; - default fixed campaign file discovery is removed; - `campaign:` is no longer accepted in `campaign.yml`; - `campaign_id:` is required. Keep `--campaign-file` as the only explicit file override. Do not retain hidden aliases for the old `--campaign ` behavior. ## Implementation Stages ### Stage 1: Add Campaign Registry Selection Status: Implemented Add the registry model and switch command loading to resolve campaigns through pipeline config. Implementation requirements: - Add `pipeline.campaigns.root`. - Add `pipeline.campaigns.default_campaign_id`. - Add `campaign_id` to campaign config and make it the canonical identity. - Resolve pipeline config first, then campaign selection. - Use this selection order: 1. explicit `--campaign-file `; 2. explicit `--campaign `; 3. `pipeline.campaigns.default_campaign_id`; 4. fail clearly. - For ID selection, load `{campaigns.root}/{campaign_id}/campaign.yml`. - Validate that the loaded `campaign_id` matches the selected ID. - Reject `--campaign` with `--campaign-file`. - Preserve strict YAML decoding. - Preserve campaign-relative stable input and session template resolution. - Keep storage details behind the existing storage adapter and object-store helper. - Keep remote session lookup and archive key construction based on the resolved campaign ID. Acceptance criteria: - Commands can run with only a pipeline config and the pipeline default campaign ID. - Commands can select another campaign with `--campaign `. - Commands can load a specific file with `--campaign-file `. - Existing session loading, remote session fallback, prepare materialization, restore, archive, locks, clean, analyze, and publish behavior continue to use the same resolved campaign identity. - No generic config registry framework is introduced. ### Stage 2: Remove Old Single-File Campaign Behavior Status: Implemented Remove the old public campaign file model after registry selection is in place. Implementation requirements: - Remove fixed default campaign config discovery from command loading. - Remove `DefaultCampaignConfigSearchPaths` and related path-only resolution if no current tests or helpers still need them. - Remove support for `campaign:` from `campaign.yml`. - Update validation errors to refer to `campaign_id`. - Update examples to use campaign directories and `campaign_id`. - Update current-behavior docs to document: - `pipeline.campaigns.root`; - `pipeline.campaigns.default_campaign_id`; - `campaign_id`; - `--campaign `; - `--campaign-file `. - Update troubleshooting examples that currently pass `--campaign `. Acceptance criteria: - `campaign.yml` files with `campaign:` fail strict decoding. - `--campaign /path/to/campaign.yml` is treated as a campaign ID and fails unless that ID exists under `campaigns.root`. - `--campaign-file /path/to/campaign.yml` is the supported file override. - User-facing docs no longer describe fixed campaign config discovery. ## Test Guidance Focused tests: - `go test ./internal/config -v` - `go test ./internal/app -v` - `go test ./internal/stage -run Prepare -v` Full validation: - `go test ./...` Config tests to add or update: - strict decode accepts `pipeline.campaigns.root`; - strict decode accepts `pipeline.campaigns.default_campaign_id`; - strict decode accepts `campaign_id`; - selected campaign ID mismatch fails; - missing campaign root fails when ID selection is needed; - missing default campaign ID fails when no explicit campaign selector is passed; - old `campaign:` fails after Stage 2. App tests to add or update: - `--campaign ` resolves `{campaigns.root}/{id}/campaign.yml`; - omitted `--campaign` uses `pipeline.campaigns.default_campaign_id`; - `--campaign-file` loads an explicit campaign file; - `--campaign` plus `--campaign-file` fails; - remote session fallback uses the resolved campaign ID; - `session init`, `run`, `run-stage`, `resume`, `analyze`, `publish`, `clean`, and `session` subcommands all use the same campaign selection path; - path-based `--campaign` examples and tests are removed after Stage 2. ## Documentation Guidance Update current-behavior docs only after implementation lands: - `docs/config.md` - `docs/cli.md` - `docs/operations.md` - `docs/troubleshooting.md` - relevant files under `docs/internal/` - `examples/` Planned campaign registry behavior belongs only in this roadmap until the code, tests, examples, and current-behavior docs are updated. ## Architecture Guardrails - Keep Narratio explicit and stage-driven. - Do not introduce a generic configuration registry or workflow framework. - Keep YAML decoding strict. - Keep defaults centralized and testable. - Keep campaign-relative path resolution centralized. - Use centralized S3 and workspace path helpers. - Keep storage details behind `storage.ObjectStore`. - Keep secret-backed object-store construction in `internal/app`. - Preserve manifest-driven resume and restore behavior. - Do not store raw secrets in campaign configs, manifests, logs, generated configs, or archive metadata. ## Assumptions - The canonical pipeline schema is grouped under `campaigns`. - The canonical campaign identity field is `campaign_id`. - `--campaign` means campaign ID. - `--campaign-file` is retained as an explicit override. - Recursive discovery is planned but not part of the first implementation. - Existing production configs can be migrated from `campaign:` to `campaign_id:` and from `--campaign ` to `--campaign ` or `--campaign-file `.