Adopt ConfigKit overlays for pipeline selection

This commit is contained in:
2026-09-25 15:24:03 +00:00
parent e27c49e946
commit 83eaa960ec
38 changed files with 394 additions and 320 deletions

View File

@@ -256,12 +256,12 @@ func TestAssembledSplitBundleCommandsUseOneEffectiveFamilyConfiguration(t *testi
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=none") {
t.Fatalf("validate output = %q, want unprofiled production base", validated.String())
if !strings.Contains(validated.String(), "overlay=none") {
t.Fatalf("validate output = %q, want production base", validated.String())
}
for _, command := range []func(context.Context, []string, io.Writer) error{ConfigShow, ConfigSources} {
if err := command(context.Background(), []string{
"--config", pipelinePath, "--campaign-file", campaignPath, "--profile", "testing",
"--config", pipelinePath, "--campaign-file", campaignPath, "--overlay", "testing",
}, io.Discard); err != nil {
t.Fatalf("testing inspection command %T: %v", command, err)
}
@@ -278,13 +278,13 @@ func TestAssembledSplitBundleCommandsUseOneEffectiveFamilyConfiguration(t *testi
common := []string{
"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath,
"--session", sessionPath, "--profile", "testing",
"--session", sessionPath, "--overlay", "testing",
}
var planned bytes.Buffer
if err := Plan(context.Background(), common, &planned); err != nil {
t.Fatalf("plan testing bundle: %v", err)
}
if !strings.Contains(planned.String(), "profile=testing") {
if !strings.Contains(planned.String(), "overlay=testing") {
t.Fatalf("plan output = %q, want testing provenance", planned.String())
}
@@ -305,7 +305,7 @@ func TestAssembledSplitBundleCommandsUseOneEffectiveFamilyConfiguration(t *testi
if !reflect.DeepEqual(capturedPlan.Names(), []string{"analyze"}) {
t.Fatalf("bounded plan = %#v, want analyze only", capturedPlan.Names())
}
if profile, ok := config.SelectedPipelineProfile(capturedCfg.Pipeline); !ok || profile.Name != "testing" {
if profile, ok := config.SelectedPipelineOverlay(capturedCfg.Pipeline); !ok || profile.Name != "testing" {
t.Fatalf("captured profile = %#v, selected=%t", profile, ok)
}
if digest := config.EffectivePipelineDigest(capturedCfg.Pipeline); digest == "" || !strings.Contains(runOut.String(), "digest="+digest) {
@@ -391,7 +391,7 @@ func loadAssembledSplitBundleConfig(t *testing.T, profile string) *config.Config
pipelinePath, campaignPath, sessionPath := assembledSplitBundlePaths()
options := config.SessionLoadOptions{}
if profile != "" {
options.Profile = &profile
options.Overlay = &profile
}
cfg, err := config.LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, options)
if err != nil {
@@ -415,8 +415,8 @@ func assertAssembledProvenance(t *testing.T, store *manifest.LocalStore, summary
t.Fatal(err)
}
value := session.EffectiveConfig
profileMatches := value != nil && ((profile == "" && value.SelectedProfile == nil) ||
(profile != "" && value.SelectedProfile != nil && value.SelectedProfile.Name == profile))
profileMatches := value != nil && ((profile == "" && value.SelectedOverlay == nil) ||
(profile != "" && value.SelectedOverlay != nil && value.SelectedOverlay.Name == profile))
if !profileMatches || value.EffectiveConfigDigest != digest {
t.Fatalf("effective configuration provenance = %#v, want profile=%q digest=%q", value, profile, digest)
}

View File

@@ -57,7 +57,7 @@ func TestBoundedRunParsingRejectsDuplicateSingletons(t *testing.T) {
{name: "through mixed", args: []string{"session", "--through", "analyze", "--through=publish"}, want: "--through may be specified only once"},
{name: "force separate", args: []string{"session", "--force", "--force"}, want: "--force may be specified only once"},
{name: "force equals", args: []string{"session", "--force=true", "--force=false"}, want: "--force may be specified only once"},
{name: "profile", args: []string{"session", "--profile", "production", "--profile=testing"}, want: "--profile may be specified only once"},
{name: "overlay", args: []string{"session", "--overlay", "production", "--overlay=testing"}, want: "--overlay may be specified only once"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
@@ -69,29 +69,29 @@ func TestBoundedRunParsingRejectsDuplicateSingletons(t *testing.T) {
}
}
func TestBoundedRunParsingRetainsExplicitProfilePresence(t *testing.T) {
withoutProfile, err := parseBoundedRunRequest("run", []string{"session"}, io.Discard)
func TestBoundedRunParsingRetainsExplicitOverlayPresence(t *testing.T) {
withoutOverlay, err := parseBoundedRunRequest("run", []string{"session"}, io.Discard)
if err != nil {
t.Fatal(err)
}
if got := withoutProfile.Config.sessionOptions().Profile; got != nil {
t.Fatalf("omitted profile = %#v, want nil", got)
if got := withoutOverlay.Config.sessionOptions().Overlay; got != nil {
t.Fatalf("omitted overlay = %#v, want nil", got)
}
withProfile, err := parseBoundedRunRequest("run", []string{"session", "--profile", "testing"}, io.Discard)
withOverlay, err := parseBoundedRunRequest("run", []string{"session", "--overlay", "testing"}, io.Discard)
if err != nil {
t.Fatal(err)
}
if got := withProfile.Config.sessionOptions().Profile; got == nil || *got != "testing" {
t.Fatalf("explicit profile = %#v, want testing", got)
if got := withOverlay.Config.sessionOptions().Overlay; got == nil || *got != "testing" {
t.Fatalf("explicit overlay = %#v, want testing", got)
}
explicitEmpty, err := parseBoundedRunRequest("run", []string{"session", "--profile", ""}, io.Discard)
explicitEmpty, err := parseBoundedRunRequest("run", []string{"session", "--overlay", ""}, io.Discard)
if err != nil {
t.Fatal(err)
}
if got := explicitEmpty.Config.sessionOptions().Profile; got == nil || *got != "" {
t.Fatalf("explicit empty profile = %#v, want non-nil empty", got)
if got := explicitEmpty.Config.sessionOptions().Overlay; got == nil || *got != "" {
t.Fatalf("explicit empty overlay = %#v, want non-nil empty", got)
}
}

View File

@@ -93,7 +93,7 @@ func cleanAllLocalWithDependencies(flags commonConfigFlags, dryRun, clearCache b
strings.TrimSpace(flags.previousSessionID) != "" {
return fmt.Errorf("clean: --all cannot be combined with --campaign, --campaign-file, --session, a session_id, or --previous-session-id")
}
_, pipelineCfg, err := loadPipelineConfigWithDependencies(flags.pipelinePath, config.PipelineLoadOptions{Profile: flags.profile.pointer()}, flags.secretsDir.pointer(), deps)
_, pipelineCfg, err := loadPipelineConfigWithDependencies(flags.pipelinePath, config.PipelineLoadOptions{Overlay: flags.overlay.pointer()}, flags.secretsDir.pointer(), deps)
if err != nil {
return fmt.Errorf("clean: %w", err)
}

View File

@@ -80,22 +80,22 @@ func TestConfigArtifactsReportsGeneratedFamilyProvenance(t *testing.T) {
}
}
func TestConfigArtifactsReportsProfiledFamilyContributors(t *testing.T) {
func TestConfigArtifactsReportsOverlayFamilyContributors(t *testing.T) {
pipelinePath, campaignPath := writeInspectionFamilyConfig(t)
profilePath := filepath.Join(filepath.Dir(pipelinePath), "profiles", "testing.yml")
mustWriteTestFile(t, profilePath, `scriptorium:
overlayPath := filepath.Join(filepath.Dir(pipelinePath), "overlays", "testing.yml")
mustWriteTestFile(t, overlayPath, `scriptorium:
artifact_families:
character_note:
prompt_id: dnd.character_note.testing
`)
args := []string{"--config", pipelinePath, "--campaign-file", campaignPath, "--profile", "testing"}
args := []string{"--config", pipelinePath, "--campaign-file", campaignPath, "--overlay", "testing"}
var catalog bytes.Buffer
if err := ConfigArtifacts(context.Background(), args, &catalog); err != nil {
t.Fatal(err)
}
records := catalogRecordsByKey(t, catalog.String())
if got := strings.Split(records["character_note_arannis"][2], ","); !reflect.DeepEqual(got, []string{pipelinePath, profilePath}) {
if got := strings.Split(records["character_note_arannis"][2], ","); !reflect.DeepEqual(got, []string{overlayPath, pipelinePath}) {
t.Fatalf("generated family definitions = %#v", got)
}
@@ -103,16 +103,16 @@ func TestConfigArtifactsReportsProfiledFamilyContributors(t *testing.T) {
if err := ConfigSources(context.Background(), args, &sources); err != nil {
t.Fatal(err)
}
want := "scriptorium.artifacts.character_note_arannis.prompt_id\tfamily\t" + profilePath
want := "scriptorium.artifacts.character_note_arannis.prompt_id\tfamily\t" + overlayPath
if !strings.Contains(sources.String(), want) {
t.Fatalf("sources output missing profiled family contributor %q:\n%s", want, sources.String())
t.Fatalf("sources output missing overlay family contributor %q:\n%s", want, sources.String())
}
}
func TestConfigArtifactsReportsBaseAndProfileContributors(t *testing.T) {
func TestConfigArtifactsReportsBaseAndOverlayContributors(t *testing.T) {
dir := t.TempDir()
pipelinePath := filepath.Join(dir, "pipeline.yml")
profilePath := filepath.Join(dir, "profiles", "testing.yml")
overlayPath := filepath.Join(dir, "overlays", "testing.yml")
mustWriteTestFile(t, pipelinePath, `workspace:
root: /srv/narratio
whisperx:
@@ -124,26 +124,26 @@ scriptorium:
enabled: false
output_path: artifacts/notes.md
`)
mustWriteTestFile(t, profilePath, `scriptorium:
mustWriteTestFile(t, overlayPath, `scriptorium:
artifacts:
notes:
enabled: true
prompt_id: notes
profile_only:
overlay_only:
enabled: false
output_path: artifacts/profile-only.md
output_path: artifacts/overlay-only.md
`)
var out bytes.Buffer
if err := ConfigArtifacts(context.Background(), []string{"--config", pipelinePath, "--profile", "testing"}, &out); err != nil {
if err := ConfigArtifacts(context.Background(), []string{"--config", pipelinePath, "--overlay", "testing"}, &out); err != nil {
t.Fatal(err)
}
records := catalogRecordsByKey(t, out.String())
if got := strings.Split(records["notes"][2], ","); !reflect.DeepEqual(got, []string{pipelinePath, profilePath}) {
if got := strings.Split(records["notes"][2], ","); !reflect.DeepEqual(got, []string{overlayPath, pipelinePath}) {
t.Fatalf("notes definitions = %#v", got)
}
if got := records["profile_only"][2]; got != profilePath {
t.Fatalf("profile-only definition = %q, want %q", got, profilePath)
if got := records["overlay_only"][2]; got != overlayPath {
t.Fatalf("overlay-only definition = %q, want %q", got, overlayPath)
}
}

View File

@@ -17,7 +17,7 @@ type inspectionFlags struct {
pipelinePath string
campaignPath string
campaignFilePath string
profile singletonStringFlag
overlay singletonStringFlag
secretsDir singletonStringFlag
}
@@ -77,8 +77,8 @@ func configValidateWithDependencies(_ context.Context, args []string, out io.Wri
if err != nil {
return fmt.Errorf("config validate: %w", err)
}
_, err = fmt.Fprintf(out, "Configuration valid: root=%s; profile=%s; digest=%s\n",
inspectionRootPath(resolved), inspectionProfile(resolved.Pipeline), config.EffectivePipelineDigest(resolved.Pipeline))
_, err = fmt.Fprintf(out, "Configuration valid: root=%s; overlay=%s; digest=%s\n",
inspectionRootPath(resolved), inspectionOverlay(resolved.Pipeline), config.EffectivePipelineDigest(resolved.Pipeline))
return err
}
@@ -214,12 +214,12 @@ func parseInspectionFlags(command string, args []string, out io.Writer) (inspect
fs.StringVar(&flags.pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)")
fs.StringVar(&flags.campaignPath, "campaign", "", "campaign ID")
fs.StringVar(&flags.campaignFilePath, "campaign-file", "", "path to campaign.yml")
flags.profile.name = "profile"
fs.Var(&flags.profile, "profile", "named pipeline profile")
flags.overlay.name = "overlay"
fs.Var(&flags.overlay, "overlay", "named pipeline overlay")
flags.secretsDir.name = "secrets-dir"
fs.Var(&flags.secretsDir, "secrets-dir", "secrets directory (optional; defaults beside pipeline.yml)")
fs.Usage = func() {
fmt.Fprintf(out, "Usage: narratio %s [--config <pipeline.yml>] [--campaign <id> | --campaign-file <campaign.yml>] [--profile <name>] [--secrets-dir <path>]\n\n", command)
fmt.Fprintf(out, "Usage: narratio %s [--config <pipeline.yml>] [--campaign <id> | --campaign-file <campaign.yml>] [--overlay <name>] [--secrets-dir <path>]\n\n", command)
fmt.Fprintln(out, "Resolves configuration without a session, workspace, manifest, adapters, or external services.")
fmt.Fprintln(out)
fmt.Fprintln(out, "Flags:")
@@ -238,7 +238,7 @@ func parseInspectionFlags(command string, args []string, out io.Writer) (inspect
}
func resolveInspectionConfigWithDependencies(flags inspectionFlags, deps commandDependencies) (*inspectionConfig, error) {
pipelinePath, pipeline, err := loadPipelineConfigWithDependencies(flags.pipelinePath, config.PipelineLoadOptions{Profile: flags.profile.pointer()}, flags.secretsDir.pointer(), deps)
pipelinePath, pipeline, err := loadPipelineConfigWithDependencies(flags.pipelinePath, config.PipelineLoadOptions{Overlay: flags.overlay.pointer()}, flags.secretsDir.pointer(), deps)
if err != nil {
return nil, err
}
@@ -315,9 +315,9 @@ func inspectionRootPath(resolved *inspectionConfig) string {
return resolved.PipelinePath
}
func inspectionProfile(pipeline *config.PipelineConfig) string {
if profile, ok := config.SelectedPipelineProfile(pipeline); ok {
return profile.Name
func inspectionOverlay(pipeline *config.PipelineConfig) string {
if overlay, ok := config.SelectedPipelineOverlay(pipeline); ok {
return overlay.Name
}
return "none"
}
@@ -325,7 +325,7 @@ func inspectionProfile(pipeline *config.PipelineConfig) string {
func writeConfigCommandUsage(out io.Writer) {
fmt.Fprintln(out, "Usage: narratio config <validate|show|sources|artifacts|diff>")
fmt.Fprintln(out)
fmt.Fprintln(out, "Use config validate to check an effective pipeline, config show to print normalized YAML, config sources to report ownership, config artifacts to list concrete artifacts, or config diff to compare two profiles.")
fmt.Fprintln(out, "Use config validate to check an effective pipeline, config show to print normalized YAML, config sources to report ownership, config artifacts to list concrete artifacts, or config diff to compare two overlays.")
}
func writeInspectionSourceHeader(out io.Writer, resolved *inspectionConfig) error {
@@ -343,11 +343,11 @@ func writeInspectionSourceHeader(out io.Writer, resolved *inspectionConfig) erro
return err
}
}
if profile, ok := config.SelectedPipelineProfile(resolved.Pipeline); ok {
if _, err := fmt.Fprintf(out, "profile: name=%s selection=%s overlay=%s\n", profile.Name, profile.Source, config.NormalizedConfigurationPath(profile.OverlayPath)); err != nil {
if overlay, ok := config.SelectedPipelineOverlay(resolved.Pipeline); ok {
if _, err := fmt.Fprintf(out, "overlay: name=%s selection=%s path=%s\n", overlay.Name, overlay.Source, config.NormalizedConfigurationPath(overlay.OverlayPath)); err != nil {
return err
}
} else if _, err := fmt.Fprintln(out, "profile: none"); err != nil {
} else if _, err := fmt.Fprintln(out, "overlay: none"); err != nil {
return err
}
if resolved.Campaign == nil {

View File

@@ -34,6 +34,7 @@ func TestConfigCommandHelpAndDispatch(t *testing.T) {
{name: "stage flag", args: []string{"config", "show", "--from", "prepare"}, want: "flag provided but not defined", code: 1},
{name: "force flag", args: []string{"config", "show", "--force"}, want: "flag provided but not defined", code: 1},
{name: "artifact flag", args: []string{"config", "show", "--artifacts", "recap"}, want: "flag provided but not defined", code: 1},
{name: "retired selector", args: []string{"config", "validate", "--profile", "testing"}, want: "flag provided but not defined", code: 1},
} {
t.Run(test.name, func(t *testing.T) {
var stdout, stderr bytes.Buffer
@@ -102,7 +103,7 @@ whisperx:
if err := ConfigValidate(context.Background(), []string{"--config", pipelinePath}, &validateOut); err != nil {
t.Fatalf("ConfigValidate() error = %v", err)
}
if !strings.Contains(validateOut.String(), "Configuration valid: root=") || !strings.Contains(validateOut.String(), "; profile=none; digest=") {
if !strings.Contains(validateOut.String(), "Configuration valid: root=") || !strings.Contains(validateOut.String(), "; overlay=none; digest=") {
t.Fatalf("validate output = %q", validateOut.String())
}
@@ -127,7 +128,7 @@ whisperx:
if strings.Contains(sourcesOut.String(), secret) {
t.Fatalf("sources output leaked a raw secret: %q", sourcesOut.String())
}
writeInspectionProfiles(t, pipelinePath, "whisperx:\n language: en\n", "whisperx:\n language: fr\n")
writeInspectionOverlays(t, pipelinePath, "whisperx:\n language: en\n", "whisperx:\n language: fr\n")
var diffOut bytes.Buffer
if err := ConfigDiff(context.Background(), []string{"production", "testing", "--config", pipelinePath}, &diffOut); err != nil {
t.Fatalf("ConfigDiff() error = %v", err)
@@ -143,10 +144,10 @@ whisperx:
}
}
func TestConfigDiffReportsSemanticProfileChanges(t *testing.T) {
func TestConfigDiffReportsSemanticOverlayChanges(t *testing.T) {
workspaceRoot := t.TempDir()
pipelinePath, _, _ := writeValidConfigFiles(t, workspaceRoot)
writeInspectionProfiles(t, pipelinePath, `workspace:
writeInspectionOverlays(t, pipelinePath, `workspace:
cleanup_after_publish: true
publish:
upload_run: true
@@ -203,7 +204,7 @@ scriptorium:
t.Fatalf("reversed ConfigDiff() error = %v", err)
}
if !strings.Contains(reversed.String(), "changed\twhisperx.language\t\"fr\"\t\"en\"\n") {
t.Fatalf("reversed diff did not independently resolve profiles:\n%s", reversed.String())
t.Fatalf("reversed diff did not independently resolve overlays:\n%s", reversed.String())
}
}
@@ -219,7 +220,7 @@ audita:
scriptorium:
artifacts: null
`)
mustWriteTestFile(t, filepath.Join(dir, "profiles", "empty.yml"), `audita:
mustWriteTestFile(t, filepath.Join(dir, "overlays", "empty.yml"), `audita:
modules: []
scriptorium:
artifacts: {}
@@ -229,7 +230,7 @@ scriptorium:
if err := ConfigShow(context.Background(), []string{"--config", pipelinePath}, &base); err != nil {
t.Fatal(err)
}
if err := ConfigShow(context.Background(), []string{"--config", pipelinePath, "--profile", "empty"}, &empty); err != nil {
if err := ConfigShow(context.Background(), []string{"--config", pipelinePath, "--overlay", "empty"}, &empty); err != nil {
t.Fatal(err)
}
for _, want := range []string{"modules: null\n", "artifacts: null\n"} {
@@ -239,7 +240,7 @@ scriptorium:
}
for _, want := range []string{"modules: []\n", "artifacts: {}\n"} {
if !strings.Contains(empty.String(), want) {
t.Fatalf("empty-profile show output missing %q:\n%s", want, empty.String())
t.Fatalf("empty-overlay show output missing %q:\n%s", want, empty.String())
}
}
@@ -259,7 +260,7 @@ scriptorium:
func TestConfigDiffComparesExpandedFamiliesAndPublishRules(t *testing.T) {
pipelinePath, campaignPath := writeInspectionFamilyConfig(t)
writeInspectionProfiles(t, pipelinePath, `scriptorium:
writeInspectionOverlays(t, pipelinePath, `scriptorium:
artifact_families:
character_note:
enabled: false
@@ -301,11 +302,11 @@ func TestConfigDiffComparesExpandedFamiliesAndPublishRules(t *testing.T) {
func TestConfigDiffReportsEqualityAndRejectsInvalidInput(t *testing.T) {
workspaceRoot := t.TempDir()
pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot)
writeInspectionProfiles(t, pipelinePath, "whisperx:\n language: en\n", "whisperx:\n language: en\n")
writeInspectionOverlays(t, pipelinePath, "whisperx:\n language: en\n", "whisperx:\n language: en\n")
var equal bytes.Buffer
if err := ConfigDiff(context.Background(), []string{"production", "testing", "--config", pipelinePath}, &equal); err != nil {
t.Fatalf("ConfigDiff() equal profiles error = %v", err)
t.Fatalf("ConfigDiff() equal overlays error = %v", err)
}
if got := equal.String(); got != "no differences\n" {
t.Fatalf("equal diff output = %q", got)
@@ -316,7 +317,7 @@ func TestConfigDiffReportsEqualityAndRejectsInvalidInput(t *testing.T) {
{"production", "--config", pipelinePath},
{"production", "production", "--config", pipelinePath},
{"unknown", "testing", "--config", pipelinePath},
{"production", "testing", "--config", pipelinePath, "--profile", "production"},
{"production", "testing", "--config", pipelinePath, "--overlay", "production"},
{"production", "testing", "--config", pipelinePath, "--config", pipelinePath},
{"production", "testing", "--config", pipelinePath, "--campaign", "sample-campaign", "--campaign-file", campaignPath},
} {
@@ -326,10 +327,10 @@ func TestConfigDiffReportsEqualityAndRejectsInvalidInput(t *testing.T) {
}
}
func TestConfigDiffBootstrapsSecretsOnceForBothProfiles(t *testing.T) {
func TestConfigDiffBootstrapsSecretsOnceForBothOverlays(t *testing.T) {
workspaceRoot := t.TempDir()
pipelinePath, _, _ := writeValidConfigFiles(t, workspaceRoot)
writeInspectionProfiles(t, pipelinePath, "whisperx:\n language: en\n", "whisperx:\n language: fr\n")
writeInspectionOverlays(t, pipelinePath, "whisperx:\n language: en\n", "whisperx:\n language: fr\n")
secretsDir := t.TempDir()
const secretName = "NARRATIO_DIFF_BOOTSTRAP_REPORT"
restoreEnvAfterTest(t, secretName)
@@ -393,27 +394,27 @@ func TestConfigInspectionOutputsNeverExposeResolvedCredentialValues(t *testing.T
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
t.Fatal(err)
}
writeInspectionProfiles(t, pipelinePath,
writeInspectionOverlays(t, pipelinePath,
"audita:\n model: production\n llm_api_key: NARRATIO_INSPECT_LLM_ONE\n",
"audita:\n model: testing\n llm_api_key: NARRATIO_INSPECT_LLM_TWO\nnotarius:\n llm_api_key: NARRATIO_INSPECT_NOTARIUS_TWO\n",
)
mustWriteTestFile(t, filepath.Join(filepath.Dir(pipelinePath), "profiles", "without-notarius-key.yml"), "notarius:\n llm_api_key: null\n")
mustWriteTestFile(t, filepath.Join(filepath.Dir(pipelinePath), "overlays", "without-notarius-key.yml"), "notarius:\n llm_api_key: null\n")
var validate bytes.Buffer
if err := ConfigValidate(context.Background(), []string{"--config", pipelinePath, "--profile", "production"}, &validate); err != nil {
if err := ConfigValidate(context.Background(), []string{"--config", pipelinePath, "--overlay", "production"}, &validate); err != nil {
t.Fatalf("config validate: %v", err)
}
var show bytes.Buffer
if err := ConfigShow(context.Background(), []string{"--config", pipelinePath, "--profile", "production"}, &show); err != nil {
if err := ConfigShow(context.Background(), []string{"--config", pipelinePath, "--overlay", "production"}, &show); err != nil {
t.Fatalf("config show: %v", err)
}
var sources bytes.Buffer
if err := ConfigSources(context.Background(), []string{"--config", pipelinePath, "--profile", "production"}, &sources); err != nil {
if err := ConfigSources(context.Background(), []string{"--config", pipelinePath, "--overlay", "production"}, &sources); err != nil {
t.Fatalf("config sources: %v", err)
}
var catalog bytes.Buffer
if err := ConfigArtifacts(context.Background(), []string{"--config", pipelinePath, "--profile", "production"}, &catalog); err != nil {
if err := ConfigArtifacts(context.Background(), []string{"--config", pipelinePath, "--overlay", "production"}, &catalog); err != nil {
t.Fatalf("config artifacts: %v", err)
}
var diff bytes.Buffer
@@ -421,7 +422,7 @@ func TestConfigInspectionOutputsNeverExposeResolvedCredentialValues(t *testing.T
t.Fatalf("config diff: %v", err)
}
var removed bytes.Buffer
if err := ConfigShow(context.Background(), []string{"--config", pipelinePath, "--profile", "without-notarius-key"}, &removed); err != nil {
if err := ConfigShow(context.Background(), []string{"--config", pipelinePath, "--overlay", "without-notarius-key"}, &removed); err != nil {
t.Fatalf("config show with removed Notarius credential: %v", err)
}
for label, output := range map[string]string{"show": show.String(), "sources": sources.String(), "artifacts": catalog.String(), "diff": diff.String()} {
@@ -442,7 +443,7 @@ func TestConfigInspectionOutputsNeverExposeResolvedCredentialValues(t *testing.T
t.Fatalf("sources omitted Notarius credential ownership: %q", sources.String())
}
if strings.Contains(removed.String(), "llm_api_key") || strings.Contains(removed.String(), "NARRATIO_INSPECT_NOTARIUS_ONE") {
t.Fatalf("profile null did not remove Notarius credential: %q", removed.String())
t.Fatalf("overlay null did not remove Notarius credential: %q", removed.String())
}
}
@@ -476,35 +477,35 @@ func TestConfigCommandsInspectMaintainedSplitBundleWithoutRuntimeState(t *testin
t.Fatalf("ConfigDiff() error = %v", err)
}
if !strings.Contains(diff.String(), "audita.model") {
t.Fatalf("split bundle profile diff = %q, want model change", diff.String())
t.Fatalf("split bundle overlay diff = %q, want model change", diff.String())
}
if _, err := os.Stat(workspacePath); !os.IsNotExist(err) {
t.Fatalf("inspection created example workspace: %v", err)
}
}
func writeInspectionProfiles(t *testing.T, pipelinePath, production, testing string) {
func writeInspectionOverlays(t *testing.T, pipelinePath, production, testing string) {
t.Helper()
profilesDir := filepath.Join(filepath.Dir(pipelinePath), "profiles")
mustWriteTestFile(t, filepath.Join(profilesDir, "production.yml"), production)
mustWriteTestFile(t, filepath.Join(profilesDir, "testing.yml"), testing)
overlaysDir := filepath.Join(filepath.Dir(pipelinePath), "overlays")
mustWriteTestFile(t, filepath.Join(overlaysDir, "production.yml"), production)
mustWriteTestFile(t, filepath.Join(overlaysDir, "testing.yml"), testing)
}
func TestConfigCommandsSelectProfilesAndCampaigns(t *testing.T) {
func TestConfigCommandsSelectOverlaysAndCampaigns(t *testing.T) {
workspaceRoot := t.TempDir()
pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot)
enableCommandTestProfiles(t, pipelinePath)
enableCommandTestOverlays(t, pipelinePath)
var defaultOut bytes.Buffer
if err := ConfigValidate(context.Background(), []string{"--config", pipelinePath}, &defaultOut); err != nil {
t.Fatalf("default ConfigValidate() error = %v", err)
}
if !strings.Contains(defaultOut.String(), "profile=none") {
if !strings.Contains(defaultOut.String(), "overlay=none") {
t.Fatalf("default output = %q", defaultOut.String())
}
var explicitOut bytes.Buffer
if err := ConfigShow(context.Background(), []string{"--config", pipelinePath, "--profile", "testing"}, &explicitOut); err != nil {
if err := ConfigShow(context.Background(), []string{"--config", pipelinePath, "--overlay", "testing"}, &explicitOut); err != nil {
t.Fatalf("explicit ConfigShow() error = %v", err)
}
if !strings.Contains(explicitOut.String(), "language: fr") {
@@ -589,9 +590,9 @@ whisperx:
func TestConfigSourcesReportsStableOwnership(t *testing.T) {
workspaceRoot := t.TempDir()
pipelinePath, campaignPath, _ := writeValidConfigFiles(t, workspaceRoot)
enableCommandTestProfiles(t, pipelinePath)
enableCommandTestOverlays(t, pipelinePath)
args := []string{"--config", pipelinePath, "--campaign-file", campaignPath, "--profile", "testing"}
args := []string{"--config", pipelinePath, "--campaign-file", campaignPath, "--overlay", "testing"}
var first, second bytes.Buffer
if err := ConfigSources(context.Background(), args, &first); err != nil {
t.Fatalf("first ConfigSources() error = %v", err)
@@ -606,13 +607,13 @@ func TestConfigSourcesReportsStableOwnership(t *testing.T) {
for _, want := range []string{
"root: ",
"imports: none",
"profile: name=testing selection=explicit overlay=",
"overlay: name=testing selection=explicit path=",
"campaign: id=sample-campaign source=",
"party: source=",
"digest: ",
"path\trole\tsource\n",
"workspace.root\troot\t",
"whisperx.language\tprofile\t",
"whisperx.language\toverlay\t",
"derived.players\tparty\t",
"trim.enabled\tdefault\tdefault",
} {
@@ -647,10 +648,10 @@ func TestConfigSourcesReportsCanonicalFamilyAndPublishOrigins(t *testing.T) {
}
}
func TestConfigSourcesPreservesProfileOwnershipThroughFamilyExpansion(t *testing.T) {
func TestConfigSourcesPreservesOverlayOwnershipThroughFamilyExpansion(t *testing.T) {
pipelinePath, campaignPath := writeInspectionFamilyConfig(t)
profilePath := filepath.Join(filepath.Dir(pipelinePath), "profiles", "testing.yml")
mustWriteTestFile(t, profilePath, `scriptorium:
overlayPath := filepath.Join(filepath.Dir(pipelinePath), "overlays", "testing.yml")
mustWriteTestFile(t, overlayPath, `scriptorium:
artifact_families:
character_note:
enabled: true
@@ -658,12 +659,12 @@ func TestConfigSourcesPreservesProfileOwnershipThroughFamilyExpansion(t *testing
`)
var out bytes.Buffer
if err := ConfigSources(context.Background(), []string{"--config", pipelinePath, "--campaign-file", campaignPath, "--profile", "testing"}, &out); err != nil {
if err := ConfigSources(context.Background(), []string{"--config", pipelinePath, "--campaign-file", campaignPath, "--overlay", "testing"}, &out); err != nil {
t.Fatalf("ConfigSources() error = %v", err)
}
want := "scriptorium.artifacts.character_note_arannis.enabled\tfamily\t" + profilePath
want := "scriptorium.artifacts.character_note_arannis.enabled\tfamily\t" + overlayPath
if !strings.Contains(out.String(), want) {
t.Fatalf("sources output missing profile-generated ownership %q:\n%s", want, out.String())
t.Fatalf("sources output missing overlay-generated ownership %q:\n%s", want, out.String())
}
}
@@ -671,7 +672,7 @@ func TestConfigSourcesAttributesInferredArtifactDependencies(t *testing.T) {
dir := t.TempDir()
pipelinePath := filepath.Join(dir, "pipeline.yml")
inputPath := filepath.Join(dir, "input-only.yml")
profilePath := filepath.Join(dir, "profiles", "testing.yml")
overlayPath := filepath.Join(dir, "overlays", "testing.yml")
mustWriteTestFile(t, pipelinePath, `configkit:
imports:
- path: input-only.yml
@@ -701,7 +702,7 @@ scriptorium:
prior:
source: narratio.artifact.first
`)
mustWriteTestFile(t, profilePath, `scriptorium:
mustWriteTestFile(t, overlayPath, `scriptorium:
artifacts:
mixed:
inputs:
@@ -710,13 +711,13 @@ scriptorium:
`)
var out bytes.Buffer
if err := ConfigSources(context.Background(), []string{"--config", pipelinePath, "--profile", "testing"}, &out); err != nil {
if err := ConfigSources(context.Background(), []string{"--config", pipelinePath, "--overlay", "testing"}, &out); err != nil {
t.Fatal(err)
}
for _, want := range []string{
"scriptorium.artifacts.input_only.depends_on\timport\t" + inputPath,
"scriptorium.artifacts.mixed.depends_on\troot\t" + pipelinePath,
"scriptorium.artifacts.mixed.depends_on\tprofile\t" + profilePath,
"scriptorium.artifacts.mixed.depends_on\toverlay\t" + overlayPath,
} {
if !strings.Contains(out.String(), want) {
t.Fatalf("sources output missing dependency contributor %q:\n%s", want, out.String())
@@ -727,11 +728,11 @@ scriptorium:
}
}
func TestConfigSourcesReportsRootImportProfileAndDefaultOwnership(t *testing.T) {
func TestConfigSourcesReportsRootImportOverlayAndDefaultOwnership(t *testing.T) {
dir := t.TempDir()
rootPath := filepath.Join(dir, "pipeline.yml")
importPath := filepath.Join(dir, "base.yml")
profilePath := filepath.Join(dir, "profiles", "testing.yml")
overlayPath := filepath.Join(dir, "overlays", "testing.yml")
mustWriteTestFile(t, rootPath, `configkit:
imports:
- path: base.yml
@@ -741,19 +742,19 @@ whisperx:
transcribe_url: https://transcription.example.com/transcribe
`)
mustWriteTestFile(t, importPath, "storage:\n backend: local\n")
mustWriteTestFile(t, profilePath, "whisperx:\n language: fr\n")
mustWriteTestFile(t, overlayPath, "whisperx:\n language: fr\n")
var out bytes.Buffer
if err := ConfigSources(context.Background(), []string{"--config", rootPath, "--profile", "testing"}, &out); err != nil {
if err := ConfigSources(context.Background(), []string{"--config", rootPath, "--overlay", "testing"}, &out); err != nil {
t.Fatalf("ConfigSources() error = %v", err)
}
output := out.String()
for _, want := range []string{
"import: " + importPath,
"profile: name=testing selection=explicit overlay=" + profilePath,
"overlay: name=testing selection=explicit path=" + overlayPath,
"workspace.root\troot\t" + rootPath,
"storage.backend\timport\t" + importPath,
"whisperx.language\tprofile\t" + profilePath,
"whisperx.language\toverlay\t" + overlayPath,
"trim.enabled\tdefault\tdefault",
} {
if !strings.Contains(output, want) {

View File

@@ -20,8 +20,8 @@ type configDiffFlags struct {
}
type configDiffRequest struct {
leftProfile string
rightProfile string
leftOverlay string
rightOverlay string
flags configDiffFlags
}
@@ -37,7 +37,7 @@ type configDiffRecord struct {
Right string
}
// ConfigDiff compares two explicitly selected profiles through the same
// ConfigDiff compares two explicitly selected overlays through the same
// read-only configuration resolution boundary used by config validate, show,
// and sources. Differences describe normalized effective values rather than
// the layout of root, import, or overlay files.
@@ -100,7 +100,7 @@ func configDiffWithDependencies(_ context.Context, args []string, out io.Writer,
}
func parseConfigDiffRequest(args []string, out io.Writer) (configDiffRequest, error) {
profiles, flagArgs := splitConfigDiffArguments(args)
overlays, flagArgs := splitConfigDiffArguments(args)
fs := flag.NewFlagSet("config diff", flag.ContinueOnError)
fs.SetOutput(out)
var request configDiffRequest
@@ -113,9 +113,9 @@ func parseConfigDiffRequest(args []string, out io.Writer) (configDiffRequest, er
fs.Var(&request.flags.campaignFilePath, "campaign-file", "path to campaign.yml")
fs.Var(&request.flags.secretsDir, "secrets-dir", "secrets directory (optional; defaults beside pipeline.yml)")
fs.Usage = func() {
fmt.Fprintln(out, "Usage: narratio config diff <left-profile> <right-profile> [--config <pipeline.yml>] [--campaign <id> | --campaign-file <campaign.yml>] [--secrets-dir <path>]")
fmt.Fprintln(out, "Usage: narratio config diff <left-overlay> <right-overlay> [--config <pipeline.yml>] [--campaign <id> | --campaign-file <campaign.yml>] [--secrets-dir <path>]")
fmt.Fprintln(out)
fmt.Fprintln(out, "Compares fully resolved profiles without a session, workspace, manifest, adapters, or external services.")
fmt.Fprintln(out, "Compares fully resolved overlays without a session, workspace, manifest, adapters, or external services.")
fmt.Fprintln(out)
fmt.Fprintln(out, "Flags:")
fs.PrintDefaults()
@@ -123,30 +123,30 @@ func parseConfigDiffRequest(args []string, out io.Writer) (configDiffRequest, er
if err := fs.Parse(flagArgs); err != nil {
return configDiffRequest{}, fmt.Errorf("config diff: invalid flags: %w", err)
}
profiles = append(profiles, fs.Args()...)
if len(profiles) != 2 || strings.TrimSpace(profiles[0]) == "" || strings.TrimSpace(profiles[1]) == "" {
return configDiffRequest{}, fmt.Errorf("config diff: exactly two non-empty profile names are required")
overlays = append(overlays, fs.Args()...)
if len(overlays) != 2 || strings.TrimSpace(overlays[0]) == "" || strings.TrimSpace(overlays[1]) == "" {
return configDiffRequest{}, fmt.Errorf("config diff: exactly two non-empty overlay names are required")
}
if profiles[0] == profiles[1] {
return configDiffRequest{}, fmt.Errorf("config diff: profile names must differ")
if overlays[0] == overlays[1] {
return configDiffRequest{}, fmt.Errorf("config diff: overlay names must differ")
}
if request.flags.campaignPath.set && request.flags.campaignFilePath.set {
return configDiffRequest{}, fmt.Errorf("config diff: --campaign and --campaign-file are mutually exclusive")
}
request.leftProfile = profiles[0]
request.rightProfile = profiles[1]
request.leftOverlay = overlays[0]
request.rightOverlay = overlays[1]
return request, nil
}
// splitConfigDiffArguments accepts the documented positional-first syntax and
// also permits flags before or between profile names. Values belonging to the
// also permits flags before or between overlay names. Values belonging to the
// supported string flags stay with their flag so they are not mistaken for
// profile names.
func splitConfigDiffArguments(args []string) (profiles, flagArgs []string) {
// overlay names.
func splitConfigDiffArguments(args []string) (overlays, flagArgs []string) {
for index := 0; index < len(args); index++ {
argument := args[index]
if !strings.HasPrefix(argument, "-") || argument == "-" {
profiles = append(profiles, argument)
overlays = append(overlays, argument)
continue
}
flagArgs = append(flagArgs, argument)
@@ -161,12 +161,12 @@ func splitConfigDiffArguments(args []string) (profiles, flagArgs []string) {
index++
flagArgs = append(flagArgs, args[index])
}
return profiles, flagArgs
return overlays, flagArgs
}
func configDiffStringFlag(name string) bool {
switch name {
case "config", "campaign", "campaign-file", "profile", "secrets-dir":
case "config", "campaign", "campaign-file", "secrets-dir":
return true
default:
return false
@@ -175,13 +175,13 @@ func configDiffStringFlag(name string) bool {
func resolveConfigDiff(request configDiffRequest, deps commandDependencies) (*configDiffResolved, error) {
deps = normalizeCommandDependencies(deps)
pipelinePath, leftPipeline, err := loadPipelineConfigWithDependencies(request.flags.pipelinePath.value, config.PipelineLoadOptions{Profile: &request.leftProfile}, request.flags.secretsDir.pointer(), deps)
pipelinePath, leftPipeline, err := loadPipelineConfigWithDependencies(request.flags.pipelinePath.value, config.PipelineLoadOptions{Overlay: &request.leftOverlay}, request.flags.secretsDir.pointer(), deps)
if err != nil {
return nil, fmt.Errorf("load left pipeline profile: %w", err)
return nil, fmt.Errorf("load left pipeline overlay: %w", err)
}
_, rightPipeline, err := loadPipelineConfigWithDependencies(request.flags.pipelinePath.value, config.PipelineLoadOptions{Profile: &request.rightProfile}, request.flags.secretsDir.pointer(), deps)
_, rightPipeline, err := loadPipelineConfigWithDependencies(request.flags.pipelinePath.value, config.PipelineLoadOptions{Overlay: &request.rightOverlay}, request.flags.secretsDir.pointer(), deps)
if err != nil {
return nil, fmt.Errorf("load right pipeline profile: %w", err)
return nil, fmt.Errorf("load right pipeline overlay: %w", err)
}
needsCampaign := pipelineHasArtifactFamilies(leftPipeline) || pipelineHasArtifactFamilies(rightPipeline)
hasCampaignSelection := request.flags.campaignPath.set || request.flags.campaignFilePath.set
@@ -259,7 +259,7 @@ func resolveSharedDiffCampaignPath(left, right *config.PipelineConfig, flags con
return "", rightErr
}
if leftPath != rightPath {
return "", fmt.Errorf("profile selections resolve different campaign files; specify --campaign-file to compare one campaign")
return "", fmt.Errorf("overlay selections resolve different campaign files; specify --campaign-file to compare one campaign")
}
return leftPath, nil
}

View File

@@ -46,7 +46,7 @@ func loadCommandConfigWithDependencies(ctx context.Context, pipelineFlag, campai
}
}()
base, err := loadPipelineCampaignConfigWithDependencies(pipelineFlag, campaignFlag, campaignFileFlag, config.PipelineLoadOptions{Profile: sessionOpts.Profile}, secretsDir, deps)
base, err := loadPipelineCampaignConfigWithDependencies(pipelineFlag, campaignFlag, campaignFileFlag, config.PipelineLoadOptions{Overlay: sessionOpts.Overlay}, secretsDir, deps)
if err != nil {
return nil, err
}

View File

@@ -14,13 +14,13 @@ func effectiveConfigProvenance(cfg *config.Config) *manifest.EffectiveConfigProv
provenance := &manifest.EffectiveConfigProvenance{
EffectiveConfigDigest: config.EffectivePipelineDigest(cfg.Pipeline),
}
if selected, ok := config.SelectedPipelineProfile(cfg.Pipeline); ok {
provenance.SelectedProfile = &manifest.SelectedProfileProvenance{
if selected, ok := config.SelectedPipelineOverlay(cfg.Pipeline); ok {
provenance.SelectedOverlay = &manifest.SelectedOverlayProvenance{
Name: selected.Name,
Source: selected.Source,
}
}
if provenance.SelectedProfile == nil && provenance.EffectiveConfigDigest == "" {
if provenance.SelectedOverlay == nil && provenance.EffectiveConfigDigest == "" {
return nil
}
return provenance
@@ -35,30 +35,30 @@ func applyEffectiveConfigProvenance(m *manifest.Manifest, cfg *config.Config) {
func effectiveConfigSummary(cfg *config.Config) string {
provenance := effectiveConfigProvenance(cfg)
if provenance == nil {
return "profile=none digest=unavailable"
return "overlay=none digest=unavailable"
}
profile := "none"
if provenance.SelectedProfile != nil {
profile = provenance.SelectedProfile.Name + " (" + provenance.SelectedProfile.Source + ")"
overlay := "none"
if provenance.SelectedOverlay != nil {
overlay = provenance.SelectedOverlay.Name + " (" + provenance.SelectedOverlay.Source + ")"
}
digest := provenance.EffectiveConfigDigest
if digest == "" {
digest = "unavailable"
}
return fmt.Sprintf("profile=%s digest=%s", profile, digest)
return fmt.Sprintf("overlay=%s digest=%s", overlay, digest)
}
func persistedEffectiveConfigSummary(provenance *manifest.EffectiveConfigProvenance) string {
if provenance == nil {
return "profile=none digest=unavailable"
return "overlay=none digest=unavailable"
}
profile := "none"
if provenance.SelectedProfile != nil {
profile = provenance.SelectedProfile.Name + " (" + provenance.SelectedProfile.Source + ")"
overlay := "none"
if provenance.SelectedOverlay != nil {
overlay = provenance.SelectedOverlay.Name + " (" + provenance.SelectedOverlay.Source + ")"
}
digest := provenance.EffectiveConfigDigest
if digest == "" {
digest = "unavailable"
}
return fmt.Sprintf("profile=%s digest=%s", profile, digest)
return fmt.Sprintf("overlay=%s digest=%s", overlay, digest)
}

View File

@@ -817,7 +817,7 @@ func TestExtractLifecycleRealConfigProfilesSelectNotariusExplicitly(t *testing.T
if err := os.WriteFile(filepath.Join(filepath.Dir(pipelinePath), "notarius.yml"), []byte("pipelines: {}\n"), 0o644); err != nil {
t.Fatal(err)
}
profilesDir := filepath.Join(filepath.Dir(pipelinePath), "profiles")
profilesDir := filepath.Join(filepath.Dir(pipelinePath), "overlays")
if err := os.MkdirAll(profilesDir, 0o755); err != nil {
t.Fatal(err)
}
@@ -832,7 +832,7 @@ func TestExtractLifecycleRealConfigProfilesSelectNotariusExplicitly(t *testing.T
}
load := func(profile string) *config.Config {
t.Helper()
loaded, err := config.LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, config.SessionLoadOptions{Profile: &profile})
loaded, err := config.LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, config.SessionLoadOptions{Overlay: &profile})
if err != nil {
t.Fatalf("load profile %q: %v", profile, err)
}

View File

@@ -23,7 +23,7 @@ type commonConfigFlags struct {
sessionPath string
sessionID string
previousSessionID string
profile singletonStringFlag
overlay singletonStringFlag
secretsDir singletonStringFlag
}
@@ -34,8 +34,8 @@ func addCommonConfigFlags(fs *flag.FlagSet, flags *commonConfigFlags) {
fs.StringVar(&flags.sessionPath, "session", "", "path to session.yml")
fs.StringVar(&flags.sessionID, "session-id", "", "session identifier")
fs.StringVar(&flags.previousSessionID, "previous-session-id", "", "expected previous session identifier")
flags.profile.name = "profile"
fs.Var(&flags.profile, "profile", "named pipeline profile")
flags.overlay.name = "overlay"
fs.Var(&flags.overlay, "overlay", "named pipeline overlay")
flags.secretsDir.name = "secrets-dir"
fs.Var(&flags.secretsDir, "secrets-dir", "secrets directory (optional; defaults beside pipeline.yml)")
}
@@ -44,7 +44,7 @@ func (f commonConfigFlags) sessionOptions() config.SessionLoadOptions {
return config.SessionLoadOptions{
SessionID: f.sessionID,
PreviousSessionID: f.previousSessionID,
Profile: f.profile.pointer(),
Overlay: f.overlay.pointer(),
}
}

View File

@@ -27,7 +27,7 @@ func sessionInitWithDependencies(ctx context.Context, args []string, out io.Writ
fs := flag.NewFlagSet("session init", flag.ContinueOnError)
fs.SetOutput(io.Discard)
var pipelinePath, campaignPath, campaignFilePath, sessionID, previousSessionID, date, title, output, audioS3Prefix, audioDir string
var profile singletonStringFlag
var overlay singletonStringFlag
var secretsDir singletonStringFlag
var remote, force bool
fs.StringVar(&pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)")
@@ -40,8 +40,8 @@ func sessionInitWithDependencies(ctx context.Context, args []string, out io.Writ
fs.StringVar(&output, "output", "", "local output session.yml path")
fs.StringVar(&audioS3Prefix, "audio-s3-prefix", "", "session audio S3 prefix")
fs.StringVar(&audioDir, "audio-dir", "", "local audio directory")
profile.name = "profile"
fs.Var(&profile, "profile", "named pipeline profile")
overlay.name = "overlay"
fs.Var(&overlay, "overlay", "named pipeline overlay")
secretsDir.name = "secrets-dir"
fs.Var(&secretsDir, "secrets-dir", "secrets directory (optional; defaults beside pipeline.yml)")
fs.BoolVar(&remote, "remote", false, "write session.yml to S3 session prefix")
@@ -59,7 +59,7 @@ func sessionInitWithDependencies(ctx context.Context, args []string, out io.Writ
return fmt.Errorf("session init: --audio-dir and --audio-s3-prefix are mutually exclusive")
}
base, err := loadPipelineCampaignConfigWithDependencies(pipelinePath, campaignPath, campaignFilePath, config.PipelineLoadOptions{Profile: profile.pointer()}, secretsDir.pointer(), deps)
base, err := loadPipelineCampaignConfigWithDependencies(pipelinePath, campaignPath, campaignFilePath, config.PipelineLoadOptions{Overlay: overlay.pointer()}, secretsDir.pointer(), deps)
if err != nil {
return fmt.Errorf("session init: %w", err)
}

View File

@@ -11,10 +11,10 @@ import (
"gitea.maximumdirect.net/eric/narratio/internal/config"
)
func TestRunProfileSelectionFlowsThroughSharedLoader(t *testing.T) {
func TestRunOverlaySelectionFlowsThroughSharedLoader(t *testing.T) {
workspaceRoot := t.TempDir()
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
enableCommandTestProfiles(t, pipelinePath)
enableCommandTestOverlays(t, pipelinePath)
var language string
executor := func(_ context.Context, cfg *config.Config, _ BoundedPlan, _ RunOptions) (*RunSummary, error) {
@@ -23,12 +23,17 @@ func TestRunProfileSelectionFlowsThroughSharedLoader(t *testing.T) {
}
if err := runWithPipelineExecutor(context.Background(), []string{
"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--profile", "testing",
"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--overlay", "testing",
}, io.Discard, executor); err != nil {
t.Fatal(err)
}
if language != "fr" {
t.Fatalf("explicit profile language = %q, want fr", language)
t.Fatalf("explicit overlay language = %q, want fr", language)
}
if err := runWithPipelineExecutor(context.Background(), []string{
"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--profile", "testing",
}, io.Discard, executor); err == nil {
t.Fatal("retired pipeline selector was accepted")
}
if err := runWithPipelineExecutor(context.Background(), []string{
@@ -40,25 +45,25 @@ func TestRunProfileSelectionFlowsThroughSharedLoader(t *testing.T) {
t.Fatalf("base configuration language = %q, want en", language)
}
for _, profile := range []string{"unknown"} {
for _, overlay := range []string{"unknown"} {
err := runWithPipelineExecutor(context.Background(), []string{
"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--profile", profile,
"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--overlay", overlay,
}, io.Discard, executor)
if err == nil || !strings.Contains(err.Error(), "profile") {
t.Fatalf("profile %q error = %v, want selection rejection", profile, err)
if err == nil || !strings.Contains(err.Error(), "overlay") {
t.Fatalf("overlay %q error = %v, want selection rejection", overlay, err)
}
}
if err := runWithPipelineExecutor(context.Background(), []string{
"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--profile", "",
"2026-05-03", "--config", pipelinePath, "--campaign-file", campaignPath, "--session", sessionPath, "--overlay", "",
}, io.Discard, executor); err != nil {
t.Fatalf("explicit empty profile should select base: %v", err)
t.Fatalf("explicit empty overlay should select base: %v", err)
}
}
func enableCommandTestProfiles(t *testing.T, pipelinePath string) {
func enableCommandTestOverlays(t *testing.T, pipelinePath string) {
t.Helper()
dir := filepath.Join(filepath.Dir(pipelinePath), "profiles")
dir := filepath.Join(filepath.Dir(pipelinePath), "overlays")
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}

View File

@@ -14,7 +14,7 @@ import (
func TestRegenerateArtifactsForwardsCanonicalPipelineRequest(t *testing.T) {
workspaceRoot := t.TempDir()
pipelinePath, campaignPath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot)
enableCommandTestProfiles(t, pipelinePath)
enableCommandTestOverlays(t, pipelinePath)
var capturedPlan BoundedPlan
var capturedOptions RunOptions
executor := func(_ context.Context, cfg *config.Config, plan BoundedPlan, options RunOptions) (*RunSummary, error) {
@@ -30,7 +30,7 @@ func TestRegenerateArtifactsForwardsCanonicalPipelineRequest(t *testing.T) {
"--config", pipelinePath,
"--campaign-file", campaignPath,
"--session", sessionPath,
"--profile", "testing",
"--overlay", "testing",
}, io.Discard, io.Discard, executor)
if code != 0 {
t.Fatalf("executeWithPipelineExecutor() code = %d, want 0", code)

View File

@@ -80,7 +80,7 @@ func runStageWithDependencies(ctx context.Context, args []string, out io.Writer,
SessionPath: flags.sessionPath,
SessionID: flags.sessionID,
PreviousSessionID: flags.previousSessionID,
Profile: flags.profile.pointer(),
Overlay: flags.overlay.pointer(),
SecretsDir: flags.secretsDir.pointer(),
Force: force,
SelectedArtifacts: normalizedArtifacts,
@@ -139,7 +139,7 @@ func analyzeWithDependencies(ctx context.Context, args []string, out io.Writer,
SessionPath: flags.sessionPath,
SessionID: flags.sessionID,
PreviousSessionID: flags.previousSessionID,
Profile: flags.profile.pointer(),
Overlay: flags.overlay.pointer(),
SecretsDir: flags.secretsDir.pointer(),
Force: true,
SelectedArtifacts: normalizedArtifacts,
@@ -196,7 +196,7 @@ func publishWithDependencies(ctx context.Context, args []string, out io.Writer,
SessionPath: flags.sessionPath,
SessionID: flags.sessionID,
PreviousSessionID: flags.previousSessionID,
Profile: flags.profile.pointer(),
Overlay: flags.overlay.pointer(),
SecretsDir: flags.secretsDir.pointer(),
Force: true,
SelectedArtifacts: normalizedArtifacts,
@@ -224,7 +224,7 @@ type singleStageCommand struct {
SessionPath string
SessionID string
PreviousSessionID string
Profile *string
Overlay *string
SecretsDir *string
Force bool
SelectedArtifacts []string
@@ -240,7 +240,7 @@ func runSingleStageCommand(ctx context.Context, req singleStageCommand, executor
loaded, err := loadCommandConfigWithDependencies(ctx, req.PipelinePath, req.CampaignPath, req.CampaignFilePath, req.SessionPath, config.SessionLoadOptions{
SessionID: req.SessionID,
PreviousSessionID: req.PreviousSessionID,
Profile: req.Profile,
Overlay: req.Overlay,
}, req.SecretsDir, deps)
if err != nil {
return nil, fmt.Errorf("%s: %w", req.CommandName, err)

View File

@@ -93,7 +93,7 @@ func declarationDefinitionSources(cfg *PipelineConfig, declaration configkit.Pat
seen := map[string]struct{}{}
for _, change := range cfg.resolution.configkitMetadata.History(declaration) {
if change.Origin.Layer != configkit.LayerBase && change.Origin.Layer != configkit.LayerProfile {
if change.Origin.Layer != configkit.LayerBase && change.Origin.Layer != configkit.LayerOverlay {
continue
}
for _, location := range change.Origin.Locations {

View File

@@ -14,21 +14,21 @@ const (
effectivePipelineDigestVersion = "narratio-effective-pipeline-v3\n"
)
// PipelineProfileProvenance identifies the profile selected while resolving a
// PipelineOverlayProvenance identifies the overlay selected while resolving a
// pipeline. It contains only non-secret selection metadata.
type PipelineProfileProvenance struct {
type PipelineOverlayProvenance struct {
Name string
Source string
OverlayPath string
}
// SelectedPipelineProfile reports the profile used to resolve cfg, if any.
func SelectedPipelineProfile(cfg *PipelineConfig) (*PipelineProfileProvenance, bool) {
if cfg == nil || cfg.resolution == nil || cfg.resolution.selectedProfile == nil {
// SelectedPipelineOverlay reports the overlay used to resolve cfg, if any.
func SelectedPipelineOverlay(cfg *PipelineConfig) (*PipelineOverlayProvenance, bool) {
if cfg == nil || cfg.resolution == nil || cfg.resolution.selectedOverlay == nil {
return nil, false
}
selection := cfg.resolution.selectedProfile
return &PipelineProfileProvenance{Name: selection.name, Source: selection.source, OverlayPath: selection.overlayPath}, true
selection := cfg.resolution.selectedOverlay
return &PipelineOverlayProvenance{Name: selection.name, Source: selection.source, OverlayPath: selection.overlayPath}, true
}
// EffectivePipelineDigest reports the deterministic secret-free digest for a

View File

@@ -401,8 +401,8 @@ func pipelineSourceRole(cfg *PipelineConfig, source string) string {
if source == cfg.resolution.rootPath {
return "root"
}
if cfg.resolution.selectedProfile != nil && source == cfg.resolution.selectedProfile.overlayPath {
return "profile"
if cfg.resolution.selectedOverlay != nil && source == cfg.resolution.selectedOverlay.overlayPath {
return "overlay"
}
for _, imported := range cfg.resolution.imports {
if source == imported {

View File

@@ -15,14 +15,14 @@ func LoadPipeline(path string) (*PipelineConfig, error) {
return LoadPipelineWithOptions(path, PipelineLoadOptions{})
}
// PipelineLoadOptions carries an optional explicit profile selection. Nil,
// PipelineLoadOptions carries an optional explicit overlay selection. Nil,
// empty, and the exact name "default" select the base configuration.
type PipelineLoadOptions struct {
Profile *string
Overlay *string
}
// LoadPipelineWithOptions loads pipeline configuration with strict field
// checking and optional named-profile selection.
// checking and optional named-overlay selection.
func LoadPipelineWithOptions(path string, opts PipelineLoadOptions) (*PipelineConfig, error) {
cfg, err := loadPipelineWithConfigKit(path, opts)
if err != nil {
@@ -31,20 +31,20 @@ func LoadPipelineWithOptions(path string, opts PipelineLoadOptions) (*PipelineCo
return finalizeLoadedPipeline(path, cfg)
}
// LoadPipelineProfilePair resolves two profiles as independent ConfigKit loads.
func LoadPipelineProfilePair(path, leftProfile, rightProfile string) (*PipelineConfig, *PipelineConfig, error) {
if leftProfile == rightProfile {
return nil, nil, fmt.Errorf("load pipeline profiles: left and right profile selections must differ")
// LoadPipelineOverlayPair resolves two overlays as independent ConfigKit loads.
func LoadPipelineOverlayPair(path, leftOverlay, rightOverlay string) (*PipelineConfig, *PipelineConfig, error) {
if leftOverlay == rightOverlay {
return nil, nil, fmt.Errorf("load pipeline overlays: left and right overlay selections must differ")
}
leftOptions := PipelineLoadOptions{Profile: &leftProfile}
rightOptions := PipelineLoadOptions{Profile: &rightProfile}
leftOptions := PipelineLoadOptions{Overlay: &leftOverlay}
rightOptions := PipelineLoadOptions{Overlay: &rightOverlay}
left, err := LoadPipelineWithOptions(path, leftOptions)
if err != nil {
return nil, nil, fmt.Errorf("load left pipeline profile: %w", err)
return nil, nil, fmt.Errorf("load left pipeline overlay: %w", err)
}
right, err := LoadPipelineWithOptions(path, rightOptions)
if err != nil {
return nil, nil, fmt.Errorf("load right pipeline profile: %w", err)
return nil, nil, fmt.Errorf("load right pipeline overlay: %w", err)
}
return left, right, nil
}
@@ -76,7 +76,7 @@ func LoadSession(path string) (*SessionConfig, error) {
type SessionLoadOptions struct {
SessionID string
PreviousSessionID string
Profile *string
Overlay *string
}
// LoadSessionWithOptions loads session configuration from a YAML file with
@@ -143,7 +143,7 @@ func Load(pipelinePath string, paths ...string) (*Config, error) {
// LoadWithSessionOptions loads and resolves combined pipeline, campaign, and
// session configuration with expected session identity checks.
func LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath string, sessionOpts SessionLoadOptions) (*Config, error) {
pipelineCfg, err := LoadPipelineWithOptions(pipelinePath, PipelineLoadOptions{Profile: sessionOpts.Profile})
pipelineCfg, err := LoadPipelineWithOptions(pipelinePath, PipelineLoadOptions{Overlay: sessionOpts.Overlay})
if err != nil {
return nil, err
}

View File

@@ -358,7 +358,7 @@ func TestExamplesLoadAndValidate(t *testing.T) {
t.Run(profile.name, func(t *testing.T) {
options := PipelineLoadOptions{}
if profile.selection != "" {
options.Profile = &profile.selection
options.Overlay = &profile.selection
}
pipeline, err := LoadPipelineWithOptions(pipelinePath, options)
if err != nil {
@@ -381,12 +381,12 @@ 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)
}
selectedProfile, selected := SelectedPipelineProfile(cfg.Pipeline)
selectedOverlay, selected := SelectedPipelineOverlay(cfg.Pipeline)
if profile.selection == "" && selected {
t.Fatalf("production base unexpectedly selected profile %#v", selectedProfile)
t.Fatalf("production base unexpectedly selected overlay %#v", selectedOverlay)
}
if profile.selection != "" && (!selected || selectedProfile.Name != profile.selection) {
t.Fatalf("selected profile = %#v, want %q", selectedProfile, profile.selection)
if profile.selection != "" && (!selected || selectedOverlay.Name != profile.selection) {
t.Fatalf("selected overlay = %#v, want %q", selectedOverlay, profile.selection)
}
for _, key := range []string{
"session_recap",

View File

@@ -16,7 +16,7 @@ type pipelineResolutionMetadata struct {
rootPath string
imports []string
sources []string
selectedProfile *pipelineProfileSelection
selectedOverlay *pipelineOverlaySelection
effectiveDigest string
logicalNotariusConfig string
logicalNotariusWorking string
@@ -29,7 +29,7 @@ type pipelineResolutionMetadata struct {
configkitSources []configkit.SourceInfo
}
type pipelineProfileSelection struct {
type pipelineOverlaySelection struct {
name string
source string
overlayPath string
@@ -41,8 +41,8 @@ type pipelineFieldOwnership struct {
}
func loadPipelineWithConfigKit(path string, opts PipelineLoadOptions) (*PipelineConfig, error) {
profile := selectedPipelineProfileName(opts)
result, err := loadPipelineConfigKitResult(path, profile)
overlay := selectedPipelineOverlayName(opts)
result, err := loadPipelineConfigKitResult(path, overlay)
if err != nil {
return nil, describePipelineLoadError(err)
}
@@ -55,10 +55,10 @@ func loadPipelineWithConfigKit(path string, opts PipelineLoadOptions) (*Pipeline
return &cfg, nil
}
func loadPipelineConfigKitResult(path, profile string) (*configkit.Result[PipelineConfig], error) {
func loadPipelineConfigKitResult(path, overlay string) (*configkit.Result[PipelineConfig], error) {
return configkit.Load(configkit.File(path), configkit.Options[PipelineConfig]{
Imports: true,
Profile: profile,
Overlay: overlay,
Definition: configkit.Definition[PipelineConfig]{
Defaults: pipelineConfigDefaults,
Resolve: resolvePipelineConfig,
@@ -104,11 +104,11 @@ func describeConfigKitLoadError(kind string, err error) error {
return fmt.Errorf("load %s config: strict decode failed or validation failed: %w: %s", kind, err, strings.TrimSpace(diagnostics.String()))
}
func selectedPipelineProfileName(opts PipelineLoadOptions) string {
if opts.Profile == nil || *opts.Profile == "" || *opts.Profile == "default" {
func selectedPipelineOverlayName(opts PipelineLoadOptions) string {
if opts.Overlay == nil || *opts.Overlay == "" || *opts.Overlay == "default" {
return ""
}
return *opts.Profile
return *opts.Overlay
}
func pipelineConfigDefaults() PipelineConfig {
@@ -282,8 +282,8 @@ func pipelineMetadataFromConfigKit(cfg *PipelineConfig, result *configkit.Result
} else if source.Layer == configkit.LayerBase {
resolved.imports = append(resolved.imports, path)
}
if source.Layer == configkit.LayerProfile {
resolved.selectedProfile = &pipelineProfileSelection{name: selectedPipelineProfileName(opts), source: "explicit", overlayPath: path}
if source.Layer == configkit.LayerOverlay {
resolved.selectedOverlay = &pipelineOverlaySelection{name: selectedPipelineOverlayName(opts), source: "explicit", overlayPath: path}
}
}
detailed, err := pipelineMetadataPaths(cfg)
@@ -347,7 +347,7 @@ func rejectDuplicateBaseDefinitions(metadata configkit.Metadata, sources []confi
pathsByID[source.ID] = source.Path
}
// Enumerate retained declarations, not just surviving effective keys. Loading
// the base separately would also resolve credentials removed by the profile.
// the base separately would also resolve credentials removed by the overlay.
// ConfigKit returns canonical JSON Pointers in deterministic order.
for _, path := range metadata.HistoricalPaths() {
parts := strings.Split(path.String(), "/")

View File

@@ -8,7 +8,7 @@ import (
"testing"
)
func TestPipelineConfigKitImportsProfileAndBaseSelection(t *testing.T) {
func TestPipelineConfigKitImportsOverlayAndBaseSelection(t *testing.T) {
dir := t.TempDir()
writePipelineFixture(t, dir, "pipeline.yml", `configkit:
imports:
@@ -24,33 +24,62 @@ whisperx:
enabled: false
output_path: artifacts/recap.md
`)
writePipelineFixture(t, dir, "profiles/testing.yml", "workspace:\n root: /srv/testing\n")
writePipelineFixture(t, dir, "profiles/unselected.yml", "unknown: [\n")
writePipelineFixture(t, dir, "overlays/testing.yml", "workspace:\n root: /srv/testing\n")
writePipelineFixture(t, dir, "overlays/identical.yml", "workspace:\n root: /srv/base\n")
writePipelineFixture(t, dir, "overlays/unselected.yml", "unknown: [\n")
root := filepath.Join(dir, "pipeline.yml")
var baseDigest string
for _, profile := range []*string{nil, stringPointer(""), stringPointer("default")} {
cfg, err := LoadPipelineWithOptions(root, PipelineLoadOptions{Profile: profile})
cfg, err := LoadPipelineWithOptions(root, PipelineLoadOptions{Overlay: profile})
if err != nil {
t.Fatalf("load base selection %#v: %v", profile, err)
}
if cfg.Workspace.Root != "/srv/base" {
t.Fatalf("base workspace = %q", cfg.Workspace.Root)
}
if baseDigest == "" {
baseDigest = EffectivePipelineDigest(cfg)
} else if EffectivePipelineDigest(cfg) != baseDigest {
t.Fatal("equivalent base selections changed effective digest")
}
}
identical := "identical"
unchanged, err := LoadPipelineWithOptions(root, PipelineLoadOptions{Overlay: &identical})
if err != nil {
t.Fatal(err)
}
if EffectivePipelineDigest(unchanged) != baseDigest {
t.Fatal("equivalent overlay changed effective digest")
}
selected := "testing"
cfg, err := LoadPipelineWithOptions(root, PipelineLoadOptions{Profile: &selected})
cfg, err := LoadPipelineWithOptions(root, PipelineLoadOptions{Overlay: &selected})
if err != nil {
t.Fatal(err)
}
if cfg.Workspace.Root != "/srv/testing" || len(EffectivePipelineImports(cfg)) != 2 {
t.Fatalf("profiled pipeline = %#v, imports=%#v", cfg.Workspace, EffectivePipelineImports(cfg))
}
profile, ok := SelectedPipelineProfile(cfg)
if !ok || profile.Name != "testing" || profile.OverlayPath != filepath.Join(dir, "profiles", "testing.yml") {
profile, ok := SelectedPipelineOverlay(cfg)
if !ok || profile.Name != "testing" || profile.OverlayPath != filepath.Join(dir, "overlays", "testing.yml") {
t.Fatalf("selected profile = %#v, %v", profile, ok)
}
}
func TestPipelineOverlayDoesNotDiscoverLegacyDirectory(t *testing.T) {
dir := t.TempDir()
writePipelineFixture(t, dir, "pipeline.yml", "configkit:\n imports:\n - path: profiles/shared.yml\n")
writePipelineFixture(t, dir, "profiles/shared.yml", "workspace:\n root: /srv/base\n")
writePipelineFixture(t, dir, "profiles/testing.yml", "workspace:\n root: /srv/old\n")
if base, err := LoadPipeline(filepath.Join(dir, "pipeline.yml")); err != nil || base.Workspace.Root != "/srv/base" {
t.Fatalf("explicit import from profiles directory = %#v, %v", base, err)
}
selected := "testing"
if _, err := LoadPipelineWithOptions(filepath.Join(dir, "pipeline.yml"), PipelineLoadOptions{Overlay: &selected}); err == nil || !strings.Contains(err.Error(), "overlay.missing") {
t.Fatalf("legacy selected file result = %v, want missing overlay", err)
}
}
func TestPipelineConfigKitRejectsDuplicateBaseDeclarations(t *testing.T) {
for _, test := range []struct {
name, first, second, clearedContainer string
@@ -69,14 +98,14 @@ whisperx:
`)
writePipelineFixture(t, dir, "first.yml", test.first)
writePipelineFixture(t, dir, "second.yml", test.second)
writePipelineFixture(t, dir, "profiles/clear-container.yml", test.clearedContainer)
writePipelineFixture(t, dir, "profiles/clear-scriptorium.yml", "scriptorium: null\n")
writePipelineFixture(t, dir, "overlays/clear-container.yml", test.clearedContainer)
writePipelineFixture(t, dir, "overlays/clear-scriptorium.yml", "scriptorium: null\n")
for _, profile := range []string{"", "clear-container", "clear-scriptorium"} {
t.Run(profile, func(t *testing.T) {
options := PipelineLoadOptions{}
if profile != "" {
options.Profile = &profile
options.Overlay = &profile
}
_, err := LoadPipelineWithOptions(filepath.Join(dir, "pipeline.yml"), options)
if err == nil || !strings.Contains(err.Error(), "declared by multiple base files") ||
@@ -89,7 +118,7 @@ whisperx:
}
}
func TestPipelineProfileDoesNotRequireUnusedBaseCredentials(t *testing.T) {
func TestPipelineOverlayDoesNotRequireUnusedBaseCredentials(t *testing.T) {
const production = "NARRATIO_PROFILE_UNUSED_PROD_KEY"
const testingKey = "NARRATIO_PROFILE_TEST_KEY"
t.Setenv(production, "")
@@ -101,9 +130,9 @@ func TestPipelineProfileDoesNotRequireUnusedBaseCredentials(t *testing.T) {
t.Run(credential, func(t *testing.T) {
dir := t.TempDir()
root := writePipelineFixture(t, dir, "pipeline.yml", "whisperx:\n transcribe_url: https://transcription.example.test/transcribe\naudita:\n llm_api_key: "+production+"\n")
writePipelineFixture(t, dir, "profiles/testing.yml", "audita:\n llm_api_key: "+credential+"\n")
writePipelineFixture(t, dir, "overlays/testing.yml", "audita:\n llm_api_key: "+credential+"\n")
profile := "testing"
cfg, err := LoadPipelineWithOptions(root, PipelineLoadOptions{Profile: &profile})
cfg, err := LoadPipelineWithOptions(root, PipelineLoadOptions{Overlay: &profile})
if err != nil {
t.Fatal(err)
}
@@ -130,9 +159,9 @@ func TestNotariusCredentialProfileReplacementAndProvenance(t *testing.T) {
t.Run(credential, func(t *testing.T) {
dir := t.TempDir()
root := writePipelineFixture(t, dir, "pipeline.yml", "whisperx:\n transcribe_url: https://transcription.example.test/transcribe\nnotarius:\n enabled: false\n llm_api_key: "+baseKey+"\n")
overlay := writePipelineFixture(t, dir, "profiles/testing.yml", "notarius:\n llm_api_key: "+credential+"\n")
overlay := writePipelineFixture(t, dir, "overlays/testing.yml", "notarius:\n llm_api_key: "+credential+"\n")
profile := "testing"
cfg, err := LoadPipelineWithOptions(root, PipelineLoadOptions{Profile: &profile})
cfg, err := LoadPipelineWithOptions(root, PipelineLoadOptions{Overlay: &profile})
if err != nil {
t.Fatal(err)
}
@@ -187,8 +216,8 @@ func TestNotariusConfigProfileComposition(t *testing.T) {
root := writePipelineFixture(t, dir, "pipeline.yml", rootYAML)
options := PipelineLoadOptions{}
if test.overlayName != "" {
writePipelineFixture(t, dir, "profiles/"+test.overlayName+".yml", test.overlay)
options.Profile = &test.overlayName
writePipelineFixture(t, dir, "overlays/"+test.overlayName+".yml", test.overlay)
options.Overlay = &test.overlayName
}
cfg, err := LoadPipelineWithOptions(root, options)
if err != nil {
@@ -238,9 +267,9 @@ func TestNotariusCacheControlProfileReplacementAndNullReset(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
dir := t.TempDir()
root := writePipelineFixture(t, dir, "pipeline.yml", "whisperx:\n transcribe_url: https://transcription.example.test/transcribe\nnotarius:\n enabled: false\n resume: false\n chunk_cache: bypass\n")
overlay := writePipelineFixture(t, dir, "profiles/testing.yml", test.overlay)
overlay := writePipelineFixture(t, dir, "overlays/testing.yml", test.overlay)
profile := "testing"
cfg, err := LoadPipelineWithOptions(root, PipelineLoadOptions{Profile: &profile})
cfg, err := LoadPipelineWithOptions(root, PipelineLoadOptions{Overlay: &profile})
if err != nil {
t.Fatal(err)
}
@@ -282,9 +311,9 @@ whisperx:
transcribe_url: https://transcription.example.test/transcribe
`)
writePipelineFixture(t, dir, "artifact.yml", "scriptorium:\n artifacts:\n recap:\n prompt_id: base\n output_path: artifacts/recap.md\n")
writePipelineFixture(t, dir, "profiles/testing.yml", "scriptorium:\n artifacts:\n recap:\n prompt_id: testing\n")
writePipelineFixture(t, dir, "overlays/testing.yml", "scriptorium:\n artifacts:\n recap:\n prompt_id: testing\n")
profile := "testing"
cfg, err := LoadPipelineWithOptions(filepath.Join(dir, "pipeline.yml"), PipelineLoadOptions{Profile: &profile})
cfg, err := LoadPipelineWithOptions(filepath.Join(dir, "pipeline.yml"), PipelineLoadOptions{Overlay: &profile})
if err != nil {
t.Fatal(err)
}

View File

@@ -1,6 +1,7 @@
package manifest
import (
"encoding/json"
"strings"
"time"
@@ -71,13 +72,25 @@ type StageRecord struct {
// EffectiveConfigProvenance records bounded non-secret configuration identity
// for one resolved invocation.
type EffectiveConfigProvenance struct {
SelectedProfile *SelectedProfileProvenance `json:"selected_profile,omitempty"`
SelectedOverlay *SelectedOverlayProvenance `json:"selected_overlay,omitempty"`
EffectiveConfigDigest string `json:"effective_config_digest,omitempty"`
}
// SelectedProfileProvenance identifies an explicitly or default-selected
// pipeline profile without recording profile content.
type SelectedProfileProvenance struct {
// UnmarshalJSON accepts unknown informational provenance from older manifests.
// The enclosing manifest and its execution state remain strictly decoded.
func (p *EffectiveConfigProvenance) UnmarshalJSON(data []byte) error {
type provenance EffectiveConfigProvenance
var decoded provenance
if err := json.Unmarshal(data, &decoded); err != nil {
return err
}
*p = EffectiveConfigProvenance(decoded)
return nil
}
// SelectedOverlayProvenance identifies an explicitly or default-selected
// pipeline overlay without recording overlay content.
type SelectedOverlayProvenance struct {
Name string `json:"name"`
Source string `json:"source"`
}

View File

@@ -34,7 +34,7 @@ func TestLocalStoreCreateSaveLoadRoundTrip(t *testing.T) {
m.S3SessionPrefix = "dnd/campaigns/forsaken/sessions/2026-05-03/"
m.S3RunPrefix = "dnd/campaigns/forsaken/sessions/2026-05-03/runs/20260515T031522Z-a1b2c3d4/"
m.EffectiveConfig = &EffectiveConfigProvenance{
SelectedProfile: &SelectedProfileProvenance{Name: "production", Source: "default"},
SelectedOverlay: &SelectedOverlayProvenance{Name: "production", Source: "default"},
EffectiveConfigDigest: "001122",
}
@@ -57,7 +57,7 @@ func TestLocalStoreCreateSaveLoadRoundTrip(t *testing.T) {
if loaded.RunID != "20260515T031522Z-a1b2c3d4" {
t.Fatalf("RunID = %q, want run id", loaded.RunID)
}
if loaded.EffectiveConfig == nil || loaded.EffectiveConfig.SelectedProfile == nil || loaded.EffectiveConfig.SelectedProfile.Name != "production" || loaded.EffectiveConfig.EffectiveConfigDigest != "001122" {
if loaded.EffectiveConfig == nil || loaded.EffectiveConfig.SelectedOverlay == nil || loaded.EffectiveConfig.SelectedOverlay.Name != "production" || loaded.EffectiveConfig.EffectiveConfigDigest != "001122" {
t.Fatalf("effective config provenance = %#v", loaded.EffectiveConfig)
}
stage, ok := loaded.Stages["prepare"]
@@ -75,6 +75,21 @@ func TestLocalStoreCreateSaveLoadRoundTrip(t *testing.T) {
}
}
func TestLocalStoreLoadsManifestWithHistoricalSelectionProvenance(t *testing.T) {
path := filepath.Join(t.TempDir(), "manifest.json")
data := `{"schema_version":"narratio.session-manifest.v1","session_id":"2026-05-03","created_at":"2026-05-03T10:00:00Z","updated_at":"2026-05-03T10:00:00Z","effective_config":{"selected_profile":{"name":"testing","source":"explicit"},"effective_config_digest":"001122"},"stages":{}}`
if err := os.WriteFile(path, []byte(data), 0o600); err != nil {
t.Fatal(err)
}
loaded, err := (&LocalStore{}).Load(context.Background(), path)
if err != nil {
t.Fatal(err)
}
if loaded.EffectiveConfig == nil || loaded.EffectiveConfig.EffectiveConfigDigest != "001122" || loaded.EffectiveConfig.SelectedOverlay != nil {
t.Fatalf("historical informational provenance = %#v", loaded.EffectiveConfig)
}
}
func TestLocalStorePreservesOptionalArtifactMetadata(t *testing.T) {
store := &LocalStore{}
ctx := context.Background()