Track Audita v2 selections in polish resume evidence

This commit is contained in:
2026-09-25 15:55:38 +00:00
parent 569f467393
commit 2d167d0d67
7 changed files with 128 additions and 23 deletions

View File

@@ -35,19 +35,18 @@ are recorded as diagnostics, not artifact outputs.
## Resume Semantics
The versioned semantic fingerprint covers the Audita service endpoint, model,
validation model, module set, transcript description, output schema, selected
external configuration path, and canonical polished-transcript identity. Module
ordering is normalized because the configured modules form a set. Audita's
executable, timeouts, concurrency, report and debug behavior, work retention,
and resolved credential value are operational and do not invalidate a
successful result. The public credential reference name may participate in
configuration provenance but never exposes its resolved value.
Version 2 semantic evidence covers Audita's config path, downstream overlay,
proposal and validation profile selectors, ordered module selection (including
the distinction between omission and an explicit empty list), transcript
description, output schema, and canonical polished-transcript identity.
Credentials, executable path, timeout, and diagnostics are operational and do
not invalidate a successful result. Public credential reference names may
participate in configuration provenance but never expose resolved values.
Narratio can fingerprint a selected model, module, or configuration identifier,
but it cannot inspect content that Audita privately resolves behind that stable
identifier. Force `polish` after changing such private content without changing
its identifier.
Narratio fingerprints selections, not the contents of Audita-owned config or
profiles. Force `polish` after changing those files behind unchanged selections.
When polish is selected, older semantic evidence is reconsidered and the stage
reruns; a downstream-only invocation does not eagerly run polish.
## Related Contracts And Tests

View File

@@ -354,7 +354,10 @@ promote an incidental transcript.
## Stage 6: Complete semantic evidence and resume behavior
Status: Not started.
Status: Complete (2026-09-25).
Verification: `go test ./internal/stage ./internal/app`,
`go test -race ./internal/stage ./internal/app`, and `git diff --check` passed.
Preparation: `internal/stage/semantic_contracts_refinement.go`, semantic
fingerprint tests, app runner semantic comparison tests, and extraction semantic

View File

@@ -373,7 +373,7 @@ func TestAssembledSplitBundleProfileSwitchRecordsProvenanceAndLimitsReuse(t *tes
if err != nil {
t.Fatalf("switch to testing profile: %v", err)
}
if got, want := testingRuns, []int{0, 0, 0, 0, 0, 0, 0}; !reflect.DeepEqual(got, want) {
if got, want := testingRuns, []int{0, 0, 0, 1, 1, 1, 1}; !reflect.DeepEqual(got, want) {
t.Fatalf("testing profile runs = %v, want %v", got, want)
}
assertAssembledProvenance(t, store, testingSummary, "testing", config.EffectivePipelineDigest(testingCfg.Pipeline))

View File

@@ -5,6 +5,7 @@ import (
"crypto/sha256"
"encoding/hex"
"errors"
"reflect"
"strings"
"testing"
"time"
@@ -424,6 +425,69 @@ func TestRefinementSemanticChangesRespectDependencyBranches(t *testing.T) {
}
}
func TestOldPolishSemanticEvidenceIsReconsideredOnlyWhenSelected(t *testing.T) {
cfg := testConfig(t)
names := []string{"prepare", "transcribe", "merge", "polish", "normalize", "trim", "render"}
seed := manifest.New(cfg.Session.SessionID, time.Now().UTC())
for _, candidate := range stage.All() {
seed.MarkStageSucceeded(candidate.Name(), time.Now().UTC(), nil)
}
providers := make([]stage.SemanticConfigFingerprinter, len(names))
runs := make([]int, len(names))
selected := make([]stage.Stage, 0, len(names))
for index, name := range names {
providers[index] = canonicalSemanticProvider(t, name)
fingerprint, err := providers[index].SemanticConfigFingerprint(&stage.Env{Config: cfg})
if err != nil {
t.Fatal(err)
}
if name == "polish" {
fingerprint.Version--
}
seed.Stages[name].SemanticConfig = &fingerprint
selected = append(selected, semanticContractRunStub{name: name, provider: providers[index], runs: &runs[index]})
}
store := &manifest.LocalStore{}
if err := store.Save(context.Background(), manifestPathFor(cfg), seed); err != nil {
t.Fatal(err)
}
if _, err := executeStages(context.Background(), cfg, selected[len(selected)-1:], RunOptions{}); err != nil {
t.Fatal(err)
}
if runs[3] != 0 {
t.Fatal("downstream-only execution invoked polish")
}
loaded, err := store.Load(context.Background(), manifestPathFor(cfg))
if err != nil {
t.Fatal(err)
}
if loaded.Stages["polish"].SemanticConfig.Version != seed.Stages["polish"].SemanticConfig.Version {
t.Fatal("downstream-only execution rewrote unselected polish evidence")
}
if _, err := executeStages(context.Background(), cfg, selected, RunOptions{}); err != nil {
t.Fatal(err)
}
if want := []int{0, 0, 0, 1, 1, 1, 1}; !reflect.DeepEqual(runs, want) {
t.Fatalf("runs = %v, want %v", runs, want)
}
loaded, err = store.Load(context.Background(), manifestPathFor(cfg))
if err != nil {
t.Fatal(err)
}
if loaded.Stages["polish"].SemanticConfig.Version != 2 || loaded.Stages["extract"].Status != manifest.StatusStale {
t.Fatalf("old evidence was not replaced with normal dependent invalidation: polish=%#v extract=%#v", loaded.Stages["polish"], loaded.Stages["extract"])
}
for index := range runs {
runs[index] = 0
}
if _, err := executeStages(context.Background(), cfg, selected, RunOptions{}); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(runs, make([]int, len(names))) {
t.Fatalf("unchanged new evidence reran stages: %v", runs)
}
}
func TestDeliverySemanticChangesInvalidateOnlyTheirFixedDependents(t *testing.T) {
t.Run("extract contract", func(t *testing.T) {
cfg := testConfig(t)

View File

@@ -192,6 +192,7 @@ func TestExtractSemanticConfigExcludesLoadedCredentialChanges(t *testing.T) {
first := loadNotariusCredentialForSemanticTest(t, firstName, "first-value")
rotated := loadNotariusCredentialForSemanticTest(t, firstName, "rotated-value")
renamed := loadNotariusCredentialForSemanticTest(t, secondName, "second-value")
third := loadNotariusCredentialForSemanticTest(t, "NARRATIO_SEMANTIC_NOTARIUS_KEY_THREE", "third-value")
baseline := downstreamSemanticEnv()
baseline.Config.Pipeline.Notarius.Credentials = []configkit.Secret{*first}
@@ -221,6 +222,14 @@ func TestExtractSemanticConfigExcludesLoadedCredentialChanges(t *testing.T) {
}
})
}
baseline.Config.Pipeline.Notarius.Credentials = []configkit.Secret{*renamed, *third}
multiple, err := (extractStage{}).SemanticConfigFingerprint(baseline)
if err != nil {
t.Fatal(err)
}
if !want.Equal(multiple) {
t.Fatal("multiple credential references altered extract semantic fingerprint")
}
}
func loadNotariusCredentialForSemanticTest(t *testing.T, name, value string) *configkit.Secret {

View File

@@ -2,7 +2,6 @@ package stage
import (
"fmt"
"sort"
"strings"
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
@@ -11,7 +10,7 @@ import (
)
const (
polishSemanticConfigVersion = 1
polishSemanticConfigVersion = 2
normalizeSemanticConfigVersion = 1
trimSemanticConfigVersion = 1
renderSemanticConfigVersion = 1
@@ -19,9 +18,12 @@ const (
type polishSemanticConfig struct {
Modules []string `json:"modules"`
Overlay string `json:"overlay"`
ProposalProfile string `json:"proposal_profile"`
ValidationProfile string `json:"validation_profile"`
TranscriptDescription string `json:"transcript_description"`
OutputSchema string `json:"output_schema"`
ExternalConfig string `json:"external_config"`
ConfigPath string `json:"config_path"`
OutputIdentity string `json:"output_identity"`
}
@@ -65,16 +67,21 @@ func (polishStage) SemanticConfigFingerprint(env *Env) (manifest.SemanticConfigF
return manifest.SemanticConfigFingerprint{}, fmt.Errorf("polish semantic configuration requires resolved pipeline config")
}
audita := env.Config.Pipeline.Audita
modules := append([]string(nil), audita.Modules...)
var modules []string
if audita.Modules != nil {
modules = append([]string{}, audita.Modules...)
}
for index := range modules {
modules[index] = strings.TrimSpace(modules[index])
}
sort.Strings(modules)
return FingerprintSemanticConfig(polishSemanticConfigVersion, polishSemanticConfig{
Modules: modules,
TranscriptDescription: strings.TrimSpace(audita.TranscriptDescription),
OutputSchema: strings.TrimSpace(audita.OutputSchema),
ExternalConfig: strings.TrimSpace(audita.ConfigPath),
Overlay: audita.Overlay,
ProposalProfile: audita.ProposalProfile,
ValidationProfile: audita.ValidationProfile,
TranscriptDescription: audita.TranscriptDescription,
OutputSchema: audita.OutputSchema,
ConfigPath: audita.ConfigPath,
OutputIdentity: artifacts.ArtifactTranscriptPolished,
})
}

View File

@@ -13,6 +13,13 @@ func TestPolishSemanticConfigSensitivity(t *testing.T) {
mutate func(*Env)
}{
{name: "modules", mutate: func(env *Env) { env.Config.Pipeline.Audita.Modules = []string{"terms", "speakers", "style"} }},
{name: "module order", mutate: func(env *Env) { env.Config.Pipeline.Audita.Modules = []string{"terms", "speakers"} }},
{name: "module repetition", mutate: func(env *Env) { env.Config.Pipeline.Audita.Modules = []string{"speakers", "terms", "terms"} }},
{name: "modules omitted", mutate: func(env *Env) { env.Config.Pipeline.Audita.Modules = nil }},
{name: "modules empty", mutate: func(env *Env) { env.Config.Pipeline.Audita.Modules = []string{} }},
{name: "overlay", mutate: func(env *Env) { env.Config.Pipeline.Audita.Overlay = "testing" }},
{name: "proposal profile", mutate: func(env *Env) { env.Config.Pipeline.Audita.ProposalProfile = "draft" }},
{name: "validation profile", mutate: func(env *Env) { env.Config.Pipeline.Audita.ValidationProfile = "review" }},
{name: "description", mutate: func(env *Env) { env.Config.Pipeline.Audita.TranscriptDescription = "live campaign" }},
{name: "schema", mutate: func(env *Env) { env.Config.Pipeline.Audita.OutputSchema = "audita.transcript.v2" }},
{name: "external config", mutate: func(env *Env) { env.Config.Pipeline.Audita.ConfigPath = "configs/other.yml" }},
@@ -22,7 +29,6 @@ func TestPolishSemanticConfigSensitivity(t *testing.T) {
name string
mutate func(*Env)
}{
{name: "module order", mutate: func(env *Env) { env.Config.Pipeline.Audita.Modules = []string{"terms", "speakers"} }},
{name: "runner settings", mutate: func(env *Env) {
env.Config.Pipeline.Audita.Binary = "/opt/audita"
env.Config.Pipeline.Audita.Timeout = "1h"
@@ -31,6 +37,23 @@ func TestPolishSemanticConfigSensitivity(t *testing.T) {
})
}
func TestPolishSemanticConfigDistinguishesOmittedAndExplicitlyEmptyModules(t *testing.T) {
env := refinementSemanticEnv()
env.Config.Pipeline.Audita.Modules = nil
omitted, err := (polishStage{}).SemanticConfigFingerprint(env)
if err != nil {
t.Fatal(err)
}
env.Config.Pipeline.Audita.Modules = []string{}
empty, err := (polishStage{}).SemanticConfigFingerprint(env)
if err != nil {
t.Fatal(err)
}
if omitted.Equal(empty) {
t.Fatal("omitted and explicitly empty module selections have equal semantic evidence")
}
}
func TestNormalizeSemanticConfigSensitivity(t *testing.T) {
assertRefinementChanges(t, normalizeStage{}, []struct {
name string