32 KiB
Pipeline Configuration Ergonomics
Status
Accepted target state. This document owns the intended feature scope, user intent, policy choices, and target end state until the behavior is implemented.
Goal
Make Narratio's pipeline configuration easier to organize, vary, inspect, and reuse without weakening its strict configuration contract or turning it into a general-purpose configuration or workflow language.
The feature should let operators:
- split a large pipeline configuration into explicitly imported partial files;
- maintain one production-default pipeline plus a selectable testing profile;
- keep stable settings separate from frequently edited models and artifacts;
- define a character-oriented artifact workflow once and expand it for every character in the selected campaign;
- reuse one campaign-owned party roster across multiple artifact families and the Notarius integration; and
- inspect and compare the fully resolved configuration before running a session.
All composition and expansion must produce the same concrete, validated
PipelineConfig and concrete configured-artifact map consumed by the existing
stage, planning, manifest, artifact, and adapter boundaries.
User Intent
Pipeline configuration currently combines long-lived platform and transcript settings with model choices, prompt-backed artifact definitions, and publish rules that change frequently during development. Keeping all of those concerns in one file makes edits noisy and makes production/testing variation difficult to review.
Character-specific artifacts introduce a second kind of repetition. Several artifact families, including character meta-analysis and character-specific item tracking, should execute once per campaign character. Their prompt, inputs, dependencies, and output shape are shared, while player, character, class, alias, and output identity vary by party member. Those campaign facts do not belong in a global pipeline definition.
The desired model is therefore:
pipeline.yml + explicit imports + one selected profile
│
campaign.yml + party.yml ─┤
▼
resolved effective configuration
│
expand party-driven families
│
defaults and strict validation
▼
existing concrete pipeline and artifact model
Architectural Constraints
The feature must preserve the configuration and orchestration policies in
docs/policy/architecture.md:
- YAML remains strict and rejects unknown fields.
- Defaults remain centralized and are applied exactly once.
- Composition and expansion are deterministic and testable.
- Session templating remains narrow; this feature does not introduce arbitrary expressions, scripting, or a general template language.
- The canonical stage sequence and fixed invalidation relation remain application-owned and non-configurable.
- Stages receive resolved Narratio-owned configuration and do not implement import, profile, or family semantics independently.
- External tools continue to own their private runtime configuration and defaults.
- Raw secrets remain indirect and are never introduced into composed YAML, effective-configuration output, manifests, or diagnostics.
Configuration composition belongs to internal/config. CLI selection and
production loading belong to internal/app. Party parsing must be a
Narratio-owned campaign-input contract. Artifact-family expansion must finish
before existing configured-artifact validation, catalog construction,
planning, reconciliation, or execution begins.
Configuration Imports
Root document
Keep pipeline.yml as the selected pipeline entry point. Add an optional
top-level composition envelope with explicitly ordered imports:
composition:
imports:
- conf.d/platform.yml
- conf.d/transcripts.yml
- conf.d/extraction.yml
- conf.d/artifacts.yml
- conf.d/publish.yml
default_profile: production
profiles:
production:
overlay: profiles/production.yml
testing:
overlay: profiles/testing.yml
An existing monolithic pipeline.yml without composition remains valid and
retains its current behavior. The root document may contain ordinary pipeline
fields alongside composition, allowing incremental migration.
imports is the public term. Imported files are partial pipeline
configuration documents rather than independently runnable pipelines.
Import boundaries
- Only the root pipeline document may declare
composition, imports, or profiles. Imported and overlay documents cannot import other documents. - Every import is explicit. Narratio does not automatically scan
conf.dor interpret unlisted files. - Import paths are relative to the root pipeline file's directory, must remain confined beneath that directory, and must resolve to supported regular YAML files. Absolute paths, traversal, non-regular files, and cycles are rejected.
- All ordinary relative pipeline paths retain one stable base: the root pipeline file's directory. Moving a field between imported files must not silently change the meaning of its relative path.
- The declared import order is retained for diagnostics and provenance, but it is not an implicit precedence mechanism between base imports.
- Composition metadata is removed before the effective mapping is decoded as
strict
PipelineConfigdata.
Additive merge contract
Base configuration from the root document and its imports is additive:
- mappings merge recursively when their child fields are disjoint;
- two files may therefore contribute different entries beneath a shared map
such as
scriptorium.artifacts; - defining the same final field or keyed entry in more than one base source is an error, even when the values are identical;
- lists are atomic values and cannot be contributed more than once at the same base configuration path;
- mapping/scalar, mapping/list, and other incompatible type collisions are errors; and
- duplicate YAML keys within one source remain errors.
Errors must identify the complete configuration path and every source file that claims it. This contract prevents an import reorder or newly added file from silently changing production behavior.
Composition must operate on a presence-aware YAML representation before Go
struct decoding. Merging already decoded structs is not sufficient because it
cannot reliably distinguish omission from explicitly configured false, zero,
an empty collection, or another meaningful zero value.
Named Profiles
Selection and precedence
Exactly one named profile may be active for an invocation. A composition root
may define default_profile; the maintained production-shaped configuration
uses production. Add --profile <name> to the common configuration flags for
every command that loads pipeline configuration:
narratio run SESSION
narratio run SESSION --profile testing
narratio session plan SESSION --profile testing
narratio regenerate-artifacts SESSION --profile testing
The explicit CLI value overrides default_profile. An unknown profile, a
profile selection against a configuration that defines no such profile, an
empty profile name, or a default naming an undefined profile is an error before
stage composition. Do not add an environment-variable selector in this
feature; profile selection should remain visible in the command or root
configuration.
Only one profile can be selected. Profiles cannot extend other profiles, and callers cannot stack several profiles. A single overlay provides the required production/testing variation without creating user-programmable precedence.
Overlay semantics
The selected profile is the sole intentional override layer:
- mappings recursively merge with the assembled base mapping;
- scalar and boolean leaves replace base leaves;
- lists replace the complete base list rather than concatenate implicitly;
- keyed maps such as configured artifacts merge by key, allowing a testing profile to add experimental artifacts;
- an existing artifact can be disabled explicitly with
enabled: false; - an overlay cannot change a value's YAML kind incompatibly; and
- the fully overlaid result undergoes the same strict decode, defaults, path resolution, expansion, and validation as a monolithic configuration.
Generic deletion syntax is outside this feature. Shared definitions should live in base imports, while production-only and testing-only definitions live in their corresponding overlays. Existing enable/disable fields and complete list replacement cover the required cases without a YAML patch language.
Effective identity and provenance
Every loaded configuration must retain bounded, non-secret provenance:
- the root pipeline path;
- the selected profile name, including whether it came from the default or CLI;
- the ordered imported and overlay source paths;
- a deterministic digest of the normalized effective pipeline configuration; and
- enough source ownership information to explain composition errors and effective values.
Run reporting and invocation state should identify the selected profile and effective configuration digest. The profile name is provenance, not a blanket cache key: changing profiles must invalidate only work whose result-affecting effective configuration changed.
Canonical Campaign Party Contract
Ownership and schema
Make party.yml a strict, versioned Narratio campaign-input contract. A
canonical file has this shape:
schema_version: narratio.party.v1
characters:
arannis:
player:
name: Eric
character:
name: Arannis
alias:
- Ari
- The Grey Owl
classes:
- name: wizard
level: 8
brenna:
player:
name: Jane
character:
name: Brenna
classes:
- name: paladin
level: 6
- name: warlock
level: 2
The contract has these semantics:
schema_versionis required and must equalnarratio.party.v1.charactersis a non-empty mapping.- Each mapping key is the stable
character_idand must satisfy the existing configured-artifact key grammar. player.nameandcharacter.nameare required, non-empty display strings.character.aliasis optional and accepts a list of zero or more non-empty alias strings. The singular field spellingaliasis intentional.- Leading or trailing whitespace and control characters in names, aliases, and class names are rejected rather than silently normalized.
- A character alias cannot equal that character's primary name, another alias, or another character's primary name or alias under case-insensitive comparison. This keeps party-member grounding unambiguous. Player names may repeat because one player may control more than one character.
character.classesis required and non-empty. Every entry has a required free-formnameand an optional positive integerlevel.- Class names are not restricted to a Narratio-owned D&D enumeration.
- Duplicate class names for one character are rejected case-insensitively.
- Class order and alias order are preserved.
- Narratio derives a deterministic class summary such as
paladin 6 / warlock 2and an alias summary that joins declared aliases with,in their declared order. - Unknown fields and trailing YAML documents are rejected.
The stable character_id, rather than a display name, owns generated artifact
identity. Changing a display name, alias, class, or level retains that identity
and changes the relevant semantic inputs. Changing the mapping key is an
intentional remove-and-add operation.
Load and prepare behavior
The selected campaign's effective party_file resolves relative to
campaign.yml. Narratio must load and validate canonical party data during
combined pipeline/campaign resolution so session plan, configuration
inspection, and artifact-family expansion see the same roster before stage
execution.
prepare continues to materialize the canonical document as
inputs/party.yml, record its source and content identity, and expose it as
narratio.input.party. The same canonical file is passed unchanged to
Notarius's party reference slot, which currently accepts YAML reference
material without imposing a competing roster schema.
Players projection and compatibility
The canonical party document becomes the eventual single source of truth for
player-to-character relationships. Narratio derives a deterministic,
documented players-only YAML projection during prepare, materializes it at
inputs/players.yml, records its content identity, and exposes it through the
existing narratio.input.players source. This preserves the separate Notarius
players reference and existing Scriptorium source without requiring every
campaign to maintain duplicate facts.
The projection uses this versioned shape, with entries sorted by stable character ID:
schema_version: narratio.players.v1
players:
- name: Eric
character:
id: arannis
name: Arannis
alias:
- Ari
- The Grey Owl
There is one projection entry per character rather than one grouped entry per display player name. This permits one player to control several characters and does not conflate distinct players who happen to share a display name.
Backward compatibility must be narrow and removable:
- an unversioned legacy party file continues to be treated as opaque reference
material when the existing
players_fileis also configured; - legacy mode preserves the current prepared
partyandplayersbehavior but cannot drive artifact-family expansion; - configuring a party-driven artifact family with a legacy roster produces a clear migration error;
- canonical
narratio.party.v1mode derives players and rejects a separateplayers_file, preventing contradictory authorities; - parsing, validation, documentation, and tests for legacy mode live behind a clearly identified compatibility boundary with comments stating that it is intended for removal after migration; and
- the maintained examples migrate to canonical mode and demonstrate no separate players file.
The derived players projection is a durable integration contract and must be
documented under docs/integrations/ rather than left as incidental generated
YAML.
Party-Driven Artifact Families
Configuration model
Add scriptorium.artifact_families, separate from the existing concrete
scriptorium.artifacts map:
scriptorium:
artifact_families:
character_meta:
enabled: true
for_each: party.characters
prompt_id: dnd.character_meta
profile_id: production
output_path_pattern: artifacts/characters/{character_id}/meta.md
inputs:
transcript:
source: narratio.transcript.final_trimmed
required: true
member_vars:
player_name: player.name
character_name: character.name
character_class: character.class_summary
character_aliases: character.alias_summary
character_items:
enabled: true
for_each: party.characters
prompt_id: dnd.character_items
profile_id: production
output_path_pattern: artifacts/characters/{character_id}/items.md
inputs:
transcript:
source: narratio.transcript.final_trimmed
required: true
item_occurrences:
source: narratio.extraction.item_occurrences
required: true
member_vars:
character_name: character.name
Families support the ordinary result-affecting fields shared by concrete
Scriptorium artifacts: enablement, prompt and profile IDs, timeout,
render-debug policy, ordinary dependencies, inputs, and static variables.
output_path_pattern replaces concrete output_path at the family level.
for_each is not an expression language. This feature accepts only the exact
source party.characters. member_vars maps a Scriptorium variable name to
one of a closed set of canonical values:
character_id;player.name;character.name;character.class_summary; andcharacter.alias_summary.
Static vars and resolved member variables merge deterministically. Duplicate
variable names across the two maps are rejected rather than assigned implicit
precedence. Narratio's existing reserved sticky-session variable remains
application-owned and is applied after expansion under its current rules.
Narrow substitution
{character_id} is the only family substitution supported in this feature.
It is required in output_path_pattern and may appear exactly where documented
for family-owned publish destinations. Unknown, repeated in an invalid
position, unresolved, or malformed substitutions are errors. Names, aliases,
classes, environment values, and arbitrary YAML paths cannot be interpolated
into configuration strings.
Concrete expansion
For each family and party character, resolution creates one ordinary concrete artifact key by joining the normalized family key, an underscore, and the stable character ID:
character_meta_arannis
character_items_arannis
Expansion must be sorted, deterministic, and completed before existing configured-artifact defaults and validation. Every generated artifact then uses the existing catalog, dependency planner, fingerprinting, manifest, reconciliation, execution, materialization, and publish boundaries.
Validation rejects:
- invalid family keys or character IDs;
- generated keys that fail the existing configured-artifact grammar;
- collisions between generated artifacts, explicit concrete artifacts, or family names;
- duplicate or escaping output paths;
- a family with no canonical party source;
- invalid member-variable selectors;
- a selected profile that leaves an executable family incomplete; and
- any expanded artifact that fails ordinary concrete-artifact validation.
An added party character creates missing concrete artifacts. Removing a character removes those artifacts from effective configuration so existing analyze reconciliation classifies their old records as removed. A change to a member field changes only fingerprints that actually consume its resolved value or the party document as an input.
Dependencies between families
Ordinary depends_on continues to name shared concrete artifacts such as
session_recap. Add a typed member_dependencies list for corresponding
members of another family:
character_items:
member_dependencies:
- character_meta
For arannis, this expands to a dependency on
character_meta_arannis. The referenced family must use the same party source
and generate the same character ID. Cycles and missing members are rejected by
the existing concrete dependency validation after expansion.
When a family input consumes the corresponding member output, provide a narrow pre-expansion member-artifact source form owned by configuration resolution. The syntax is:
inputs:
prior_meta:
source: narratio.member_artifact.character_meta
required: true
For the arannis member, this resolves to
narratio.artifact.character_meta_arannis before ordinary runtime validation.
The referenced family must also appear in member_dependencies. The
narratio.member_artifact.* form must not survive into stage or adapter
configuration as a new runtime artifact kind.
Selection and reporting
Extend artifact selection so an exact family key selects all of that family's concrete members. An exact generated key selects only that member. Explicitly selecting a disabled family follows the existing rule for explicitly selected disabled concrete artifacts: all generated targets must still have valid executable fields. Mixed family and concrete selections are normalized and deduplicated after expansion.
Planning, run summaries, artifact listing, and effective-configuration output must show both family origin and concrete artifact identity. Execution remains entirely concrete; a family is not a dynamic pipeline stage or runtime loop.
Family publish rules
Allow a family to declare one typed publish policy so operators need not repeat one publish output per character:
publish:
enabled: true
required: false
dest_pattern: artifacts/characters/{character_id}/meta.md
Resolution expands this into ordinary concrete publish rules. When
dest_pattern is omitted, the generated artifact's output path is used where
the existing publish contract permits derivation. Conflicts with explicit
publish rules, duplicate destinations, unsafe paths, and generated sources
that are unavailable are rejected by the existing publish policy after
expansion. Family publish behavior must not introduce wildcard source matching
at runtime.
Profile-Safe Resume And Invalidation
Named profiles are safe only if result-affecting model and configuration changes cannot silently reuse incompatible successful stage results. Artifact analysis already has a versioned semantic fingerprint that includes effective prompt/profile identifiers, variables, dependencies, inputs, and output identity. The broader pipeline requires the same principle at every applicable stage boundary.
Audit each stage's resume contract and add a versioned semantic configuration fingerprint wherever current evidence does not already cover all Narratio-observable result-affecting settings. In particular, switching an Audita model through a profile must not reuse polish output produced by the testing model. Conversely, changing only an operational timeout, binary path, workspace path, diagnostic location, or profile name must not invalidate byte-equivalent semantic work unless that value genuinely affects the stage's canonical result.
The selected profile name and complete effective-config digest are retained as provenance. Resume decisions use stage-specific effective semantics rather than the profile name as a blanket invalidator. This permits production and testing profiles to share unaffected transcript work while correctly staling changed stages and their fixed transitive dependents.
Private configuration, prompt, model, executable, and module contents loaded inside an external tool remain outside Narratio's observable fingerprint boundary unless an existing integration contract exposes their identity. Changing such private inputs continues to require explicit force, and the documentation must not claim otherwise.
Configuration Inspection
Add a config command family that performs composition without running a
session:
narratio config validate [--config <pipeline.yml>] [--profile <name>]
narratio config show [--config <pipeline.yml>] [--profile <name>]
narratio config sources [--config <pipeline.yml>] [--profile <name>]
narratio config diff <left-profile> <right-profile> [--config <pipeline.yml>]
Required behavior:
validatecomposes imports and the selected profile, resolves campaign data when required for party-driven families, expands artifacts and publish rules, applies defaults, and runs strict validation without creating session or run state.showemits deterministic normalized effective YAML, including generated concrete artifacts, without raw credentials or environment values.sourcesreports root, import, profile, campaign, and party provenance and identifies which source owns effective configuration paths.diffcompares normalized effective configurations rather than raw files and clearly identifies additions, removals, and value changes.- output order is deterministic and suitable for review, but output formatting is not a substitute for the effective configuration digest.
If family expansion requires a campaign, these commands accept the existing campaign selection mechanisms or report clearly that pipeline-only validation cannot complete the party-dependent portion. They must not silently choose an unintended campaign.
Required Application Changes
The implemented feature will require coordinated changes across existing owners rather than a parallel configuration subsystem:
internal/config: presence-aware YAML composition, import confinement, profile overlays, provenance, effective digesting, party parsing and validation, players projection, family expansion, defaults, and final strict validation;internal/app:--profileplumbing through every configuration-consuming command,configinspection commands, campaign-aware inspection, profile reporting, and one shared loading path;internal/artifactpolicyandinternal/artifacts: generated concrete source registration, family-aware selection metadata, derived-player identity, and reuse of the existing configured-artifact grammar and path policy;internal/stage: consumption only of expanded concrete artifacts, party and derived-player preparation, and stage-specific semantic resume evidence;internal/manifest: bounded profile/effective-config provenance and any versioned stage semantic fingerprints required for safe reuse;- publish planning: expansion of family publish declarations into existing concrete rules before ordinary validation and execution;
- Notarius composition: continue passing prepared
partyandplayerssources, with canonical mode sourcing both from the party-owned contract; and - Scriptorium composition: receive only resolved concrete artifacts and string/bool variables under the existing adapter contract.
There must be one production configuration-loading path. Remote session loading, restore, status, planning, helper commands, and execution must not independently reimplement import, profile, campaign, or family selection.
Documentation And Examples
When behavior is implemented, update the canonical owners in the same change:
docs/config.md: import, merge, profile, party selection, family fields, defaults, and validation contracts;docs/cli.md:--profileandconfigcommand syntax and output behavior;docs/operations.md: recommended production/testing bundle layout, migration, inspection, and profile-switching workflow;docs/policy/architecture.md: only the durable composition, campaign-data ownership, and semantic-resume invariants;docs/integrations/: the canonical party and derived players YAML contracts;docs/internal/overview.mdand focused internal configuration, manifest, analyze, prepare, and publish documents: implemented ownership and mechanics; andexamples/: a validated split configuration bundle, production/testing overlays, canonical party roster with aliases and multiclass data, at least two party-driven artifact families, and representative family publishing.
Do not copy complete example bundles into reference prose. Maintained examples must be secret-free and loaded by the repository's example validation test.
Testing Expectations
Testing should protect the public contracts and high-risk composition and state-reuse behavior without coupling to private merge helpers. Required confidence includes:
- monolithic configuration compatibility;
- recursive mapping composition and all duplicate/type/list conflict classes;
- import confinement, missing files, non-regular files, unsupported nesting, source-aware diagnostics, deterministic ordering, and duplicate YAML keys;
- explicit zero-value profile overrides, list replacement, keyed-map additions, unknown profiles, invalid defaults, and absence of profile stacking;
- canonical party schema validation, aliases, alias/name ambiguity, multiclass summaries, stable IDs, malformed data, and legacy/canonical mode separation;
- deterministic players projection and prepared-input provenance;
- deterministic multi-family expansion, generated identities and paths, variable binding, same-member dependencies, collisions, additions, removals, and family selection;
- expansion into ordinary analyze planning, fingerprints, manifests, partial selection, failure handling, and publish rules;
- model/profile changes staling only stages whose semantic configuration changed while unaffected transcript work remains reusable;
- representative CLI validation, show, sources, diff, plan, and run behavior; and
- maintained monolithic, imported, production, and testing examples loading and validating offline.
Use package-level behavioral tests through the narrowest stable owner. A small number of assembled workflow tests should prove that composed profiles and party-driven families reach the existing pipeline correctly; higher-level tests should not repeat every parser and validation case.
Compatibility And Migration
- Existing monolithic pipeline files remain supported.
- Omitting
--profileretains existing behavior for configurations without profiles and selects the declared production default for composed bundles. - Existing explicit concrete Scriptorium artifacts and publish rules remain valid and may coexist with non-colliding families.
- Legacy opaque party plus players files remain temporarily usable only through the isolated compatibility mode and cannot power party-driven families.
- Migration consists of converting the campaign party document to
narratio.party.v1, removing the separate players file, and then enabling party-driven families. - Configuration inspection must make the migration result reviewable before a session run.
- Compatibility code and documentation must clearly identify the eventual removal boundary; new features must target canonical party mode rather than extend legacy semantics.
Non-Goals And Deferred Work
This feature does not introduce:
- automatic loading of every file in
conf.d; - recursive imports, imports in profile overlays, or profile inheritance;
- multiple simultaneously active profiles;
- environment-selected profiles;
- generic YAML patch/delete operations;
- arbitrary string interpolation, expressions, scripting, or loops;
- artifact expansion from arbitrary files, Notarius outputs, or runtime data;
- configurable pipeline stages or a workflow DAG;
- runtime wildcard artifact or publish sources;
- automatic hashing of private external-tool configuration, prompts, modules, models, or executables; or
- immediate removal of the isolated legacy party/player compatibility path.
Named reusable parameter sets beyond the canonical party roster may be considered later if a second non-party use case demonstrates a need. Additional party-member types, campaign-domain fields, or family iteration sources should be added only through a versioned schema decision rather than an untyped extension map.
Target End State
The feature is complete when:
- operators can organize a pipeline as a root file plus explicit additive imports with deterministic, source-aware conflict errors;
- one production-default or explicitly selected testing profile applies a deliberate override without duplicating stable configuration;
- every command uses the same resolved profile and reports its identity;
- a campaign owns one strict, versioned party roster containing stable character IDs, players, character names, optional alias lists, and one or more classes;
- Narratio derives the existing players input from that roster and supplies the canonical campaign context to Notarius;
- multiple Scriptorium artifact families expand once per campaign character into ordinary concrete artifacts, dependencies, variables, output paths, and optional publish rules;
- adding, changing, or removing a character produces deterministic and manifest-correct artifact reconciliation;
- profile model changes cannot silently reuse semantically incompatible stage outputs and do not unnecessarily invalidate unaffected transcript work;
- operators can validate, display, trace, and compare effective configuration without executing a session;
- existing monolithic and concrete-artifact configurations remain compatible; and
- current user, operator, integration, architecture, internal, and example documentation accurately owns the implemented contracts.