Files
narratio/docs/roadmap/campaign.md

8.0 KiB

Roadmap: Campaign Registry

Status: Implemented

Problem

Narratio currently treats campaign configuration as one selected campaign.yml file:

  • command flags use --campaign <path>;
  • 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 <id> selects a campaign ID.
  • --campaign-file <path> 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 <path> 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 <path>;
    2. explicit --campaign <id>;
    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 <id>.
  • Commands can load a specific file with --campaign-file <path>.
  • 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 <id>;
    • --campaign-file <path>.
  • Update troubleshooting examples that currently pass --campaign <path>.

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 <id> 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 <path> to --campaign <id> or --campaign-file <path>.