Adopt Audita v2 configuration and subprocess contract

This commit is contained in:
2026-09-25 15:45:02 +00:00
parent 7e0151ef93
commit 77d9d57937
26 changed files with 522 additions and 1260 deletions

View File

@@ -165,21 +165,16 @@ type SeriatimEnvConfig struct {
// AuditaConfig configures audita adapter settings.
type AuditaConfig struct {
Binary string `yaml:"binary" json:"binary"`
Timeout string `yaml:"timeout" json:"timeout"`
LLMAPIKey *configkit.Secret `yaml:"llm_api_key,omitempty" json:"llm_api_key,omitempty"`
Modules []string `yaml:"modules" json:"modules"`
BaseURL string `yaml:"base_url" json:"base_url"`
Model string `yaml:"model" json:"model"`
TotalLLMConcurrency *int `yaml:"total_llm_concurrency" json:"total_llm_concurrency"`
ProposalLLMConcurrency *int `yaml:"proposal_llm_concurrency" json:"proposal_llm_concurrency"`
ValidationModel string `yaml:"validation_model" json:"validation_model"`
ValidationLLMConcurrency *int `yaml:"validation_llm_concurrency" json:"validation_llm_concurrency"`
TranscriptDescription string `yaml:"transcript_description" json:"transcript_description"`
ConfigPath string `yaml:"config_path" json:"config_path"`
OutputSchema string `yaml:"output_schema" json:"output_schema"`
WorkDirRetention string `yaml:"work_dir_retention" json:"work_dir_retention"`
Report *bool `yaml:"report" json:"report"`
Binary string `yaml:"binary" json:"binary"`
Timeout string `yaml:"timeout" json:"timeout"`
Credentials []configkit.Secret `yaml:"credentials,omitempty" json:"credentials,omitempty"`
Modules []string `yaml:"modules" json:"modules"`
Overlay string `yaml:"overlay" json:"overlay"`
ProposalProfile string `yaml:"proposal_profile" json:"proposal_profile"`
ValidationProfile string `yaml:"validation_profile" json:"validation_profile"`
TranscriptDescription string `yaml:"transcript_description" json:"transcript_description"`
ConfigPath string `yaml:"config_path" json:"config_path"`
OutputSchema string `yaml:"output_schema" json:"output_schema"`
}
// NormalizeConfig configures normalize-stage transcript schema/output behavior.

View File

@@ -21,32 +21,25 @@ func (cfg StorageS3Config) MarshalYAML() (any, error) {
}, nil
}
// MarshalYAML exposes only the public Audita credential reference name.
// MarshalYAML exposes only public Audita credential reference names.
func (cfg AuditaConfig) MarshalYAML() (any, error) {
type safeAudita struct {
Binary string `yaml:"binary"`
Timeout string `yaml:"timeout"`
LLMAPIKey *string `yaml:"llm_api_key,omitempty"`
Modules []string `yaml:"modules"`
BaseURL string `yaml:"base_url"`
Model string `yaml:"model"`
TotalLLMConcurrency *int `yaml:"total_llm_concurrency"`
ProposalLLMConcurrency *int `yaml:"proposal_llm_concurrency"`
ValidationModel string `yaml:"validation_model"`
ValidationLLMConcurrency *int `yaml:"validation_llm_concurrency"`
TranscriptDescription string `yaml:"transcript_description"`
ConfigPath string `yaml:"config_path"`
OutputSchema string `yaml:"output_schema"`
WorkDirRetention string `yaml:"work_dir_retention"`
Report *bool `yaml:"report"`
Binary string `yaml:"binary"`
Timeout string `yaml:"timeout"`
Credentials []string `yaml:"credentials,omitempty"`
Modules []string `yaml:"modules"`
Overlay string `yaml:"overlay"`
ProposalProfile string `yaml:"proposal_profile"`
ValidationProfile string `yaml:"validation_profile"`
TranscriptDescription string `yaml:"transcript_description"`
ConfigPath string `yaml:"config_path"`
OutputSchema string `yaml:"output_schema"`
}
return safeAudita{
Binary: cfg.Binary, Timeout: cfg.Timeout, LLMAPIKey: secretReferenceName(cfg.LLMAPIKey),
Modules: cfg.Modules, BaseURL: cfg.BaseURL, Model: cfg.Model,
TotalLLMConcurrency: cfg.TotalLLMConcurrency, ProposalLLMConcurrency: cfg.ProposalLLMConcurrency,
ValidationModel: cfg.ValidationModel, ValidationLLMConcurrency: cfg.ValidationLLMConcurrency,
TranscriptDescription: cfg.TranscriptDescription, ConfigPath: cfg.ConfigPath,
OutputSchema: cfg.OutputSchema, WorkDirRetention: cfg.WorkDirRetention, Report: cfg.Report,
Binary: cfg.Binary, Timeout: cfg.Timeout, Credentials: secretReferenceNames(cfg.Credentials),
Modules: cfg.Modules, Overlay: cfg.Overlay, ProposalProfile: cfg.ProposalProfile,
ValidationProfile: cfg.ValidationProfile, TranscriptDescription: cfg.TranscriptDescription,
ConfigPath: cfg.ConfigPath, OutputSchema: cfg.OutputSchema,
}, nil
}

View File

@@ -30,7 +30,6 @@ const (
DefaultAuditaBinary = "audita"
DefaultAuditaTimeout = "3h"
DefaultAuditaReport = true
DefaultNotariusBinary = "notarius"
DefaultNotariusTimeout = "3h"
// MaxNotariusReferenceBindings is the maximum number of CLI reference

View File

@@ -93,7 +93,7 @@ func TestCredentialValuesNeverEnterEffectiveProjectionOrDigest(t *testing.T) {
t.Setenv("NARRATIO_TEST_SECRET_KEY", firstCanary)
t.Setenv("NARRATIO_TEST_AUDITA_KEY", firstCanary)
t.Setenv("NARRATIO_TEST_NOTARIUS_KEY", firstCanary)
pipelineYAML := strings.Replace(testPipelineBaseYAML, "audita:\n binary: audita\n", "audita:\n binary: audita\n llm_api_key: NARRATIO_TEST_AUDITA_KEY\n", 1) + `
pipelineYAML := strings.Replace(testPipelineBaseYAML, "audita:\n binary: audita\n", "audita:\n binary: audita\n credentials: [NARRATIO_TEST_AUDITA_KEY]\n", 1) + `
storage:
backend: s3
s3:
@@ -375,10 +375,10 @@ func TestExamplesLoadAndValidate(t *testing.T) {
if err := Validate(cfg); err != nil {
t.Fatalf("validate split bundle: %v", err)
}
if cfg.Pipeline.Audita.Model != profile.model {
t.Fatalf("audita model = %q, want %q", cfg.Pipeline.Audita.Model, profile.model)
if cfg.Pipeline.Audita.ProposalProfile != profile.model {
t.Fatalf("audita proposal profile = %q, want %q", cfg.Pipeline.Audita.ProposalProfile, profile.model)
}
if cfg.Pipeline.Audita.LLMAPIKey != nil || cfg.Pipeline.Storage.Backend != StorageBackendLocal || cfg.Pipeline.Storage.S3 != nil {
if len(cfg.Pipeline.Audita.Credentials) != 0 || 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)
}
selectedOverlay, selected := SelectedPipelineOverlay(cfg.Pipeline)

View File

@@ -132,7 +132,7 @@ func pipelineConfigDefaults() PipelineConfig {
OutputSchema: DefaultSeriatimOutputSchema, CoalesceGap: float64Ptr(DefaultSeriatimCoalesceGap),
Report: boolPtr(DefaultSeriatimReport),
},
Audita: AuditaConfig{Binary: DefaultAuditaBinary, Timeout: DefaultAuditaTimeout, Report: boolPtr(DefaultAuditaReport)},
Audita: AuditaConfig{Binary: DefaultAuditaBinary, Timeout: DefaultAuditaTimeout},
Normalize: &NormalizeConfig{OutputPath: DefaultNormalizeOutputPath, OutputSchema: DefaultNormalizeOutputSchema, Report: boolPtr(DefaultNormalizeReport)},
Trim: &TrimConfig{
Enabled: boolPtr(DefaultTrimEnabled), OutputPath: DefaultTrimOutputPath,

View File

@@ -129,24 +129,71 @@ func TestPipelineOverlayDoesNotRequireUnusedBaseCredentials(t *testing.T) {
for _, credential := range []string{testingKey, "null"} {
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, "overlays/testing.yml", "audita:\n llm_api_key: "+credential+"\n")
root := writePipelineFixture(t, dir, "pipeline.yml", "whisperx:\n transcribe_url: https://transcription.example.test/transcribe\naudita:\n credentials: ["+production+"]\n")
selected := "[" + credential + "]"
if credential == "null" {
selected = credential
}
writePipelineFixture(t, dir, "overlays/testing.yml", "audita:\n credentials: "+selected+"\n")
profile := "testing"
cfg, err := LoadPipelineWithOptions(root, PipelineLoadOptions{Overlay: &profile})
if err != nil {
t.Fatal(err)
}
if credential == "null" {
if cfg.Audita.LLMAPIKey != nil {
if len(cfg.Audita.Credentials) != 0 {
t.Fatal("profile did not remove the base credential")
}
} else if cfg.Audita.LLMAPIKey == nil || cfg.Audita.LLMAPIKey.EnvName() != testingKey || !cfg.Audita.LLMAPIKey.IsResolved() {
} else if len(cfg.Audita.Credentials) != 1 || cfg.Audita.Credentials[0].EnvName() != testingKey || !cfg.Audita.Credentials[0].IsResolved() {
t.Fatal("profile credential was not resolved")
}
})
}
}
func TestAuditaModulesPreserveOmittedEmptyAndReplacementSelections(t *testing.T) {
for _, test := range []struct {
name, base, overlay string
wantNil bool
want []string
}{
{name: "omitted", wantNil: true},
{name: "null", base: " modules: null\n", wantNil: true},
{name: "empty", base: " modules: []\n", want: []string{}},
{name: "overlay empty", base: " modules: [glossary]\n", overlay: "audita:\n modules: []\n", want: []string{}},
{name: "overlay order", base: " modules: []\n", overlay: "audita:\n modules: [grammar, glossary, grammar]\n", want: []string{"grammar", "glossary", "grammar"}},
} {
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\naudita:\n binary: audita\n"+test.base)
options := PipelineLoadOptions{}
if test.overlay != "" {
writePipelineFixture(t, dir, "overlays/testing.yml", test.overlay)
selection := "testing"
options.Overlay = &selection
}
cfg, err := LoadPipelineWithOptions(root, options)
if err != nil {
t.Fatal(err)
}
if (cfg.Audita.Modules == nil) != test.wantNil || !reflect.DeepEqual(cfg.Audita.Modules, test.want) {
t.Fatalf("modules = %#v, want %#v (nil=%v)", cfg.Audita.Modules, test.want, test.wantNil)
}
})
}
}
func TestAuditaRejectsRetiredConfigurationFields(t *testing.T) {
for _, field := range []string{"llm_api_key", "base_url", "model", "validation_model", "total_llm_concurrency", "proposal_llm_concurrency", "validation_llm_concurrency", "work_dir_retention", "report"} {
t.Run(field, func(t *testing.T) {
root := writePipelineFixture(t, t.TempDir(), "pipeline.yml", "whisperx:\n transcribe_url: https://transcription.example.test/transcribe\naudita:\n "+field+": retired\n")
if _, err := LoadPipeline(root); err == nil || !strings.Contains(err.Error(), "decode.unknown_field") {
t.Fatalf("error = %v, want unknown field", err)
}
})
}
}
func TestNotariusCredentialOverlayReplacementAndProvenance(t *testing.T) {
const baseKey = "NARRATIO_NOTARIUS_UNUSED_BASE_KEY"
const profileKey = "NARRATIO_NOTARIUS_PROFILE_KEY"

View File

@@ -176,24 +176,6 @@ func validateAudita(cfg AuditaConfig) error {
return fmt.Errorf("pipeline.audita.modules[%d] must be one of: glossary, homophones, spoken_word, grammar", i)
}
}
if strings.TrimSpace(cfg.BaseURL) != "" {
u, err := url.Parse(cfg.BaseURL)
if err != nil || u.Scheme == "" || u.Host == "" {
if err != nil {
return fmt.Errorf("pipeline.audita.base_url must be a valid URL: %w", err)
}
return fmt.Errorf("pipeline.audita.base_url must be a valid URL")
}
}
if cfg.TotalLLMConcurrency != nil && *cfg.TotalLLMConcurrency <= 0 {
return fmt.Errorf("pipeline.audita.total_llm_concurrency must be > 0")
}
if cfg.ProposalLLMConcurrency != nil && *cfg.ProposalLLMConcurrency <= 0 {
return fmt.Errorf("pipeline.audita.proposal_llm_concurrency must be > 0")
}
if cfg.ValidationLLMConcurrency != nil && *cfg.ValidationLLMConcurrency <= 0 {
return fmt.Errorf("pipeline.audita.validation_llm_concurrency must be > 0")
}
if strings.TrimSpace(cfg.TranscriptDescription) == "" && cfg.TranscriptDescription != "" {
return fmt.Errorf("pipeline.audita.transcript_description must be non-empty when provided")
}
@@ -205,10 +187,5 @@ func validateAudita(cfg AuditaConfig) error {
default:
return fmt.Errorf("pipeline.audita.output_schema must be one of: bare-segments, audita-v1")
}
switch strings.TrimSpace(cfg.WorkDirRetention) {
case "", "always", "auto", "never":
default:
return fmt.Errorf("pipeline.audita.work_dir_retention must be one of: always, auto, never")
}
return nil
}