Files
narratio/docs/internal/configuration.md

7.8 KiB

Configuration Internals

User-visible fields, defaults, and selection behavior belong in the Configuration Reference. This document describes the internal pipeline-loading boundary implemented by internal/config.

Pipeline Loading

LoadPipeline assembles and validates a pipeline in this order:

  1. Parse the root YAML into a presence-aware composition tree. The tree retains source names, full field paths, node kinds, declaration order, and explicit zero, false, empty-map, and empty-list values.
  2. Remove the root-only composition envelope and validate its explicit imports, default_profile, and named profiles declarations. A load option retains the difference between omitted and explicitly empty profile selection.
  3. Open each import relative to the root pipeline directory through the confined regular-file boundary. Imports must use a .yml or .yaml extension and cannot traverse, use symlinks, repeat a file, import the root, or contain another composition envelope.
  4. Resolve and structurally parse every declared profile overlay through the same confined regular-file boundary. Missing or malformed unselected overlays fail the load. Overlays cannot contain a composition envelope.
  5. Additively merge the root body and imports. Distinct map leaves compose; repeated scalar or list paths and node-kind disagreements are conflicts.
  6. Select exactly one declared profile from an explicit option or the default, then recursively merge its overlay. Overlay leaves replace base leaves, lists are atomic replacements, and null or kind changes fail.
  7. Emit deterministic canonical YAML and strictly decode it into PipelineConfig.
  8. Apply pipeline defaults once, resolve ordinary relative pipeline paths from the root pipeline file, and digest the normalized effective mapping.

This ordering preserves monolithic configuration behavior. Moving a field to an imported fragment changes its source ownership, not its path base, default, or schema semantics.

Loaded Context Resolution

LoadedPipelineCampaign carries one already composed pipeline and its selected campaign into session resolution. LoadSessionWithPipelineCampaignOptions loads a local session against that context, while ResolveLoadedPipelineCampaign also accepts an already loaded remote session or no session while a caller retrieves one. Compatibility loaders route through these functions after their initial pipeline and campaign reads.

Application commands own pipeline and campaign discovery, campaign-file versus registry selection, and the corresponding mutual-exclusion rules. Once they have a LoadedPipelineCampaign, local session discovery and remote-session download retain that exact pipeline object and its private provenance. Removing a temporary downloaded session file therefore cannot invalidate the resolved pipeline or campaign context.

Campaign context construction also reads and classifies the campaign-owned party source through ParseParty. A canonical party retains its raw bytes and normalized roster in runtime-only ResolvedParty provenance, while a legacy party remains opaque. Canonical resolution creates a virtual derived_from_party players input and rejects competing campaign or session players files and session party overrides. The compact legacy compatibility path resolves the effective campaign/session party and players files together.

Canonical Party Domain

ParseParty is the package-owned boundary for classifying a party source. When a top-level schema_version is present, it strictly validates the narratio.party.v1 contract into ordered character domain values. The canonical value retains a separate exact byte copy of its source so consumers can materialize the authored party document without reserializing it. Its PlayersYAML method deterministically derives the versioned players-only projection.

An unversioned source is classified by the small legacy compatibility boundary in party_legacy.go; it deliberately exposes no parsed roster information. That boundary exists solely to isolate removable compatibility behavior from the canonical parser.

Diagnostics And Runtime Metadata

Syntax, duplicate-key, composition, conflict, and schema failures include the relevant source name and full field path. Additive conflicts report every claiming source so operators can repair the split without repeatedly rediscovering additional conflicts.

The loaded pipeline retains private runtime metadata for the absolute root path, ordered imports, selected profile name and selection source, selected overlay, contributing sources, effective digest, and leaf ownership. Base leaves retain their root/import owners, replaced leaves belong to the selected overlay, and centrally supplied values use the synthetic default owner. This metadata does not participate in YAML decoding or alter the public configuration model.

The effective digest is SHA-256 over deterministic canonical YAML produced from the defaulted and path-resolved PipelineConfig. Because composition and resolution metadata are private, the digest excludes source layout, profile name, and ownership. Configuration stores environment variable names rather than resolving raw credentials, so raw secret values are neither loaded nor hashed. recomputePipelineEffectiveDigest is the single package-owned refresh point for later runtime expansion.

Test Surfaces

composition_test.go protects the presence and merge algebra independently of the public schema. pipeline_composition_test.go exercises explicit imports, confinement, conflicts, strict decoding, metadata, and root-relative path behavior through LoadPipeline. pipeline_profiles_test.go covers selection, all-overlay validation, overlay behavior, provenance, option propagation, and effective-digest stability. Application configuration-loader tests protect the single-read boundary by changing the pipeline file after its initial load and confirming local session resolution retains the original pipeline. Other configuration tests continue to protect defaults and validation after assembly. party_test.go protects the versioned party schema, domain invariants, and deterministic players projection without involving campaign or runtime wiring. party_resolution_test.go protects campaign-owned party loading, canonical input restrictions, legacy overrides, source provenance, and virtual players input selection.

Artifact Family Resolution

Pipeline loading retains scriptorium.artifact_families as a resolution-only declaration. Once campaign party resolution establishes a canonical roster, configuration expands families in sorted family-key and character-ID order into ordinary ScriptoriumArtifactConfig values. The expansion owns the narrow {character_id} output substitution, closed member-variable selectors, key and output collision checks, and the runtime-only family-origin catalog. It then removes family declarations from ScriptoriumConfig, runs ordinary Scriptorium validation, and refreshes the effective pipeline digest. Stages and adapters therefore receive only concrete artifact maps.

The catalog retains sorted family member keys plus family/character/source origins and the typed dependency/publish declarations for their later owners. member_dependencies add corresponding ordinary concrete dependencies, while the family-only narratio.member_artifact.<family> input form is rewritten to the matching ordinary configured-artifact source. The catalog records those resolved dependency and input identities with their declaring family and party member. No member-artifact source is registered as a runtime policy source. An enabled family publish declaration expands to ordinary configured-artifact publish rules before the existing publish and lock validators run. Runtime publication consequently receives no family wildcard or special matcher.