From a4f37ff42769123829d3e2f87ac0da95c4c805ee Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 7 Sep 2026 04:55:13 +0000 Subject: [PATCH] Document the v2 operator migration --- docs/config.md | 32 ++++--- docs/internal/artifacts.md | 10 +- docs/internal/configuration.md | 13 ++- docs/operations.md | 9 +- docs/releases/README.md | 2 + docs/releases/v2.0.0.md | 92 +++++++++++++++++++ docs/roadmap/implementation.md | 8 +- examples/README.md | 28 ++++-- .../character_items.yml} | 14 +-- .../artifact-families/character_meta.yml | 13 +++ .../artifacts/player_handout.yml | 11 +++ .../artifacts/session_recap.yml | 11 +++ .../production-testing/conf.d/transcript.yml | 2 + examples/production-testing/pipeline.yml | 11 ++- .../profiles/production.yml | 10 -- .../production-testing/profiles/testing.yml | 4 + internal/app/assembled_workflow_test.go | 18 ++-- internal/app/config_commands_test.go | 4 +- internal/config/load_validate_test.go | 45 +++++++-- 19 files changed, 263 insertions(+), 74 deletions(-) create mode 100644 docs/releases/v2.0.0.md rename examples/production-testing/{conf.d/artifacts.yml => artifact-families/character_items.yml} (56%) create mode 100644 examples/production-testing/artifact-families/character_meta.yml create mode 100644 examples/production-testing/artifacts/player_handout.yml create mode 100644 examples/production-testing/artifacts/session_recap.yml delete mode 100644 examples/production-testing/profiles/production.yml diff --git a/docs/config.md b/docs/config.md index f7c767e..715af0f 100644 --- a/docs/config.md +++ b/docs/config.md @@ -44,8 +44,9 @@ the durable copied session input. ### Read-only effective pipeline inspection -`narratio config validate`, `narratio config show`, and `narratio config -sources` use the same `--config`, `--campaign`, `--campaign-file`, and +`narratio config validate`, `narratio config show`, `narratio config sources`, +and `narratio config artifacts` use the same `--config`, `--campaign`, +`--campaign-file`, and `--profile` selection rules as pipeline commands, but do not select, discover, or load a session. They resolve configured typed credential references as part 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 root, profile, and effective digest. `config show` emits the normalized 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 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. `config diff` compares normalized field values rather than YAML text or source @@ -78,14 +81,14 @@ difference. ### Migrating to the maintained bundle Use the [production/testing bundle](../examples/production-testing/pipeline.yml) -as the complete copyable migration reference. Split stable pipeline settings -into explicit additive imports and place production/testing differences in the -fixed `profiles/production.yml` and `profiles/testing.yml` overlays. Convert -campaign -rosters to [canonical party input](integrations/party.md), then express -character work as families. Inspect the result -with `config validate`, `config show`, and `config sources`; use `config diff` -to review profiles before running a session. Unversioned party documents are +as the complete copyable migration reference. Keep production settings in the +base, split them into explicit additive imports, and place testing differences +in the fixed `profiles/testing.yml` overlay. Put ordinary artifacts and families +in the recommended one-definition files shown by that bundle. Convert campaign +rosters to [canonical party input](integrations/party.md), then express character +work as families. Inspect the result with `config validate`, `config show`, +`config sources`, and `config artifacts`; use `config diff default testing` to +review the overlay before running a session. Unversioned party documents are rejected. ### 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 and party inputs inspected before typed loading are limited to 8 MiB. - 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. - Required stable files (`speakers_file`, `autocorrect_file`, `glossary_file`, `party_file`) are campaign-owned; the first three may be overridden by the diff --git a/docs/internal/artifacts.md b/docs/internal/artifacts.md index 14d82de..687b986 100644 --- a/docs/internal/artifacts.md +++ b/docs/internal/artifacts.md @@ -58,10 +58,12 @@ Configured definitions are always registered. Without an explicit selection, the effective analyze set contains enabled definitions. With `--artifacts`, the exact named configured definitions become the effective set for that invocation, regardless of their `enabled` value. The effective-set resolver itself does not -expand dependencies; the analyze work planner closes those targets over their -configured prerequisite graph. 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 +expand dependencies. Configuration has already materialized the sorted union of +explicit ordering edges and configured-artifact input edges, and the analyze +work planner closes targets over that one concrete prerequisite graph. +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 lookups remain concrete configured-artifact identities. Family publish policies are likewise expanded into ordinary configured-source diff --git a/docs/internal/configuration.md b/docs/internal/configuration.md index fcafd8c..98ae142 100644 --- a/docs/internal/configuration.md +++ b/docs/internal/configuration.md @@ -53,13 +53,16 @@ the same typed loader and retain the existing post-load identity expectations, stable-input precedence, and source-relative path semantics. 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, -remote-session download, secret loading, adapter composition, workspace +remote-session download, adapter composition, workspace initialization, manifest access, or cleanup. A pipeline with retained artifact family declarations must resolve its selected campaign before ordinary pipeline validation, which expands its canonical-party members and generated publish 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 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 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 loads. When campaign resolution is needed, the command loads one selected campaign and party and diff --git a/docs/operations.md b/docs/operations.md index 47526eb..51742b4 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -76,12 +76,15 @@ value from the last value persisted in the session manifest, which helps diagnose profile switches without changing resume authority. Before switching an operational profile, compare its effective meaning with the -current selection through `narratio config diff `. +production base through `narratio config diff default `. The command is read-only and succeeds whether it finds differences or not. Its sorted records describe defaulted, expanded concrete configuration—not source 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 -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. +without creating a session or run. Use `narratio config artifacts` to review the +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 diff --git a/docs/releases/README.md b/docs/releases/README.md index f2124ec..f0ea822 100644 --- a/docs/releases/README.md +++ b/docs/releases/README.md @@ -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 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.5.0](v1.5.0.md) diff --git a/docs/releases/v2.0.0.md b/docs/releases/v2.0.0.md new file mode 100644 index 0000000..c08e7e2 --- /dev/null +++ b/docs/releases/v2.0.0.md @@ -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/.yml` and one family + entry per `artifact-families/.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.` 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/.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. diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 30784ba..b73adce 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -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 -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”; `examples/README.md`, existing example bundles and their loader test, diff --git a/examples/README.md b/examples/README.md index 8194888..cea4d70 100644 --- a/examples/README.md +++ b/examples/README.md @@ -11,18 +11,26 @@ in the [configuration reference](../docs/config.md). WhisperX URL. - [Production-shaped pipeline](pipeline.production.yml): S3 storage, publish, external tools, and configured Scriptorium artifacts. -- [Production/testing split bundle](production-testing/pipeline.yml): explicit - `conf.d` imports, a production default, and selectable production/testing - overlays. It also demonstrates canonical-party artifact families and a - testing-only disabled artifact. Validate it with: +- [Production-base/testing-overlay bundle](production-testing/pipeline.yml): + explicit `conf.d` imports, one ordinary artifact per `artifacts/.yml`, + one family per `artifact-families/.yml`, and the fixed + `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 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 - read-only. + These inspection commands load configuration and resolve explicit credential + 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 pipeline section with explanatory comments. - [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 annotated pipelines. They use fictional campaign content and reserved example 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. diff --git a/examples/production-testing/conf.d/artifacts.yml b/examples/production-testing/artifact-families/character_items.yml similarity index 56% rename from examples/production-testing/conf.d/artifacts.yml rename to examples/production-testing/artifact-families/character_items.yml index d5984c9..ff17a00 100644 --- a/examples/production-testing/conf.d/artifacts.yml +++ b/examples/production-testing/artifact-families/character_items.yml @@ -1,23 +1,11 @@ scriptorium: - binary: scriptorium - config_path: ./scriptorium/config.yml 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: enabled: true for_each: party.characters prompt_id: dnd.character_items + profile_id: production-placeholder output_path_pattern: artifacts/characters/{character_id}/items.md - member_dependencies: [character_meta] inputs: character_meta: source: narratio.member_artifact.character_meta diff --git a/examples/production-testing/artifact-families/character_meta.yml b/examples/production-testing/artifact-families/character_meta.yml new file mode 100644 index 0000000..8f2c825 --- /dev/null +++ b/examples/production-testing/artifact-families/character_meta.yml @@ -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 diff --git a/examples/production-testing/artifacts/player_handout.yml b/examples/production-testing/artifacts/player_handout.yml new file mode 100644 index 0000000..857cbfc --- /dev/null +++ b/examples/production-testing/artifacts/player_handout.yml @@ -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 diff --git a/examples/production-testing/artifacts/session_recap.yml b/examples/production-testing/artifacts/session_recap.yml new file mode 100644 index 0000000..e0dba10 --- /dev/null +++ b/examples/production-testing/artifacts/session_recap.yml @@ -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 diff --git a/examples/production-testing/conf.d/transcript.yml b/examples/production-testing/conf.d/transcript.yml index 52b8bf4..2d91c9d 100644 --- a/examples/production-testing/conf.d/transcript.yml +++ b/examples/production-testing/conf.d/transcript.yml @@ -10,6 +10,8 @@ audita: binary: audita modules: [glossary, grammar] output_schema: audita-v1 + model: narratio-production-model-placeholder + validation_model: narratio-production-validator-placeholder normalize: output_path: transcripts/final.json diff --git a/examples/production-testing/pipeline.yml b/examples/production-testing/pipeline.yml index 9e966f9..a70c5f2 100644 --- a/examples/production-testing/pipeline.yml +++ b/examples/production-testing/pipeline.yml @@ -1,9 +1,14 @@ -# Copyable production/testing pipeline entry point. Every fragment is named -# explicitly; Narratio never scans conf.d automatically. +# Copyable production-base/testing-overlay entry point. ConfigKit expands only +# these explicit path and glob imports; Narratio never scans directories itself. configkit: imports: - path: conf.d/platform.yml - path: conf.d/storage.yml - path: conf.d/transcript.yml - - path: conf.d/artifacts.yml - path: conf.d/publish.yml + - glob: artifacts/*.yml + - glob: artifact-families/*.yml + +scriptorium: + binary: scriptorium + config_path: ./scriptorium/config.yml diff --git a/examples/production-testing/profiles/production.yml b/examples/production-testing/profiles/production.yml deleted file mode 100644 index 4219373..0000000 --- a/examples/production-testing/profiles/production.yml +++ /dev/null @@ -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 diff --git a/examples/production-testing/profiles/testing.yml b/examples/production-testing/profiles/testing.yml index ff285fc..41a4950 100644 --- a/examples/production-testing/profiles/testing.yml +++ b/examples/production-testing/profiles/testing.yml @@ -4,6 +4,10 @@ audita: scriptorium: artifacts: + session_recap: + profile_id: testing-placeholder + player_handout: + profile_id: testing-placeholder testing_notes: enabled: false output_path: artifacts/testing-notes.md diff --git a/internal/app/assembled_workflow_test.go b/internal/app/assembled_workflow_test.go index 7e97653..629ac63 100644 --- a/internal/app/assembled_workflow_test.go +++ b/internal/app/assembled_workflow_test.go @@ -253,11 +253,11 @@ func TestAssembledSplitBundleCommandsUseOneEffectiveFamilyConfiguration(t *testi } var validated bytes.Buffer - if err := ConfigValidate(context.Background(), []string{"--config", pipelinePath, "--campaign-file", campaignPath, "--profile", "production"}, &validated); err != nil { - t.Fatalf("validate production profile: %v", err) + if err := ConfigValidate(context.Background(), []string{"--config", pipelinePath, "--campaign-file", campaignPath}, &validated); err != nil { + t.Fatalf("validate production base: %v", err) } - if !strings.Contains(validated.String(), "profile=production") { - t.Fatalf("validate output = %q, want production profile", validated.String()) + if !strings.Contains(validated.String(), "profile=none") { + t.Fatalf("validate output = %q, want unprofiled production base", validated.String()) } for _, command := range []func(context.Context, []string, io.Writer) error{ConfigShow, ConfigSources} { if err := command(context.Background(), []string{ @@ -268,7 +268,7 @@ func TestAssembledSplitBundleCommandsUseOneEffectiveFamilyConfiguration(t *testi } var diff bytes.Buffer if err := ConfigDiff(context.Background(), []string{ - "production", "testing", "--config", pipelinePath, "--campaign-file", campaignPath, + "default", "testing", "--config", pipelinePath, "--campaign-file", campaignPath, }, &diff); err != nil { t.Fatalf("compare profiles: %v", err) } @@ -330,7 +330,7 @@ func TestAssembledSplitBundleCommandsUseOneEffectiveFamilyConfiguration(t *testi } func TestAssembledSplitBundleProfileSwitchRecordsProvenanceAndLimitsReuse(t *testing.T) { - production := loadAssembledSplitBundleConfig(t, "production") + production := loadAssembledSplitBundleConfig(t, "") testingCfg := loadAssembledSplitBundleConfig(t, "testing") workspace := t.TempDir() production.Pipeline.Workspace.Root = workspace @@ -365,7 +365,7 @@ func TestAssembledSplitBundleProfileSwitchRecordsProvenanceAndLimitsReuse(t *tes if !reflect.DeepEqual(productionRuns, make([]int, len(names))) { 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)) testingStages := assembledSemanticStages(names, providers, testingRuns) @@ -415,7 +415,9 @@ func assertAssembledProvenance(t *testing.T, store *manifest.LocalStore, summary t.Fatal(err) } 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) } } diff --git a/internal/app/config_commands_test.go b/internal/app/config_commands_test.go index 1e29c28..4cf1aaa 100644 --- a/internal/app/config_commands_test.go +++ b/internal/app/config_commands_test.go @@ -370,12 +370,12 @@ func TestConfigCommandsInspectMaintainedSplitBundleWithoutRuntimeState(t *testin } 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) } } 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) } if !strings.Contains(diff.String(), "audita.model") { diff --git a/internal/config/load_validate_test.go b/internal/config/load_validate_test.go index cf73673..2c128a8 100644 --- a/internal/config/load_validate_test.go +++ b/internal/config/load_validate_test.go @@ -285,15 +285,19 @@ func TestExamplesLoadAndValidate(t *testing.T) { t.Fatalf("load split bundle campaign: %v", err) } for _, profile := range []struct { - name string - model string + name string + selection string + model string }{ - {name: "production", model: "narratio-production-model-placeholder"}, - {name: "testing", model: "narratio-testing-model-placeholder"}, + {name: "production base", model: "narratio-production-model-placeholder"}, + {name: "testing", selection: "testing", model: "narratio-testing-model-placeholder"}, } { t.Run(profile.name, func(t *testing.T) { - selected := profile.name - pipeline, err := LoadPipelineWithOptions(pipelinePath, PipelineLoadOptions{Profile: &selected}) + options := PipelineLoadOptions{} + if profile.selection != "" { + options.Profile = &profile.selection + } + pipeline, err := LoadPipelineWithOptions(pipelinePath, options) if err != nil { 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 { 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 { - t.Fatalf("selected profile = %#v, want %q", selectedProfile, profile.name) + selectedProfile, selected := SelectedPipelineProfile(cfg.Pipeline) + 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{ + "session_recap", + "player_handout", "character_meta_arannis", "character_meta_brenna", "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" { 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 { t.Fatalf("canonical party = %#v", cfg.Party) } @@ -340,7 +356,18 @@ func TestExamplesLoadAndValidate(t *testing.T) { if len(cfg.Pipeline.Publish.Outputs) != 3 { 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"] if !exists || artifact.Enabled { t.Fatalf("testing-only disabled artifact = %#v", artifact)