Files
narratio/docs/internal/configuration.md

134 lines
7.3 KiB
Markdown

# Configuration Internals
User-visible fields, defaults, and selection behavior belong in the
[Configuration Reference](../config.md). 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.
It does not create a new runtime artifact type, dependency edge, or publish
rule during this resolution step.