Track Audita v2 selections in polish resume evidence
This commit is contained in:
@@ -35,19 +35,18 @@ are recorded as diagnostics, not artifact outputs.
|
|||||||
|
|
||||||
## Resume Semantics
|
## Resume Semantics
|
||||||
|
|
||||||
The versioned semantic fingerprint covers the Audita service endpoint, model,
|
Version 2 semantic evidence covers Audita's config path, downstream overlay,
|
||||||
validation model, module set, transcript description, output schema, selected
|
proposal and validation profile selectors, ordered module selection (including
|
||||||
external configuration path, and canonical polished-transcript identity. Module
|
the distinction between omission and an explicit empty list), transcript
|
||||||
ordering is normalized because the configured modules form a set. Audita's
|
description, output schema, and canonical polished-transcript identity.
|
||||||
executable, timeouts, concurrency, report and debug behavior, work retention,
|
Credentials, executable path, timeout, and diagnostics are operational and do
|
||||||
and resolved credential value are operational and do not invalidate a
|
not invalidate a successful result. Public credential reference names may
|
||||||
successful result. The public credential reference name may participate in
|
participate in configuration provenance but never expose resolved values.
|
||||||
configuration provenance but never exposes its resolved value.
|
|
||||||
|
|
||||||
Narratio can fingerprint a selected model, module, or configuration identifier,
|
Narratio fingerprints selections, not the contents of Audita-owned config or
|
||||||
but it cannot inspect content that Audita privately resolves behind that stable
|
profiles. Force `polish` after changing those files behind unchanged selections.
|
||||||
identifier. Force `polish` after changing such private content without changing
|
When polish is selected, older semantic evidence is reconsidered and the stage
|
||||||
its identifier.
|
reruns; a downstream-only invocation does not eagerly run polish.
|
||||||
|
|
||||||
## Related Contracts And Tests
|
## Related Contracts And Tests
|
||||||
|
|
||||||
|
|||||||
@@ -354,7 +354,10 @@ promote an incidental transcript.
|
|||||||
|
|
||||||
## Stage 6: Complete semantic evidence and resume behavior
|
## 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
|
Preparation: `internal/stage/semantic_contracts_refinement.go`, semantic
|
||||||
fingerprint tests, app runner semantic comparison tests, and extraction semantic
|
fingerprint tests, app runner semantic comparison tests, and extraction semantic
|
||||||
|
|||||||
@@ -373,7 +373,7 @@ func TestAssembledSplitBundleProfileSwitchRecordsProvenanceAndLimitsReuse(t *tes
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("switch to testing profile: %v", err)
|
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)
|
t.Fatalf("testing profile runs = %v, want %v", got, want)
|
||||||
}
|
}
|
||||||
assertAssembledProvenance(t, store, testingSummary, "testing", config.EffectivePipelineDigest(testingCfg.Pipeline))
|
assertAssembledProvenance(t, store, testingSummary, "testing", config.EffectivePipelineDigest(testingCfg.Pipeline))
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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) {
|
func TestDeliverySemanticChangesInvalidateOnlyTheirFixedDependents(t *testing.T) {
|
||||||
t.Run("extract contract", func(t *testing.T) {
|
t.Run("extract contract", func(t *testing.T) {
|
||||||
cfg := testConfig(t)
|
cfg := testConfig(t)
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ func TestExtractSemanticConfigExcludesLoadedCredentialChanges(t *testing.T) {
|
|||||||
first := loadNotariusCredentialForSemanticTest(t, firstName, "first-value")
|
first := loadNotariusCredentialForSemanticTest(t, firstName, "first-value")
|
||||||
rotated := loadNotariusCredentialForSemanticTest(t, firstName, "rotated-value")
|
rotated := loadNotariusCredentialForSemanticTest(t, firstName, "rotated-value")
|
||||||
renamed := loadNotariusCredentialForSemanticTest(t, secondName, "second-value")
|
renamed := loadNotariusCredentialForSemanticTest(t, secondName, "second-value")
|
||||||
|
third := loadNotariusCredentialForSemanticTest(t, "NARRATIO_SEMANTIC_NOTARIUS_KEY_THREE", "third-value")
|
||||||
|
|
||||||
baseline := downstreamSemanticEnv()
|
baseline := downstreamSemanticEnv()
|
||||||
baseline.Config.Pipeline.Notarius.Credentials = []configkit.Secret{*first}
|
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 {
|
func loadNotariusCredentialForSemanticTest(t *testing.T, name, value string) *configkit.Secret {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package stage
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||||
@@ -11,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
polishSemanticConfigVersion = 1
|
polishSemanticConfigVersion = 2
|
||||||
normalizeSemanticConfigVersion = 1
|
normalizeSemanticConfigVersion = 1
|
||||||
trimSemanticConfigVersion = 1
|
trimSemanticConfigVersion = 1
|
||||||
renderSemanticConfigVersion = 1
|
renderSemanticConfigVersion = 1
|
||||||
@@ -19,9 +18,12 @@ const (
|
|||||||
|
|
||||||
type polishSemanticConfig struct {
|
type polishSemanticConfig struct {
|
||||||
Modules []string `json:"modules"`
|
Modules []string `json:"modules"`
|
||||||
|
Overlay string `json:"overlay"`
|
||||||
|
ProposalProfile string `json:"proposal_profile"`
|
||||||
|
ValidationProfile string `json:"validation_profile"`
|
||||||
TranscriptDescription string `json:"transcript_description"`
|
TranscriptDescription string `json:"transcript_description"`
|
||||||
OutputSchema string `json:"output_schema"`
|
OutputSchema string `json:"output_schema"`
|
||||||
ExternalConfig string `json:"external_config"`
|
ConfigPath string `json:"config_path"`
|
||||||
OutputIdentity string `json:"output_identity"`
|
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")
|
return manifest.SemanticConfigFingerprint{}, fmt.Errorf("polish semantic configuration requires resolved pipeline config")
|
||||||
}
|
}
|
||||||
audita := env.Config.Pipeline.Audita
|
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 {
|
for index := range modules {
|
||||||
modules[index] = strings.TrimSpace(modules[index])
|
modules[index] = strings.TrimSpace(modules[index])
|
||||||
}
|
}
|
||||||
sort.Strings(modules)
|
|
||||||
return FingerprintSemanticConfig(polishSemanticConfigVersion, polishSemanticConfig{
|
return FingerprintSemanticConfig(polishSemanticConfigVersion, polishSemanticConfig{
|
||||||
Modules: modules,
|
Modules: modules,
|
||||||
TranscriptDescription: strings.TrimSpace(audita.TranscriptDescription),
|
Overlay: audita.Overlay,
|
||||||
OutputSchema: strings.TrimSpace(audita.OutputSchema),
|
ProposalProfile: audita.ProposalProfile,
|
||||||
ExternalConfig: strings.TrimSpace(audita.ConfigPath),
|
ValidationProfile: audita.ValidationProfile,
|
||||||
|
TranscriptDescription: audita.TranscriptDescription,
|
||||||
|
OutputSchema: audita.OutputSchema,
|
||||||
|
ConfigPath: audita.ConfigPath,
|
||||||
OutputIdentity: artifacts.ArtifactTranscriptPolished,
|
OutputIdentity: artifacts.ArtifactTranscriptPolished,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,13 @@ func TestPolishSemanticConfigSensitivity(t *testing.T) {
|
|||||||
mutate func(*Env)
|
mutate func(*Env)
|
||||||
}{
|
}{
|
||||||
{name: "modules", mutate: func(env *Env) { env.Config.Pipeline.Audita.Modules = []string{"terms", "speakers", "style"} }},
|
{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: "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: "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" }},
|
{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
|
name string
|
||||||
mutate func(*Env)
|
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) {
|
{name: "runner settings", mutate: func(env *Env) {
|
||||||
env.Config.Pipeline.Audita.Binary = "/opt/audita"
|
env.Config.Pipeline.Audita.Binary = "/opt/audita"
|
||||||
env.Config.Pipeline.Audita.Timeout = "1h"
|
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) {
|
func TestNormalizeSemanticConfigSensitivity(t *testing.T) {
|
||||||
assertRefinementChanges(t, normalizeStage{}, []struct {
|
assertRefinementChanges(t, normalizeStage{}, []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
Reference in New Issue
Block a user