From 995354f1a5470be6ba27d9e4ed8a07e3b327755a Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 21 Sep 2026 15:07:36 +0000 Subject: [PATCH] Track Notarius profiles in extraction reuse --- docs/roadmap/implementation.md | 10 +- internal/stage/extract.go | 15 +-- internal/stage/extract_test.go | 93 +++++++++++++++++++ internal/stage/semantic_contracts_delivery.go | 17 +++- .../stage/semantic_contracts_delivery_test.go | 69 ++++++++++++++ 5 files changed, 192 insertions(+), 12 deletions(-) diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index d97cefb..b1b1a37 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Implementation Plan: Notarius Configuration Profile Selection -Status: Implementation in progress; Stage 1 complete. +Status: Implementation in progress; Stages 1-2 complete. ## Scope and Working Instructions @@ -107,7 +107,13 @@ selections as an intermediate implementation. ## Stage 2: Wire Extraction and Profile-Sensitive Reuse -Status: Pending. +Status: Complete. + +Completion marker (2026-09-21): Wired the selected profile into extraction, +added a shared base-selector reuse identity to both existing fingerprints, and +covered request forwarding, force precedence, semantic sensitivity, prior +no-profile encoding, and focused resume validation. Verified with +`go test ./internal/stage ./internal/adapters/notarius ./internal/config`. Preparation: diff --git a/internal/stage/extract.go b/internal/stage/extract.go index 3e6ae2e..e0a619b 100644 --- a/internal/stage/extract.go +++ b/internal/stage/extract.go @@ -146,7 +146,8 @@ func (extractStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (re chunkCache = "refresh" } request := notarius.RunRequest{ - Binary: resolvedBinary, LLMAPIKey: notariusConfig.LLMAPIKey, ConfigPath: configPath, PipelineID: notariusConfig.PipelineID, + Binary: resolvedBinary, LLMAPIKey: notariusConfig.LLMAPIKey, ConfigProfile: notariusConfig.ConfigProfile, + ConfigPath: configPath, PipelineID: notariusConfig.PipelineID, InputPath: inputPath, OutputRoot: outputRoot, WorkingDirectory: workingDirectory, ReceiptPath: receiptPath, LogPath: logPath, Timeout: timeout, Resume: resume, ChunkCache: chunkCache, References: referenceBindings, } @@ -449,10 +450,11 @@ type fingerprintOutput struct { } type fingerprintDocument struct { - PipelineID string `json:"pipeline_id"` - Input artifacts.ExtractionInputIdentity `json:"input"` - References []extractReferenceIdentity `json:"references"` - Outputs []fingerprintOutput `json:"outputs"` + PipelineID string `json:"pipeline_id"` + ConfigProfile string `json:"config_profile,omitempty"` + Input artifacts.ExtractionInputIdentity `json:"input"` + References []extractReferenceIdentity `json:"references"` + Outputs []fingerprintOutput `json:"outputs"` } func extractionFingerprint( @@ -484,7 +486,8 @@ func extractionFingerprint( return sortedReferences[i].Path < sortedReferences[j].Path }) payload, err := json.Marshal(fingerprintDocument{ - PipelineID: cfg.PipelineID, Input: input, References: sortedReferences, Outputs: outputs, + PipelineID: cfg.PipelineID, ConfigProfile: notariusConfigProfileReuseIdentity(cfg.ConfigProfile), + Input: input, References: sortedReferences, Outputs: outputs, }) if err != nil { return "", err diff --git a/internal/stage/extract_test.go b/internal/stage/extract_test.go index ca73867..ec8cadf 100644 --- a/internal/stage/extract_test.go +++ b/internal/stage/extract_test.go @@ -2,6 +2,8 @@ package stage import ( "context" + "crypto/sha256" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -70,6 +72,8 @@ notarius: env, m, fake := setupExtractEnv(t) env.Config.Pipeline.Notarius.LLMAPIKey = loadedPipeline.Notarius.LLMAPIKey + configProfile := "inexpensive" + env.Config.Pipeline.Notarius.ConfigProfile = &configProfile result, err := (extractStage{}).Run(context.Background(), env, m) if err != nil { t.Fatalf("Run() error = %v", err) @@ -102,6 +106,9 @@ notarius: if req.LLMAPIKey != loadedPipeline.Notarius.LLMAPIKey || req.LLMAPIKey.EnvName() != credentialName || !req.LLMAPIKey.IsResolved() { t.Fatal("request did not retain the loaded Notarius credential reference") } + if req.ConfigProfile == nil || *req.ConfigProfile != configProfile { + t.Fatalf("request config profile = %#v, want %q", req.ConfigProfile, configProfile) + } if result.Metadata["reference_count"] != 0 || len(result.Metadata["references"].([]map[string]any)) != 0 { t.Fatalf("reference metadata = count %#v references %#v", result.Metadata["reference_count"], result.Metadata["references"]) } @@ -134,6 +141,8 @@ func TestExtractStageAppliesNotariusCacheControlsAndForcePrecedence(t *testing.T for _, test := range tests { t.Run(test.name, func(t *testing.T) { env, m, fake := setupExtractEnv(t) + configProfile := "inexpensive" + env.Config.Pipeline.Notarius.ConfigProfile = &configProfile env.Config.Pipeline.Notarius.Resume = test.configuredResume env.Config.Pipeline.Notarius.ChunkCache = test.configuredMode env.Force = test.force @@ -148,6 +157,9 @@ func TestExtractStageAppliesNotariusCacheControlsAndForcePrecedence(t *testing.T if request.Resume != test.wantResume || request.ChunkCache != test.wantMode { t.Fatalf("cache controls = resume %v, mode %q; want resume %v, mode %q", request.Resume, request.ChunkCache, test.wantResume, test.wantMode) } + if request.ConfigProfile == nil || *request.ConfigProfile != configProfile { + t.Fatalf("config profile = %#v, want %q", request.ConfigProfile, configProfile) + } }) } } @@ -971,6 +983,60 @@ func TestExtractionFingerprintIsIndependentOfOutputMapOrder(t *testing.T) { } } +func TestExtractionFingerprintBaseSelectorsPreservePriorPayloadShape(t *testing.T) { + input := artifacts.ExtractionInputIdentity{ + Checksum: "checksum", SourceID: artifacts.ArtifactTranscriptFinalTrimmed, + ProducerStage: "trim", OutputKind: artifacts.TranscriptOutputKindFinalTrimmed, + ProducerRunID: "trim-run", Provenance: "manifest.trim.outputs", + } + references := []extractReferenceIdentity{{ + Selector: "party", SourceID: artifactpolicy.SourceInputParty, + Path: "inputs/party.yml", Checksum: "party-checksum", SizeBytes: 34, + }} + cfg := &config.NotariusConfig{ + PipelineID: "pipeline", + Outputs: map[string]config.NotariusOutputConfig{ + "alpha": {LaneID: "a", MediaType: "application/json", SchemaID: "a", SchemaVersion: "v1"}, + }, + } + legacyPayload, err := json.Marshal(struct { + PipelineID string `json:"pipeline_id"` + Input artifacts.ExtractionInputIdentity `json:"input"` + References []extractReferenceIdentity `json:"references"` + Outputs []fingerprintOutput `json:"outputs"` + }{ + PipelineID: cfg.PipelineID, + Input: input, + References: references, + Outputs: []fingerprintOutput{{ + Key: "alpha", LaneID: "a", MediaType: "application/json", SchemaID: "a", SchemaVersion: "v1", + }}, + }) + if err != nil { + t.Fatal(err) + } + legacyDigest := sha256.Sum256(legacyPayload) + want := hex.EncodeToString(legacyDigest[:]) + for _, profile := range []*string{nil, stringPointerForExtractTest(""), stringPointerForExtractTest("default")} { + cfg.ConfigProfile = profile + got, err := extractionFingerprint(cfg, input, references) + if err != nil { + t.Fatal(err) + } + if got != want { + t.Fatalf("base selection fingerprint = %q, want prior no-profile payload digest %q", got, want) + } + } + cfg.ConfigProfile = stringPointerForExtractTest("inexpensive") + named, err := extractionFingerprint(cfg, input, references) + if err != nil { + t.Fatal(err) + } + if named == want { + t.Fatal("named selection retained prior no-profile fingerprint") + } +} + func TestExtractStageResumeValidationAcceptsCurrentImmutableResult(t *testing.T) { env, m, _ := setupExtractEnv(t) env.Config.Pipeline.Notarius.References = map[string]string{ @@ -989,6 +1055,31 @@ func TestExtractStageResumeValidationAcceptsCurrentImmutableResult(t *testing.T) } } +func TestExtractStageResumeValidationTracksConfigProfile(t *testing.T) { + env, m, _ := setupExtractEnv(t) + selected := "inexpensive" + env.Config.Pipeline.Notarius.ConfigProfile = &selected + seedSucceededExtractResult(t, env, m) + + validation, err := (extractStage{}).ValidateResume(context.Background(), env, m) + if err != nil { + t.Fatalf("ValidateResume(same selection) error = %v", err) + } + if !validation.Resumable { + t.Fatalf("same selection validation = %#v, want reusable recorded bundle", validation) + } + + changed := "experimental" + env.Config.Pipeline.Notarius.ConfigProfile = &changed + validation, err = (extractStage{}).ValidateResume(context.Background(), env, m) + if err != nil { + t.Fatalf("ValidateResume(changed selection) error = %v", err) + } + if validation.Resumable || !strings.Contains(validation.Reason, "invocation contract changed") { + t.Fatalf("changed selection validation = %#v, want non-resumable invocation evidence", validation) + } +} + func TestExtractStageResumeValidationRejectsChangedPreparedReferenceIdentity(t *testing.T) { env, m, _ := setupExtractEnv(t) env.Config.Pipeline.Notarius.References = map[string]string{ @@ -1277,6 +1368,8 @@ type extractFixture struct { inputPath string } +func stringPointerForExtractTest(value string) *string { return &value } + func extractFixtureFromEnv(t *testing.T, env *Env, m *manifest.Manifest) extractFixture { t.Helper() paths := sessionPathsForEnv(env, m.SessionID) diff --git a/internal/stage/semantic_contracts_delivery.go b/internal/stage/semantic_contracts_delivery.go index efa6d58..d95f9b4 100644 --- a/internal/stage/semantic_contracts_delivery.go +++ b/internal/stage/semantic_contracts_delivery.go @@ -20,10 +20,18 @@ const ( ) type extractSemanticConfig struct { - Enabled bool `json:"enabled"` - PipelineID string `json:"pipeline_id,omitempty"` - References []extractSemanticReference `json:"references"` - Outputs []extractSemanticOutput `json:"outputs"` + Enabled bool `json:"enabled"` + PipelineID string `json:"pipeline_id,omitempty"` + ConfigProfile string `json:"config_profile,omitempty"` + References []extractSemanticReference `json:"references"` + Outputs []extractSemanticOutput `json:"outputs"` +} + +func notariusConfigProfileReuseIdentity(profile *string) string { + if profile == nil || *profile == "" || *profile == "default" { + return "" + } + return *profile } type extractSemanticReference struct { @@ -77,6 +85,7 @@ func (extractStage) SemanticConfigFingerprint(env *Env) (manifest.SemanticConfig } payload.Enabled = true payload.PipelineID = strings.TrimSpace(cfg.PipelineID) + payload.ConfigProfile = notariusConfigProfileReuseIdentity(cfg.ConfigProfile) for rawSelector, rawSourceID := range cfg.References { selector, err := notariusref.NormalizeSelector(rawSelector) if err != nil { diff --git a/internal/stage/semantic_contracts_delivery_test.go b/internal/stage/semantic_contracts_delivery_test.go index a04d12b..ec9dd4d 100644 --- a/internal/stage/semantic_contracts_delivery_test.go +++ b/internal/stage/semantic_contracts_delivery_test.go @@ -9,6 +9,7 @@ import ( configkit "gitea.maximumdirect.net/eric/ConfigKit" "gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy" "gitea.maximumdirect.net/eric/narratio/internal/config" + "gitea.maximumdirect.net/eric/narratio/internal/manifest" ) func testSecretReference(name string) *configkit.Secret { @@ -96,6 +97,74 @@ func TestExtractSemanticConfigSensitivityAndOperationalExclusions(t *testing.T) } }) } + + profilePointer := func(value string) *string { return &value } + baseFingerprints := make([]manifest.SemanticConfigFingerprint, 0, 3) + for _, profile := range []*string{nil, profilePointer(""), profilePointer("default")} { + env := downstreamSemanticEnv() + env.Config.Pipeline.Notarius.ConfigProfile = profile + fingerprint, err := (extractStage{}).SemanticConfigFingerprint(env) + if err != nil { + t.Fatal(err) + } + baseFingerprints = append(baseFingerprints, fingerprint) + } + for index := 1; index < len(baseFingerprints); index++ { + if !baseFingerprints[0].Equal(baseFingerprints[index]) { + t.Fatal("nil, empty, and default Notarius selections must share base reuse evidence") + } + } + profiled := downstreamSemanticEnv() + profiled.Config.Pipeline.Notarius.ConfigProfile = profilePointer("inexpensive") + firstProfile, err := (extractStage{}).SemanticConfigFingerprint(profiled) + if err != nil { + t.Fatal(err) + } + if baseFingerprints[0].Equal(firstProfile) { + t.Fatal("base to named Notarius selection retained semantic fingerprint") + } + unchangedProfile, err := (extractStage{}).SemanticConfigFingerprint(profiled) + if err != nil { + t.Fatal(err) + } + if !firstProfile.Equal(unchangedProfile) { + t.Fatal("unchanged Notarius selection changed semantic fingerprint") + } + profiled.Config.Pipeline.Notarius.ConfigProfile = profilePointer("experimental") + secondProfile, err := (extractStage{}).SemanticConfigFingerprint(profiled) + if err != nil { + t.Fatal(err) + } + if firstProfile.Equal(secondProfile) { + t.Fatal("named to different named Notarius selection retained semantic fingerprint") + } + profiled.Config.Pipeline.Notarius.ConfigProfile = nil + returnedToBase, err := (extractStage{}).SemanticConfigFingerprint(profiled) + if err != nil { + t.Fatal(err) + } + if !baseFingerprints[0].Equal(returnedToBase) { + t.Fatal("named to base Notarius selection did not restore base semantic evidence") + } + if secondProfile.Equal(returnedToBase) { + t.Fatal("named to base Notarius selection retained named semantic evidence") + } + disabledBase := downstreamSemanticEnv() + disabledBase.Config.Pipeline.Notarius.Enabled = false + disabledNamed := downstreamSemanticEnv() + disabledNamed.Config.Pipeline.Notarius.Enabled = false + disabledNamed.Config.Pipeline.Notarius.ConfigProfile = profilePointer("ignored") + disabledBaseFingerprint, err := (extractStage{}).SemanticConfigFingerprint(disabledBase) + if err != nil { + t.Fatal(err) + } + disabledNamedFingerprint, err := (extractStage{}).SemanticConfigFingerprint(disabledNamed) + if err != nil { + t.Fatal(err) + } + if !disabledBaseFingerprint.Equal(disabledNamedFingerprint) { + t.Fatal("disabled extraction became sensitive to unused Notarius selection") + } } func TestExtractResumeValidationExcludesOperationalSettings(t *testing.T) {