Document the v2 operator migration

This commit is contained in:
2026-09-07 04:55:13 +00:00
parent 2b97ef3722
commit a4f37ff427
19 changed files with 263 additions and 74 deletions

View File

@@ -44,8 +44,9 @@ the durable copied session input.
### Read-only effective pipeline inspection ### Read-only effective pipeline inspection
`narratio config validate`, `narratio config show`, and `narratio config `narratio config validate`, `narratio config show`, `narratio config sources`,
sources` use the same `--config`, `--campaign`, `--campaign-file`, and and `narratio config artifacts` use the same `--config`, `--campaign`,
`--campaign-file`, and
`--profile` selection rules as pipeline commands, but do not select, discover, `--profile` selection rules as pipeline commands, but do not select, discover,
or load a session. They resolve configured typed credential references as part or load a session. They resolve configured typed credential references as part
of the ordinary ConfigKit load but expose only their environment-variable names; of the ordinary ConfigKit load but expose only their environment-variable names;
@@ -61,10 +62,12 @@ then parses the campaign-owned party and expands concrete artifacts and any
family publish rules before validation. `config validate` prints the resulting family publish rules before validation. `config validate` prints the resulting
root, profile, and effective digest. `config show` emits the normalized root, profile, and effective digest. `config show` emits the normalized
effective pipeline YAML, with defaults and concrete expansion included but effective pipeline YAML, with defaults and concrete expansion included but
ConfigKit directives and family declarations omitted. `config sources` prints a stable ConfigKit directives and family declarations omitted. This review projection is
not promised to be a reloadable authoring document. `config sources` prints a stable
source projection instead of effective values: root/import/profile/default source projection instead of effective values: root/import/profile/default
ownership plus campaign/party and generated-family records. Canonical derived ownership plus campaign/party and generated-family records. Canonical derived
players trace to the party. The [CLI reference](cli.md#config-validate-config-show-config-sources-and-config-diff) players trace to the party. `config artifacts` lists the concrete artifact map,
effective dependencies, and defining sources. The [CLI reference](cli.md#config-validate-config-show-config-sources-config-artifacts-and-config-diff)
owns command syntax and output conventions. owns command syntax and output conventions.
`config diff` compares normalized field values rather than YAML text or source `config diff` compares normalized field values rather than YAML text or source
@@ -78,14 +81,14 @@ difference.
### Migrating to the maintained bundle ### Migrating to the maintained bundle
Use the [production/testing bundle](../examples/production-testing/pipeline.yml) Use the [production/testing bundle](../examples/production-testing/pipeline.yml)
as the complete copyable migration reference. Split stable pipeline settings as the complete copyable migration reference. Keep production settings in the
into explicit additive imports and place production/testing differences in the base, split them into explicit additive imports, and place testing differences
fixed `profiles/production.yml` and `profiles/testing.yml` overlays. Convert in the fixed `profiles/testing.yml` overlay. Put ordinary artifacts and families
campaign in the recommended one-definition files shown by that bundle. Convert campaign
rosters to [canonical party input](integrations/party.md), then express rosters to [canonical party input](integrations/party.md), then express character
character work as families. Inspect the result work as families. Inspect the result with `config validate`, `config show`,
with `config validate`, `config show`, and `config sources`; use `config diff` `config sources`, and `config artifacts`; use `config diff default testing` to
to review profiles before running a session. Unversioned party documents are review the overlay before running a session. Unversioned party documents are
rejected. rejected.
### Identity segments ### Identity segments
@@ -116,6 +119,11 @@ remote state with an unsafe legacy identity must be migrated before use.
- Session files must be concrete; unresolved `{{ ... }}` placeholders fail load. - Session files must be concrete; unresolved `{{ ... }}` placeholders fail load.
- Session and party inputs inspected before typed loading are limited to 8 MiB. - Session and party inputs inspected before typed loading are limited to 8 MiB.
- Pipeline defaults are applied before validation. - Pipeline defaults are applied before validation.
- Defaults apply only to absent fields. Explicit empty strings, zero values,
`false`, and `null` retain ConfigKit's typed meaning and are then checked by
Narratio domain validation; they are not silently replaced by defaults.
Explicit null optional stage sections stay absent, while required sections
fail validation.
- Campaign and session identities must agree. - Campaign and session identities must agree.
- Required stable files (`speakers_file`, `autocorrect_file`, `glossary_file`, - Required stable files (`speakers_file`, `autocorrect_file`, `glossary_file`,
`party_file`) are campaign-owned; the first three may be overridden by the `party_file`) are campaign-owned; the first three may be overridden by the

View File

@@ -58,10 +58,12 @@ Configured definitions are always registered. Without an explicit selection,
the effective analyze set contains enabled definitions. With `--artifacts`, the the effective analyze set contains enabled definitions. With `--artifacts`, the
exact named configured definitions become the effective set for that invocation, exact named configured definitions become the effective set for that invocation,
regardless of their `enabled` value. The effective-set resolver itself does not regardless of their `enabled` value. The effective-set resolver itself does not
expand dependencies; the analyze work planner closes those targets over their expand dependencies. Configuration has already materialized the sorted union of
configured prerequisite graph. Availability is separate from executability. explicit ordering edges and configured-artifact input edges, and the analyze
Configuration may normalize a family selection into its concrete generated work planner closes targets over that one concrete prerequisite graph.
members before this resolver runs. The effective set retains optional family Availability is separate from executability. Configuration may normalize a
family selection into its concrete generated members before this resolver runs.
The effective set retains optional family
and character origin metadata, but its keys, catalog sources, and runtime and character origin metadata, but its keys, catalog sources, and runtime
lookups remain concrete configured-artifact identities. lookups remain concrete configured-artifact identities.
Family publish policies are likewise expanded into ordinary configured-source Family publish policies are likewise expanded into ordinary configured-source

View File

@@ -53,13 +53,16 @@ the same typed loader and retain the existing post-load identity expectations,
stable-input precedence, and source-relative path semantics. stable-input precedence, and source-relative path semantics.
The application also has a separate read-only inspection resolver for `config The application also has a separate read-only inspection resolver for `config
validate`, `config show`, and `config sources`. It uses the same production root/profile and validate`, `config show`, `config sources`, and `config artifacts`. It uses the same production root/profile and
campaign selection functions, but never routes through session discovery, campaign selection functions, but never routes through session discovery,
remote-session download, secret loading, adapter composition, workspace remote-session download, adapter composition, workspace
initialization, manifest access, or cleanup. A pipeline with retained artifact initialization, manifest access, or cleanup. A pipeline with retained artifact
family declarations must resolve its selected campaign before ordinary pipeline family declarations must resolve its selected campaign before ordinary pipeline
validation, which expands its canonical-party members and generated publish validation, which expands its canonical-party members and generated publish
rules. A pipeline without those declarations may be validated by itself. rules. A pipeline without those declarations may be validated by itself.
Secret-directory bootstrap remains invocation-scoped and happens before the
ordinary ConfigKit load because explicit typed references must resolve even for
inspection.
`MarshalEffectivePipeline` is the configuration-owned projection for `config `MarshalEffectivePipeline` is the configuration-owned projection for `config
show`. It serializes one explicit typed safe projection with the existing YAML show`. It serializes one explicit typed safe projection with the existing YAML
@@ -77,6 +80,12 @@ Campaign records identify campaign-owned fields and party inputs; canonical
derived players point to the party source. The application command only joins these sorted derived players point to the party source. The application command only joins these sorted
records with selection metadata and never reparses configuration files. records with selection metadata and never reparses configuration files.
`EffectiveArtifactCatalog` projects the same finalized concrete artifact map for
`config artifacts`. It derives source identity from each map key, carries sorted
effective dependencies, aggregates ConfigKit definition contributors, and adds
family/character/party provenance for generated members. It does not inspect
files named after artifacts or infer another runtime graph.
`config diff` uses a thin paired loader that performs two independent ConfigKit `config diff` uses a thin paired loader that performs two independent ConfigKit
loads. When campaign loads. When campaign
resolution is needed, the command loads one selected campaign and party and resolution is needed, the command loads one selected campaign and party and

View File

@@ -76,12 +76,15 @@ value from the last value persisted in the session manifest, which helps
diagnose profile switches without changing resume authority. diagnose profile switches without changing resume authority.
Before switching an operational profile, compare its effective meaning with the Before switching an operational profile, compare its effective meaning with the
current selection through `narratio config diff <left-profile> <right-profile>`. production base through `narratio config diff default <profile>`.
The command is read-only and succeeds whether it finds differences or not. Its The command is read-only and succeeds whether it finds differences or not. Its
sorted records describe defaulted, expanded concrete configuration—not source sorted records describe defaulted, expanded concrete configuration—not source
file layout—so it can be used to review model, artifact, and publish changes file layout—so it can be used to review model, artifact, and publish changes
without creating a session or run. Select the same campaign explicitly when without creating a session or run. Use `narratio config artifacts` to review the
profiles could resolve different campaign paths; see the [CLI reference](cli.md#config-validate-config-show-config-sources-and-config-diff) for syntax and record format. concrete artifact identities, definitions, outputs, and dependencies. Select the
same campaign explicitly when profiles could resolve different campaign paths;
see the [CLI reference](cli.md#config-validate-config-show-config-sources-config-artifacts-and-config-diff)
for syntax and record format.
## Stage Execution and Continuation Behavior ## Stage Execution and Continuation Behavior

View File

@@ -22,5 +22,7 @@ See the [release procedure](../release.md) for creating a candidate and tag.
When asynchronous publication succeeds, the corresponding Gitea release is the When asynchronous publication succeeds, the corresponding Gitea release is the
canonical source for downloadable binaries and checksums. canonical source for downloadable binaries and checksums.
- [v2.0.0](v2.0.0.md)
- [v1.7.0](v1.7.0.md)
- [v1.6.0](v1.6.0.md) - [v1.6.0](v1.6.0.md)
- [v1.5.0](v1.5.0.md) - [v1.5.0](v1.5.0.md)

92
docs/releases/v2.0.0.md Normal file
View File

@@ -0,0 +1,92 @@
# Narratio v2.0.0
Narratio v2.0.0 replaces its configuration infrastructure with ConfigKit,
simplifies the supported release platforms, and makes artifact definitions and
their effective dependency graph directly inspectable.
## Summary
Pipeline roots now use ConfigKit v0.1.1 for strict YAML loading, explicit
imports, fixed-path named profiles, provenance, defaults, and typed credential
references. The recommended production layout keeps production values in the
base, defines each ordinary artifact and artifact family in its own imported
file, and keeps testing changes in `profiles/testing.yml`.
Configured-artifact inputs now create their dependency edges automatically.
`narratio config artifacts` reports the finalized concrete catalog, including
generated party-family members, without loading a session or initializing
runtime adapters.
## Compatibility
This is a breaking configuration and platform release. There is no runtime
fallback for the retired Narratio composition/profile catalog, `pipeline.secrets`
or `secrets.env_dir`, credential `_env` fields, or the previous YAML
presence/default behavior. Defaults apply only to absent values; explicit empty,
zero, false, and null values retain their typed meaning and may fail Narratio
domain validation.
The maintained binaries are Linux amd64, Linux arm64, and macOS arm64. Windows
and macOS amd64 builds are no longer produced or supported. Existing v1 release
notes remain historical descriptions of their releases.
Inspection commands still avoid session, manifest, workspace, adapter, and
remote-service activity, but they are not credential-free loaders: every
explicit typed credential reference must resolve. Pipelines containing artifact
families also require a campaign so the canonical party can be expanded.
`config show` is a secret-safe review projection, not a promised reloadable
authoring file.
## Upgrade
Migrate each operational bundle before running v2:
1. Replace old composition syntax with root-owned
[`configkit.imports`](../config.md#pipeline-imports-and-profiles). Imports are
additive base sources; they do not override one another.
2. Fold the old production overlay into the base pipeline. Move testing changes
to `profiles/testing.yml`; compare the two independent resolutions with
`narratio config diff default testing`.
3. Follow the maintained
[`examples/production-testing`](../../examples/production-testing/pipeline.yml)
layout: one explicit artifact entry per `artifacts/<key>.yml` and one family
entry per `artifact-families/<key>.yml`. The map key, not the filename, is the
stable artifact identity.
4. Move credential files to an untracked `secrets/` directory beside
`pipeline.yml`, or pass `--secrets-dir`. Rename S3 fields to `access_key_id`
and `secret_access_key`, and rename the Audita field to `llm_api_key`.
5. For static S3 credentials, configure both S3 references (commonly
`OBJECT_STORAGE_KEY_ID` and `OBJECT_STORAGE_KEY`) and make them resolvable.
To use instance roles, workload identity, shared SDK files, or another AWS SDK
discovery provider, omit both references. A partial pair is invalid.
6. Remove a `depends_on` entry when it only duplicates a
`narratio.artifact.<key>` input. Keep explicit entries that express ordering
without supplying the upstream output as an input.
7. Review ConfigKit's stricter explicit-value/null semantics and correct values
that previously acquired defaults after being explicitly empty. Do not use
`config show` output as a source migration mechanism.
8. Run `config validate`, `config show`, `config sources`, and `config artifacts`
with the same profile, campaign, and secrets selection intended for runtime.
Resolve the campaign-owned party whenever families are present.
9. Update deployment and download automation to one of the three maintained
platform assets before removing the v1 binary.
The [configuration reference](../config.md), [CLI reference](../cli.md), and
[maintained examples](../../examples/README.md) are the canonical detailed
guides.
## Changes
- Adopted ConfigKit v0.1.1 across pipeline, campaign, session, and party loading
while keeping Narratio domain validation and post-campaign expansion explicit.
- Added ConfigKit imports and fixed `profiles/<name>.yml` pipeline overlays,
unified safe effective show/diff/digest projection, and source provenance.
- Added invocation-scoped ConfigKit secrets-directory bootstrap and typed S3 and
Audita credential references without exposing resolved values.
- Inferred, normalized, and validated configured-artifact input dependencies as
part of the one concrete graph consumed by analyze planning and resume logic.
- Added `config artifacts` with stable concrete identities, definition sources,
effective dependencies, and generated family/party provenance.
- Added the maintained production-base/testing-overlay artifact authoring layout.
- Reduced release assets and active platform support to Linux amd64/arm64 and
macOS arm64.

View File

@@ -426,7 +426,13 @@ Document the command in its canonical CLI owner and link to config layout exampl
## Stage 10 — Finish the operator layout and v2 migration documentation ## Stage 10 — Finish the operator layout and v2 migration documentation
Status: Pending Status: Complete
Completion: Reworked the maintained bundle around an unprofiled production base,
one-entry artifact and family imports, and a testing overlay; updated the example
harness and inspection workflows; consolidated the v2 configuration guidance; and
added the v2.0.0 operator migration checklist. Focused documentation/example tests,
the offline inspection walkthrough, and the full normal/race suites pass.
Read: roadmap “Artifact Definition Ergonomics” and “Compatibility and Migration”; Read: roadmap “Artifact Definition Ergonomics” and “Compatibility and Migration”;
`examples/README.md`, existing example bundles and their loader test, `examples/README.md`, existing example bundles and their loader test,

View File

@@ -11,18 +11,26 @@ in the [configuration reference](../docs/config.md).
WhisperX URL. WhisperX URL.
- [Production-shaped pipeline](pipeline.production.yml): S3 storage, publish, - [Production-shaped pipeline](pipeline.production.yml): S3 storage, publish,
external tools, and configured Scriptorium artifacts. external tools, and configured Scriptorium artifacts.
- [Production/testing split bundle](production-testing/pipeline.yml): explicit - [Production-base/testing-overlay bundle](production-testing/pipeline.yml):
`conf.d` imports, a production default, and selectable production/testing explicit `conf.d` imports, one ordinary artifact per `artifacts/<key>.yml`,
overlays. It also demonstrates canonical-party artifact families and a one family per `artifact-families/<key>.yml`, and the fixed
testing-only disabled artifact. Validate it with: `profiles/testing.yml` overlay. Production values live in the base. The
bundle demonstrates an input-only artifact dependency, two character families
sharing the campaign party, and a testing-only disabled artifact. Inspect it
offline with:
```sh ```sh
narratio config validate --config examples/production-testing/pipeline.yml --campaign-file examples/campaigns/sample-campaign/campaign.yml narratio config validate --config examples/production-testing/pipeline.yml --campaign-file examples/campaigns/sample-campaign/campaign.yml
narratio config diff production testing --config examples/production-testing/pipeline.yml --campaign-file examples/campaigns/sample-campaign/campaign.yml narratio config show --config examples/production-testing/pipeline.yml --campaign-file examples/campaigns/sample-campaign/campaign.yml --profile testing
narratio config sources --config examples/production-testing/pipeline.yml --campaign-file examples/campaigns/sample-campaign/campaign.yml --profile testing
narratio config artifacts --config examples/production-testing/pipeline.yml --campaign-file examples/campaigns/sample-campaign/campaign.yml --profile testing
narratio config diff default testing --config examples/production-testing/pipeline.yml --campaign-file examples/campaigns/sample-campaign/campaign.yml
``` ```
`config show` and `config sources` accept the same selectors and remain These inspection commands load configuration and resolve explicit credential
read-only. references, but do not load a session or create runtime state. This bundle is
intentionally local-storage and credential-reference free so the walkthrough
needs no tools or cloud services.
- [Full annotated pipeline](pipeline.full.annotated.yml): every implemented - [Full annotated pipeline](pipeline.full.annotated.yml): every implemented
pipeline section with explanatory comments. pipeline section with explanatory comments.
- [Extraction subset pipeline](pipeline.extraction-subset.yml): a focused - [Extraction subset pipeline](pipeline.extraction-subset.yml): a focused
@@ -63,3 +71,9 @@ The examples contain environment-variable names but no credential values. Set
nonempty synthetic or real values before loading the production-shaped and full nonempty synthetic or real values before loading the production-shaped and full
annotated pipelines. They use fictional campaign content and reserved example annotated pipelines. They use fictional campaign content and reserved example
domains. domains.
For a deployed bundle, keep credential files out of source control in a
`secrets/` directory beside the root `pipeline.yml`, one file per referenced
environment-variable name, or select another directory with `--secrets-dir`.
Static S3 credentials are optional: omit both S3 credential references to use
the AWS SDK discovery chain.

View File

@@ -1,23 +1,11 @@
scriptorium: scriptorium:
binary: scriptorium
config_path: ./scriptorium/config.yml
artifact_families: artifact_families:
character_meta:
enabled: true
for_each: party.characters
prompt_id: dnd.character_meta
output_path_pattern: artifacts/characters/{character_id}/meta.md
member_vars:
character_id: character_id
character_name: character.name
player_name: player.name
class_summary: character.class_summary
character_items: character_items:
enabled: true enabled: true
for_each: party.characters for_each: party.characters
prompt_id: dnd.character_items prompt_id: dnd.character_items
profile_id: production-placeholder
output_path_pattern: artifacts/characters/{character_id}/items.md output_path_pattern: artifacts/characters/{character_id}/items.md
member_dependencies: [character_meta]
inputs: inputs:
character_meta: character_meta:
source: narratio.member_artifact.character_meta source: narratio.member_artifact.character_meta

View File

@@ -0,0 +1,13 @@
scriptorium:
artifact_families:
character_meta:
enabled: true
for_each: party.characters
prompt_id: dnd.character_meta
profile_id: production-placeholder
output_path_pattern: artifacts/characters/{character_id}/meta.md
member_vars:
character_id: character_id
character_name: character.name
player_name: player.name
class_summary: character.class_summary

View File

@@ -0,0 +1,11 @@
scriptorium:
artifacts:
player_handout:
enabled: true
prompt_id: dnd.player_handout
profile_id: production-placeholder
output_path: artifacts/player-handout.md
inputs:
recap:
source: narratio.artifact.session_recap
required: true

View File

@@ -0,0 +1,11 @@
scriptorium:
artifacts:
session_recap:
enabled: true
prompt_id: dnd.session_recap
profile_id: production-placeholder
output_path: artifacts/session-recap.md
inputs:
transcript:
source: narratio.transcript.final_trimmed
required: true

View File

@@ -10,6 +10,8 @@ audita:
binary: audita binary: audita
modules: [glossary, grammar] modules: [glossary, grammar]
output_schema: audita-v1 output_schema: audita-v1
model: narratio-production-model-placeholder
validation_model: narratio-production-validator-placeholder
normalize: normalize:
output_path: transcripts/final.json output_path: transcripts/final.json

View File

@@ -1,9 +1,14 @@
# Copyable production/testing pipeline entry point. Every fragment is named # Copyable production-base/testing-overlay entry point. ConfigKit expands only
# explicitly; Narratio never scans conf.d automatically. # these explicit path and glob imports; Narratio never scans directories itself.
configkit: configkit:
imports: imports:
- path: conf.d/platform.yml - path: conf.d/platform.yml
- path: conf.d/storage.yml - path: conf.d/storage.yml
- path: conf.d/transcript.yml - path: conf.d/transcript.yml
- path: conf.d/artifacts.yml
- path: conf.d/publish.yml - path: conf.d/publish.yml
- glob: artifacts/*.yml
- glob: artifact-families/*.yml
scriptorium:
binary: scriptorium
config_path: ./scriptorium/config.yml

View File

@@ -1,10 +0,0 @@
audita:
model: narratio-production-model-placeholder
validation_model: narratio-production-validator-placeholder
scriptorium:
artifact_families:
character_meta:
profile_id: production-placeholder
character_items:
profile_id: production-placeholder

View File

@@ -4,6 +4,10 @@ audita:
scriptorium: scriptorium:
artifacts: artifacts:
session_recap:
profile_id: testing-placeholder
player_handout:
profile_id: testing-placeholder
testing_notes: testing_notes:
enabled: false enabled: false
output_path: artifacts/testing-notes.md output_path: artifacts/testing-notes.md

View File

@@ -253,11 +253,11 @@ func TestAssembledSplitBundleCommandsUseOneEffectiveFamilyConfiguration(t *testi
} }
var validated bytes.Buffer var validated bytes.Buffer
if err := ConfigValidate(context.Background(), []string{"--config", pipelinePath, "--campaign-file", campaignPath, "--profile", "production"}, &validated); err != nil { if err := ConfigValidate(context.Background(), []string{"--config", pipelinePath, "--campaign-file", campaignPath}, &validated); err != nil {
t.Fatalf("validate production profile: %v", err) t.Fatalf("validate production base: %v", err)
} }
if !strings.Contains(validated.String(), "profile=production") { if !strings.Contains(validated.String(), "profile=none") {
t.Fatalf("validate output = %q, want production profile", validated.String()) t.Fatalf("validate output = %q, want unprofiled production base", validated.String())
} }
for _, command := range []func(context.Context, []string, io.Writer) error{ConfigShow, ConfigSources} { for _, command := range []func(context.Context, []string, io.Writer) error{ConfigShow, ConfigSources} {
if err := command(context.Background(), []string{ if err := command(context.Background(), []string{
@@ -268,7 +268,7 @@ func TestAssembledSplitBundleCommandsUseOneEffectiveFamilyConfiguration(t *testi
} }
var diff bytes.Buffer var diff bytes.Buffer
if err := ConfigDiff(context.Background(), []string{ if err := ConfigDiff(context.Background(), []string{
"production", "testing", "--config", pipelinePath, "--campaign-file", campaignPath, "default", "testing", "--config", pipelinePath, "--campaign-file", campaignPath,
}, &diff); err != nil { }, &diff); err != nil {
t.Fatalf("compare profiles: %v", err) t.Fatalf("compare profiles: %v", err)
} }
@@ -330,7 +330,7 @@ func TestAssembledSplitBundleCommandsUseOneEffectiveFamilyConfiguration(t *testi
} }
func TestAssembledSplitBundleProfileSwitchRecordsProvenanceAndLimitsReuse(t *testing.T) { func TestAssembledSplitBundleProfileSwitchRecordsProvenanceAndLimitsReuse(t *testing.T) {
production := loadAssembledSplitBundleConfig(t, "production") production := loadAssembledSplitBundleConfig(t, "")
testingCfg := loadAssembledSplitBundleConfig(t, "testing") testingCfg := loadAssembledSplitBundleConfig(t, "testing")
workspace := t.TempDir() workspace := t.TempDir()
production.Pipeline.Workspace.Root = workspace production.Pipeline.Workspace.Root = workspace
@@ -365,7 +365,7 @@ func TestAssembledSplitBundleProfileSwitchRecordsProvenanceAndLimitsReuse(t *tes
if !reflect.DeepEqual(productionRuns, make([]int, len(names))) { if !reflect.DeepEqual(productionRuns, make([]int, len(names))) {
t.Fatalf("production runs = %v, want complete reuse", productionRuns) t.Fatalf("production runs = %v, want complete reuse", productionRuns)
} }
assertAssembledProvenance(t, store, productionSummary, "production", config.EffectivePipelineDigest(production.Pipeline)) assertAssembledProvenance(t, store, productionSummary, "", config.EffectivePipelineDigest(production.Pipeline))
testingRuns := make([]int, len(names)) testingRuns := make([]int, len(names))
testingStages := assembledSemanticStages(names, providers, testingRuns) testingStages := assembledSemanticStages(names, providers, testingRuns)
@@ -415,7 +415,9 @@ func assertAssembledProvenance(t *testing.T, store *manifest.LocalStore, summary
t.Fatal(err) t.Fatal(err)
} }
value := session.EffectiveConfig value := session.EffectiveConfig
if value == nil || value.SelectedProfile == nil || value.SelectedProfile.Name != profile || value.EffectiveConfigDigest != digest { profileMatches := value != nil && ((profile == "" && value.SelectedProfile == nil) ||
(profile != "" && value.SelectedProfile != nil && value.SelectedProfile.Name == profile))
if !profileMatches || value.EffectiveConfigDigest != digest {
t.Fatalf("effective configuration provenance = %#v, want profile=%q digest=%q", value, profile, digest) t.Fatalf("effective configuration provenance = %#v, want profile=%q digest=%q", value, profile, digest)
} }
} }

View File

@@ -370,12 +370,12 @@ func TestConfigCommandsInspectMaintainedSplitBundleWithoutRuntimeState(t *testin
} }
for _, command := range []func(context.Context, []string, io.Writer) error{ConfigValidate, ConfigShow, ConfigSources, ConfigArtifacts} { for _, command := range []func(context.Context, []string, io.Writer) error{ConfigValidate, ConfigShow, ConfigSources, ConfigArtifacts} {
if err := command(context.Background(), []string{"--config", pipelinePath, "--campaign-file", campaignPath, "--profile", "production"}, io.Discard); err != nil { if err := command(context.Background(), []string{"--config", pipelinePath, "--campaign-file", campaignPath}, io.Discard); err != nil {
t.Fatalf("inspection command %T error = %v", command, err) t.Fatalf("inspection command %T error = %v", command, err)
} }
} }
var diff bytes.Buffer var diff bytes.Buffer
if err := ConfigDiff(context.Background(), []string{"production", "testing", "--config", pipelinePath, "--campaign-file", campaignPath}, &diff); err != nil { if err := ConfigDiff(context.Background(), []string{"default", "testing", "--config", pipelinePath, "--campaign-file", campaignPath}, &diff); err != nil {
t.Fatalf("ConfigDiff() error = %v", err) t.Fatalf("ConfigDiff() error = %v", err)
} }
if !strings.Contains(diff.String(), "audita.model") { if !strings.Contains(diff.String(), "audita.model") {

View File

@@ -285,15 +285,19 @@ func TestExamplesLoadAndValidate(t *testing.T) {
t.Fatalf("load split bundle campaign: %v", err) t.Fatalf("load split bundle campaign: %v", err)
} }
for _, profile := range []struct { for _, profile := range []struct {
name string name string
model string selection string
model string
}{ }{
{name: "production", model: "narratio-production-model-placeholder"}, {name: "production base", model: "narratio-production-model-placeholder"},
{name: "testing", model: "narratio-testing-model-placeholder"}, {name: "testing", selection: "testing", model: "narratio-testing-model-placeholder"},
} { } {
t.Run(profile.name, func(t *testing.T) { t.Run(profile.name, func(t *testing.T) {
selected := profile.name options := PipelineLoadOptions{}
pipeline, err := LoadPipelineWithOptions(pipelinePath, PipelineLoadOptions{Profile: &selected}) if profile.selection != "" {
options.Profile = &profile.selection
}
pipeline, err := LoadPipelineWithOptions(pipelinePath, options)
if err != nil { if err != nil {
t.Fatalf("load split bundle pipeline: %v", err) t.Fatalf("load split bundle pipeline: %v", err)
} }
@@ -314,10 +318,16 @@ func TestExamplesLoadAndValidate(t *testing.T) {
if cfg.Pipeline.Audita.LLMAPIKey != nil || cfg.Pipeline.Storage.Backend != StorageBackendLocal || cfg.Pipeline.Storage.S3 != nil { if cfg.Pipeline.Audita.LLMAPIKey != nil || cfg.Pipeline.Storage.Backend != StorageBackendLocal || cfg.Pipeline.Storage.S3 != nil {
t.Fatalf("split bundle must remain offline and secret-free: audita=%#v storage=%#v", cfg.Pipeline.Audita, cfg.Pipeline.Storage) t.Fatalf("split bundle must remain offline and secret-free: audita=%#v storage=%#v", cfg.Pipeline.Audita, cfg.Pipeline.Storage)
} }
if selectedProfile, ok := SelectedPipelineProfile(cfg.Pipeline); !ok || selectedProfile.Name != profile.name { selectedProfile, selected := SelectedPipelineProfile(cfg.Pipeline)
t.Fatalf("selected profile = %#v, want %q", selectedProfile, profile.name) if profile.selection == "" && selected {
t.Fatalf("production base unexpectedly selected profile %#v", selectedProfile)
}
if profile.selection != "" && (!selected || selectedProfile.Name != profile.selection) {
t.Fatalf("selected profile = %#v, want %q", selectedProfile, profile.selection)
} }
for _, key := range []string{ for _, key := range []string{
"session_recap",
"player_handout",
"character_meta_arannis", "character_meta_arannis",
"character_meta_brenna", "character_meta_brenna",
"character_items_arannis", "character_items_arannis",
@@ -330,6 +340,12 @@ func TestExamplesLoadAndValidate(t *testing.T) {
if got := cfg.Pipeline.Scriptorium.Artifacts["character_items_arannis"].Inputs["character_meta"].Source; got != "narratio.artifact.character_meta_arannis" { if got := cfg.Pipeline.Scriptorium.Artifacts["character_items_arannis"].Inputs["character_meta"].Source; got != "narratio.artifact.character_meta_arannis" {
t.Fatalf("same-member input source = %q", got) t.Fatalf("same-member input source = %q", got)
} }
if got := cfg.Pipeline.Scriptorium.Artifacts["player_handout"].DependsOn; !reflect.DeepEqual(got, []string{"session_recap"}) {
t.Fatalf("input-only artifact dependency = %#v", got)
}
if got := cfg.Pipeline.Scriptorium.Artifacts["character_items_arannis"].DependsOn; !reflect.DeepEqual(got, []string{"character_meta_arannis"}) {
t.Fatalf("family input dependency = %#v", got)
}
if cfg.Party.Canonical == nil || len(cfg.Party.Canonical.Characters) != 2 { if cfg.Party.Canonical == nil || len(cfg.Party.Canonical.Characters) != 2 {
t.Fatalf("canonical party = %#v", cfg.Party) t.Fatalf("canonical party = %#v", cfg.Party)
} }
@@ -340,7 +356,18 @@ func TestExamplesLoadAndValidate(t *testing.T) {
if len(cfg.Pipeline.Publish.Outputs) != 3 { if len(cfg.Pipeline.Publish.Outputs) != 3 {
t.Fatalf("publish outputs = %#v, want transcript plus two family outputs", cfg.Pipeline.Publish.Outputs) t.Fatalf("publish outputs = %#v, want transcript plus two family outputs", cfg.Pipeline.Publish.Outputs)
} }
if profile.name == "testing" { catalog, err := EffectiveArtifactCatalog(cfg.Pipeline, cfg.Party)
if err != nil {
t.Fatal(err)
}
wantCatalogSize := 6
if profile.selection == "testing" {
wantCatalogSize++
}
if len(catalog) != wantCatalogSize {
t.Fatalf("effective artifact catalog has %d records, want %d", len(catalog), wantCatalogSize)
}
if profile.selection == "testing" {
artifact, exists := cfg.Pipeline.Scriptorium.Artifacts["testing_notes"] artifact, exists := cfg.Pipeline.Scriptorium.Artifacts["testing_notes"]
if !exists || artifact.Enabled { if !exists || artifact.Enabled {
t.Fatalf("testing-only disabled artifact = %#v", artifact) t.Fatalf("testing-only disabled artifact = %#v", artifact)