Track Notarius profiles in extraction reuse
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user