Centralize output schema and module key validation catalogs
This commit is contained in:
@@ -653,6 +653,10 @@ func runConfigValidate(args []string, stdout, stderr io.Writer) int {
|
||||
fmt.Fprintf(stderr, "audita config validate: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
fmt.Fprintf(stderr, "audita config validate: %v\n", err)
|
||||
return 2
|
||||
}
|
||||
fmt.Fprintln(stdout, "config is valid")
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/schema"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/llm"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/modules"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/proposal_generation"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/proposals"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/runner"
|
||||
@@ -210,6 +209,23 @@ func TestRunConfigValidateUnknownField(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigValidateUnsupportedModuleKey(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cfgPath := writeFile(t, "config.yml", "version: 1\npipeline:\n modules: [made_up]\n")
|
||||
|
||||
exitCode := Run([]string{"config", "validate", "--config", cfgPath}, &stdout, &stderr)
|
||||
if exitCode == 0 {
|
||||
t.Fatalf("expected failure for unsupported module key")
|
||||
}
|
||||
if stdout.Len() != 0 {
|
||||
t.Fatalf("expected empty stdout on failure, got %q", stdout.String())
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "unsupported module key") {
|
||||
t.Fatalf("expected unsupported module key error, got %q", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrintEffectiveOutputsRedactedJSON(t *testing.T) {
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
@@ -369,8 +385,8 @@ diagnostics:
|
||||
|
||||
func TestRunProcessEnvOverridesConfigFile(t *testing.T) {
|
||||
processModuleFactory = fakeModuleFactory{modules: map[string]contracts.TranscriptModule{
|
||||
"m": fakeModule{
|
||||
key: "m",
|
||||
"grammar": fakeModule{
|
||||
key: "grammar",
|
||||
policy: proposals.ReplacementPolicyRequireUnique,
|
||||
validators: []contracts.Validator{
|
||||
fakeValidator{name: "capture-config", validateF: func(req contracts.ValidationRequest) (validators.Result, error) {
|
||||
@@ -392,7 +408,7 @@ func TestRunProcessEnvOverridesConfigFile(t *testing.T) {
|
||||
cfgPath := writeFile(t, "config.yml", `
|
||||
version: 1
|
||||
pipeline:
|
||||
modules: [m]
|
||||
modules: [grammar]
|
||||
llm:
|
||||
proposal:
|
||||
model: file-model
|
||||
@@ -404,7 +420,7 @@ llm:
|
||||
fixturePath("tiny_transcript.json"),
|
||||
"--glossary", fixturePath("tiny_glossary.yaml"),
|
||||
"--config", cfgPath,
|
||||
"--modules", "m",
|
||||
"--modules", "grammar",
|
||||
}, &stdout, &stderr)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected success, got %d stderr=%q", exitCode, stderr.String())
|
||||
@@ -413,8 +429,8 @@ llm:
|
||||
|
||||
func TestRunProcessCLIOverridesEnvAndConfigFile(t *testing.T) {
|
||||
processModuleFactory = fakeModuleFactory{modules: map[string]contracts.TranscriptModule{
|
||||
"m": fakeModule{
|
||||
key: "m",
|
||||
"grammar": fakeModule{
|
||||
key: "grammar",
|
||||
policy: proposals.ReplacementPolicyRequireUnique,
|
||||
validators: []contracts.Validator{
|
||||
fakeValidator{name: "capture-config", validateF: func(req contracts.ValidationRequest) (validators.Result, error) {
|
||||
@@ -436,7 +452,7 @@ func TestRunProcessCLIOverridesEnvAndConfigFile(t *testing.T) {
|
||||
cfgPath := writeFile(t, "config.yml", `
|
||||
version: 1
|
||||
pipeline:
|
||||
modules: [m]
|
||||
modules: [grammar]
|
||||
llm:
|
||||
proposal:
|
||||
model: file-model
|
||||
@@ -448,7 +464,7 @@ llm:
|
||||
fixturePath("tiny_transcript.json"),
|
||||
"--glossary", fixturePath("tiny_glossary.yaml"),
|
||||
"--config", cfgPath,
|
||||
"--modules", "m",
|
||||
"--modules", "grammar",
|
||||
"--model", "cli-model",
|
||||
}, &stdout, &stderr)
|
||||
if exitCode != 0 {
|
||||
@@ -495,8 +511,8 @@ diagnostics:
|
||||
|
||||
func TestRunProcessTranscriptDescriptionCLIOverridesConfigFileContextDescription(t *testing.T) {
|
||||
processModuleFactory = fakeModuleFactory{modules: map[string]contracts.TranscriptModule{
|
||||
"m": fakeModule{
|
||||
key: "m",
|
||||
"grammar": fakeModule{
|
||||
key: "grammar",
|
||||
policy: proposals.ReplacementPolicyRequireUnique,
|
||||
validators: []contracts.Validator{
|
||||
fakeValidator{name: "capture-config", validateF: func(req contracts.ValidationRequest) (validators.Result, error) {
|
||||
@@ -517,7 +533,7 @@ func TestRunProcessTranscriptDescriptionCLIOverridesConfigFileContextDescription
|
||||
cfgPath := writeFile(t, "config.yml", `
|
||||
version: 1
|
||||
pipeline:
|
||||
modules: [m]
|
||||
modules: [grammar]
|
||||
context:
|
||||
description: "file transcript description"
|
||||
`)
|
||||
@@ -528,7 +544,7 @@ context:
|
||||
fixturePath("tiny_transcript.json"),
|
||||
"--glossary", fixturePath("tiny_glossary.yaml"),
|
||||
"--config", cfgPath,
|
||||
"--modules", "m",
|
||||
"--modules", "grammar",
|
||||
"--transcript-description", "cli transcript description",
|
||||
}, &stdout, &stderr)
|
||||
if exitCode != 0 {
|
||||
@@ -692,8 +708,8 @@ func TestRunProcessCLIOverridesEnvironment(t *testing.T) {
|
||||
|
||||
func TestRunProcessTranscriptDescriptionDefaultEmpty(t *testing.T) {
|
||||
processModuleFactory = fakeModuleFactory{modules: map[string]contracts.TranscriptModule{
|
||||
"m": fakeModule{
|
||||
key: "m",
|
||||
"grammar": fakeModule{
|
||||
key: "grammar",
|
||||
policy: proposals.ReplacementPolicyRequireUnique,
|
||||
validators: []contracts.Validator{
|
||||
fakeValidator{name: "capture-config", validateF: func(req contracts.ValidationRequest) (validators.Result, error) {
|
||||
@@ -720,7 +736,7 @@ func TestRunProcessTranscriptDescriptionDefaultEmpty(t *testing.T) {
|
||||
exitCode := Run([]string{
|
||||
"process", transcriptPath,
|
||||
"--glossary", fixturePath("tiny_glossary.yaml"),
|
||||
"--modules", "m",
|
||||
"--modules", "grammar",
|
||||
}, &stdout, &stderr)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected success, got %d stderr=%q", exitCode, stderr.String())
|
||||
@@ -729,8 +745,8 @@ func TestRunProcessTranscriptDescriptionDefaultEmpty(t *testing.T) {
|
||||
|
||||
func TestRunProcessTranscriptDescriptionCLIOverrideAndTrim(t *testing.T) {
|
||||
processModuleFactory = fakeModuleFactory{modules: map[string]contracts.TranscriptModule{
|
||||
"m": fakeModule{
|
||||
key: "m",
|
||||
"grammar": fakeModule{
|
||||
key: "grammar",
|
||||
policy: proposals.ReplacementPolicyRequireUnique,
|
||||
validators: []contracts.Validator{
|
||||
fakeValidator{name: "capture-config", validateF: func(req contracts.ValidationRequest) (validators.Result, error) {
|
||||
@@ -757,7 +773,7 @@ func TestRunProcessTranscriptDescriptionCLIOverrideAndTrim(t *testing.T) {
|
||||
exitCode := Run([]string{
|
||||
"process", transcriptPath,
|
||||
"--glossary", fixturePath("tiny_glossary.yaml"),
|
||||
"--modules", "m",
|
||||
"--modules", "grammar",
|
||||
"--transcript-description", " speaker background context ",
|
||||
}, &stdout, &stderr)
|
||||
if exitCode != 0 {
|
||||
@@ -823,8 +839,8 @@ func TestRunProcessRejectsValidationConcurrencyAboveTotalConcurrency(t *testing.
|
||||
|
||||
func TestRunProcessTotalLLMConcurrencyDrivesEffectiveValidationConcurrencyWhenUnset(t *testing.T) {
|
||||
processModuleFactory = fakeModuleFactory{modules: map[string]contracts.TranscriptModule{
|
||||
"m": fakeModule{
|
||||
key: "m",
|
||||
"grammar": fakeModule{
|
||||
key: "grammar",
|
||||
policy: proposals.ReplacementPolicyRequireUnique,
|
||||
validators: []contracts.Validator{
|
||||
fakeValidator{name: "capture-config", validateF: func(req contracts.ValidationRequest) (validators.Result, error) {
|
||||
@@ -865,7 +881,7 @@ func TestRunProcessTotalLLMConcurrencyDrivesEffectiveValidationConcurrencyWhenUn
|
||||
"--glossary",
|
||||
fixturePath("tiny_glossary.yaml"),
|
||||
"--modules",
|
||||
"m",
|
||||
"grammar",
|
||||
"--total-llm-concurrency",
|
||||
"4",
|
||||
}, &stdout, &stderr)
|
||||
@@ -908,8 +924,8 @@ func TestRunProcessRejectsProposalConcurrencyAboveTotalConcurrency(t *testing.T)
|
||||
|
||||
func TestRunProcessLegacyLLMConcurrencyAliasSetsTotalAndProposal(t *testing.T) {
|
||||
processModuleFactory = fakeModuleFactory{modules: map[string]contracts.TranscriptModule{
|
||||
"m": fakeModule{
|
||||
key: "m",
|
||||
"grammar": fakeModule{
|
||||
key: "grammar",
|
||||
policy: proposals.ReplacementPolicyRequireUnique,
|
||||
validators: []contracts.Validator{
|
||||
fakeValidator{name: "capture-config", validateF: func(req contracts.ValidationRequest) (validators.Result, error) {
|
||||
@@ -944,7 +960,7 @@ func TestRunProcessLegacyLLMConcurrencyAliasSetsTotalAndProposal(t *testing.T) {
|
||||
"--glossary",
|
||||
fixturePath("tiny_glossary.yaml"),
|
||||
"--modules",
|
||||
"m",
|
||||
"grammar",
|
||||
"--llm-concurrency",
|
||||
"3",
|
||||
}, &stdout, &stderr)
|
||||
@@ -955,8 +971,8 @@ func TestRunProcessLegacyLLMConcurrencyAliasSetsTotalAndProposal(t *testing.T) {
|
||||
|
||||
func TestRunProcessLLMConcurrencyFlagsOverrideEnvironment(t *testing.T) {
|
||||
processModuleFactory = fakeModuleFactory{modules: map[string]contracts.TranscriptModule{
|
||||
"m": fakeModule{
|
||||
key: "m",
|
||||
"grammar": fakeModule{
|
||||
key: "grammar",
|
||||
policy: proposals.ReplacementPolicyRequireUnique,
|
||||
validators: []contracts.Validator{
|
||||
fakeValidator{name: "capture-config", validateF: func(req contracts.ValidationRequest) (validators.Result, error) {
|
||||
@@ -997,7 +1013,7 @@ func TestRunProcessLLMConcurrencyFlagsOverrideEnvironment(t *testing.T) {
|
||||
"--glossary",
|
||||
fixturePath("tiny_glossary.yaml"),
|
||||
"--modules",
|
||||
"m",
|
||||
"grammar",
|
||||
"--total-llm-concurrency",
|
||||
"4",
|
||||
"--proposal-llm-concurrency",
|
||||
@@ -1010,8 +1026,8 @@ func TestRunProcessLLMConcurrencyFlagsOverrideEnvironment(t *testing.T) {
|
||||
|
||||
func TestRunProcessAcceptsLLMConcurrencyEnvironmentVariables(t *testing.T) {
|
||||
processModuleFactory = fakeModuleFactory{modules: map[string]contracts.TranscriptModule{
|
||||
"m": fakeModule{
|
||||
key: "m",
|
||||
"grammar": fakeModule{
|
||||
key: "grammar",
|
||||
policy: proposals.ReplacementPolicyRequireUnique,
|
||||
validators: []contracts.Validator{
|
||||
fakeValidator{name: "capture-config", validateF: func(req contracts.ValidationRequest) (validators.Result, error) {
|
||||
@@ -1052,7 +1068,7 @@ func TestRunProcessAcceptsLLMConcurrencyEnvironmentVariables(t *testing.T) {
|
||||
"--glossary",
|
||||
fixturePath("tiny_glossary.yaml"),
|
||||
"--modules",
|
||||
"m",
|
||||
"grammar",
|
||||
}, &stdout, &stderr)
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected success, got %d stderr=%q", exitCode, stderr.String())
|
||||
@@ -1883,12 +1899,12 @@ func TestRunProcessInjectedFactoryExecutesRunnerAndReportsModules(t *testing.T)
|
||||
return validators.Result{ValidatorName: "allow", Decisions: decisions}, nil
|
||||
}}
|
||||
processModuleFactory = fakeModuleFactory{modules: map[string]contracts.TranscriptModule{
|
||||
"m1": fakeModule{key: "m1", policy: proposals.ReplacementPolicyRequireUnique, validators: []contracts.Validator{allow}, proposeF: func(req contracts.ProposalRequest) ([]proposals.CorrectionProposal, error) {
|
||||
"glossary": fakeModule{key: "glossary", policy: proposals.ReplacementPolicyRequireUnique, validators: []contracts.Validator{allow}, proposeF: func(req contracts.ProposalRequest) ([]proposals.CorrectionProposal, error) {
|
||||
return []proposals.CorrectionProposal{
|
||||
{TargetSegmentID: 1, OriginalText: "Hello", CorrectedText: "Hi", Confidence: 1},
|
||||
}, nil
|
||||
}},
|
||||
"m2": fakeModule{key: "m2", policy: proposals.ReplacementPolicyRequireUnique, validators: []contracts.Validator{allow}, proposeF: func(req contracts.ProposalRequest) ([]proposals.CorrectionProposal, error) {
|
||||
"homophones": fakeModule{key: "homophones", policy: proposals.ReplacementPolicyRequireUnique, validators: []contracts.Validator{allow}, proposeF: func(req contracts.ProposalRequest) ([]proposals.CorrectionProposal, error) {
|
||||
if req.WorkingTranscript.Segments[0].Text != "Hi world" {
|
||||
t.Fatalf("expected module 2 to see module 1 changes, got %q", req.WorkingTranscript.Segments[0].Text)
|
||||
}
|
||||
@@ -1911,7 +1927,7 @@ func TestRunProcessInjectedFactoryExecutesRunnerAndReportsModules(t *testing.T)
|
||||
exitCode := Run([]string{
|
||||
"process", transcriptPath,
|
||||
"--glossary", fixturePath("tiny_glossary.yaml"),
|
||||
"--modules", "m1,m2",
|
||||
"--modules", "glossary,homophones",
|
||||
"--output", outputPath,
|
||||
"--report-json", reportPath,
|
||||
"--work-dir", workDir,
|
||||
@@ -1970,7 +1986,7 @@ func TestRunProcessInjectedFactoryLLMValidatorResultsInReports(t *testing.T) {
|
||||
}},
|
||||
}
|
||||
processModuleFactory = fakeModuleFactory{modules: map[string]contracts.TranscriptModule{
|
||||
"m": fakeModule{key: "m", policy: proposals.ReplacementPolicyRequireUnique, validators: []contracts.Validator{llmValidator}, proposeF: func(req contracts.ProposalRequest) ([]proposals.CorrectionProposal, error) {
|
||||
"grammar": fakeModule{key: "grammar", policy: proposals.ReplacementPolicyRequireUnique, validators: []contracts.Validator{llmValidator}, proposeF: func(req contracts.ProposalRequest) ([]proposals.CorrectionProposal, error) {
|
||||
return []proposals.CorrectionProposal{{TargetSegmentID: 1, OriginalText: "Hello", CorrectedText: "Hi", Confidence: 1}}, nil
|
||||
}},
|
||||
}}
|
||||
@@ -1990,7 +2006,7 @@ func TestRunProcessInjectedFactoryLLMValidatorResultsInReports(t *testing.T) {
|
||||
exitCode := Run([]string{
|
||||
"process", transcriptPath,
|
||||
"--glossary", fixturePath("tiny_glossary.yaml"),
|
||||
"--modules", "m",
|
||||
"--modules", "grammar",
|
||||
"--output", outputPath,
|
||||
"--report-json", reportPath,
|
||||
"--work-dir", workDir,
|
||||
@@ -2011,7 +2027,7 @@ func TestRunProcessInjectedFactoryLLMValidatorResultsInReports(t *testing.T) {
|
||||
|
||||
func TestRunProcessInjectedFactorySkippedKeepsAutoRetention(t *testing.T) {
|
||||
processModuleFactory = fakeModuleFactory{modules: map[string]contracts.TranscriptModule{
|
||||
"m1": fakeModule{key: "m1", policy: proposals.ReplacementPolicyRequireUnique, proposeF: func(req contracts.ProposalRequest) ([]proposals.CorrectionProposal, error) {
|
||||
"glossary": fakeModule{key: "glossary", policy: proposals.ReplacementPolicyRequireUnique, proposeF: func(req contracts.ProposalRequest) ([]proposals.CorrectionProposal, error) {
|
||||
return []proposals.CorrectionProposal{
|
||||
{TargetSegmentID: 1, OriginalText: "word", CorrectedText: "term", Confidence: 1},
|
||||
}, nil
|
||||
@@ -2027,7 +2043,7 @@ func TestRunProcessInjectedFactorySkippedKeepsAutoRetention(t *testing.T) {
|
||||
exitCode := Run([]string{
|
||||
"process", transcriptPath,
|
||||
"--glossary", fixturePath("tiny_glossary.yaml"),
|
||||
"--modules", "m1",
|
||||
"--modules", "glossary",
|
||||
"--work-dir", workDir,
|
||||
"--work-dir-retention", "auto",
|
||||
}, &stdout, &stderr)
|
||||
@@ -2041,7 +2057,7 @@ func TestRunProcessInjectedFactorySkippedKeepsAutoRetention(t *testing.T) {
|
||||
|
||||
func TestRunProcessInjectedFactoryFailureWritesFailedReport(t *testing.T) {
|
||||
processModuleFactory = fakeModuleFactory{modules: map[string]contracts.TranscriptModule{
|
||||
"m1": fakeModule{key: "m1", policy: proposals.ReplacementPolicyRequireUnique, proposeF: func(req contracts.ProposalRequest) ([]proposals.CorrectionProposal, error) {
|
||||
"glossary": fakeModule{key: "glossary", policy: proposals.ReplacementPolicyRequireUnique, proposeF: func(req contracts.ProposalRequest) ([]proposals.CorrectionProposal, error) {
|
||||
return nil, errors.New("test failure")
|
||||
}},
|
||||
}}
|
||||
@@ -2057,7 +2073,7 @@ func TestRunProcessInjectedFactoryFailureWritesFailedReport(t *testing.T) {
|
||||
exitCode := Run([]string{
|
||||
"process", transcriptPath,
|
||||
"--glossary", fixturePath("tiny_glossary.yaml"),
|
||||
"--modules", "m1",
|
||||
"--modules", "glossary",
|
||||
"--work-dir", workDir,
|
||||
"--work-dir-retention", "always",
|
||||
"--report-json", reportPath,
|
||||
@@ -2089,14 +2105,8 @@ func TestRunProcessInjectedFactoryFailureWritesFailedReport(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunProcessProductionRegistryUnsupportedModuleFailsCleanly(t *testing.T) {
|
||||
cfg := modules.Dependencies{}
|
||||
processModuleFactory = modules.NewFactory(cfg)
|
||||
t.Cleanup(func() { processModuleFactory = nil })
|
||||
|
||||
func TestRunProcessUnsupportedModuleFailsDuringConfigValidation(t *testing.T) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
workDir := t.TempDir()
|
||||
reportPath := filepath.Join(t.TempDir(), "report.json")
|
||||
transcriptPath := writeFile(t, "transcript.json", `[
|
||||
{"id":1,"speaker":"Alice","start":0.0,"end":1.0,"text":"Hello"}
|
||||
]`)
|
||||
@@ -2105,9 +2115,6 @@ func TestRunProcessProductionRegistryUnsupportedModuleFailsCleanly(t *testing.T)
|
||||
"process", transcriptPath,
|
||||
"--glossary", fixturePath("tiny_glossary.yaml"),
|
||||
"--modules", "made_up",
|
||||
"--work-dir", workDir,
|
||||
"--work-dir-retention", "always",
|
||||
"--report-json", reportPath,
|
||||
}, &stdout, &stderr)
|
||||
if exitCode == 0 {
|
||||
t.Fatal("expected failure exit code")
|
||||
@@ -2115,23 +2122,12 @@ func TestRunProcessProductionRegistryUnsupportedModuleFailsCleanly(t *testing.T)
|
||||
if stdout.Len() != 0 {
|
||||
t.Fatalf("expected empty stdout on failure, got %q", stdout.String())
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "runner_execution") {
|
||||
t.Fatalf("expected runner_execution failure on stderr, got %q", stderr.String())
|
||||
if !strings.Contains(stderr.String(), "invalid CLI configuration") {
|
||||
t.Fatalf("expected config validation failure on stderr, got %q", stderr.String())
|
||||
}
|
||||
if !strings.Contains(stderr.String(), "unsupported module key") {
|
||||
t.Fatalf("expected explicit unsupported module message, got %q", stderr.String())
|
||||
}
|
||||
|
||||
report := readProcessReport(t, reportPath)
|
||||
if report.Status != "failed" {
|
||||
t.Fatalf("expected failed report status, got %q", report.Status)
|
||||
}
|
||||
if report.ErrorPhase != "runner_execution" {
|
||||
t.Fatalf("expected runner_execution phase, got %q", report.ErrorPhase)
|
||||
}
|
||||
if !strings.Contains(report.ErrorMessage, "unsupported module key") {
|
||||
t.Fatalf("expected report error message to mention unsupported module, got %q", report.ErrorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunProcessExplicitUnsupportedModulesFailClearly(t *testing.T) {
|
||||
|
||||
@@ -3,6 +3,8 @@ package config
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/modulecatalog"
|
||||
)
|
||||
|
||||
type WorkDirRetention string
|
||||
@@ -14,7 +16,7 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultModulesCSV = "glossary,homophones,glossary,spoken_word,grammar"
|
||||
DefaultModulesCSV = modulecatalog.KeyGlossary + "," + modulecatalog.KeyHomophones + "," + modulecatalog.KeyGlossary + "," + modulecatalog.KeySpokenWord + "," + modulecatalog.KeyGrammar
|
||||
DefaultOutputSchema = "bare-segments"
|
||||
DefaultPrimaryModel = "openrouter/google/gemma-4-31b-it"
|
||||
DefaultPrimaryBaseURL = "https://openrouter.ai/api/v1"
|
||||
|
||||
@@ -4,6 +4,9 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/modulecatalog"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/outputschema"
|
||||
)
|
||||
|
||||
func TestDefaultConfigValues(t *testing.T) {
|
||||
@@ -318,6 +321,38 @@ func TestValidationFailures(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidationRejectsUnsupportedModuleKey(t *testing.T) {
|
||||
cfg := Default()
|
||||
cfg.Modules = []string{modulecatalog.KeyGlossary, "made_up"}
|
||||
|
||||
err := cfg.Validate()
|
||||
if err == nil {
|
||||
t.Fatalf("expected validation error for unsupported module key")
|
||||
}
|
||||
if !strings.Contains(err.Error(), `unsupported module key "made_up"`) {
|
||||
t.Fatalf("expected unsupported module key error, got %q", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidationAllowsRepeatedSupportedModuleKeys(t *testing.T) {
|
||||
cfg := Default()
|
||||
cfg.Modules = []string{modulecatalog.KeyGlossary, modulecatalog.KeyGlossary, modulecatalog.KeyGrammar}
|
||||
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("expected repeated supported module keys to validate, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidationAcceptsAllSupportedOutputSchemas(t *testing.T) {
|
||||
for _, schemaKey := range outputschema.SupportedKeys() {
|
||||
cfg := Default()
|
||||
cfg.OutputSchema = schemaKey
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("expected output schema %q to validate, got %v", schemaKey, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEffectiveValidationLLMInheritance(t *testing.T) {
|
||||
cfg := Default()
|
||||
cfg.PrimaryLLM.APIKey = "primary-key"
|
||||
|
||||
@@ -3,6 +3,9 @@ package config
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/modulecatalog"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/outputschema"
|
||||
)
|
||||
|
||||
func (c Config) Validate() error {
|
||||
@@ -12,19 +15,19 @@ func (c Config) Validate() error {
|
||||
issues = append(issues, "modules must not be empty")
|
||||
}
|
||||
for _, module := range c.Modules {
|
||||
if strings.TrimSpace(module) == "" {
|
||||
moduleKey := strings.TrimSpace(module)
|
||||
if moduleKey == "" {
|
||||
issues = append(issues, "modules must not contain empty values")
|
||||
break
|
||||
}
|
||||
if !modulecatalog.IsSupported(moduleKey) {
|
||||
issues = append(issues, fmt.Sprintf("unsupported module key %q", moduleKey))
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(c.OutputSchema) == "" {
|
||||
issues = append(issues, "output schema must not be empty")
|
||||
} else {
|
||||
switch strings.TrimSpace(c.OutputSchema) {
|
||||
case "bare-segments", "audita-v1":
|
||||
default:
|
||||
issues = append(issues, fmt.Sprintf("unsupported output schema %q", c.OutputSchema))
|
||||
}
|
||||
} else if !outputschema.IsSupported(c.OutputSchema) {
|
||||
issues = append(issues, fmt.Sprintf("unsupported output schema %q", c.OutputSchema))
|
||||
}
|
||||
|
||||
if c.PrimaryLLM.TimeoutSeconds <= 0 {
|
||||
|
||||
35
internal/core/modulecatalog/catalog.go
Normal file
35
internal/core/modulecatalog/catalog.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package modulecatalog
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
KeyGlossary = "glossary"
|
||||
KeyHomophones = "homophones"
|
||||
KeySpokenWord = "spoken_word"
|
||||
KeyGrammar = "grammar"
|
||||
)
|
||||
|
||||
var supportedKeys = []string{
|
||||
KeyGlossary,
|
||||
KeyHomophones,
|
||||
KeySpokenWord,
|
||||
KeyGrammar,
|
||||
}
|
||||
|
||||
var supportedKeySet = map[string]struct{}{
|
||||
KeyGlossary: {},
|
||||
KeyHomophones: {},
|
||||
KeySpokenWord: {},
|
||||
KeyGrammar: {},
|
||||
}
|
||||
|
||||
func SupportedKeys() []string {
|
||||
out := make([]string, len(supportedKeys))
|
||||
copy(out, supportedKeys)
|
||||
return out
|
||||
}
|
||||
|
||||
func IsSupported(key string) bool {
|
||||
_, ok := supportedKeySet[strings.TrimSpace(key)]
|
||||
return ok
|
||||
}
|
||||
24
internal/core/modulecatalog/catalog_test.go
Normal file
24
internal/core/modulecatalog/catalog_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package modulecatalog
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSupportedKeys(t *testing.T) {
|
||||
want := []string{KeyGlossary, KeyHomophones, KeySpokenWord, KeyGrammar}
|
||||
if got := SupportedKeys(); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("unexpected supported keys: got=%v want=%v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSupported(t *testing.T) {
|
||||
for _, key := range SupportedKeys() {
|
||||
if !IsSupported(key) {
|
||||
t.Fatalf("expected key %q to be supported", key)
|
||||
}
|
||||
}
|
||||
if IsSupported("made_up") {
|
||||
t.Fatalf("did not expect made_up to be supported")
|
||||
}
|
||||
}
|
||||
@@ -31,15 +31,31 @@ var definitions = map[string]Definition{
|
||||
},
|
||||
}
|
||||
|
||||
var supportedKeys = []string{
|
||||
SchemaBareSegments,
|
||||
SchemaAuditaV1,
|
||||
}
|
||||
|
||||
func SupportedKeys() []string {
|
||||
out := make([]string, len(supportedKeys))
|
||||
copy(out, supportedKeys)
|
||||
return out
|
||||
}
|
||||
|
||||
func IsSupported(key string) bool {
|
||||
_, ok := definitions[strings.TrimSpace(key)]
|
||||
return ok
|
||||
}
|
||||
|
||||
func Resolve(key string) (Definition, error) {
|
||||
normalized := strings.TrimSpace(key)
|
||||
if normalized == "" {
|
||||
return Definition{}, fmt.Errorf("output schema must not be empty")
|
||||
}
|
||||
def, ok := definitions[normalized]
|
||||
if !ok {
|
||||
if !IsSupported(normalized) {
|
||||
return Definition{}, fmt.Errorf("unsupported output schema %q", normalized)
|
||||
}
|
||||
def := definitions[normalized]
|
||||
return def, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package outputschema
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -56,3 +57,19 @@ func TestResolveUnknown(t *testing.T) {
|
||||
t.Fatalf("expected unsupported output schema error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSupportedKeysAndIsSupported(t *testing.T) {
|
||||
want := []string{SchemaBareSegments, SchemaAuditaV1}
|
||||
if got := SupportedKeys(); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("unexpected supported schema keys: got=%v want=%v", got, want)
|
||||
}
|
||||
|
||||
for _, key := range want {
|
||||
if !IsSupported(key) {
|
||||
t.Fatalf("expected schema key %q to be supported", key)
|
||||
}
|
||||
}
|
||||
if IsSupported("seriatim-intermediate") {
|
||||
t.Fatalf("did not expect unsupported schema to be reported as supported")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/config"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/modulecatalog"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/schema"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/contracts"
|
||||
glossarymodule "gitea.maximumdirect.net/eric/audita/internal/modules/glossary"
|
||||
@@ -15,28 +16,20 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ModuleKeyGlossary = "glossary"
|
||||
ModuleKeyHomophones = "homophones"
|
||||
ModuleKeySpokenWord = "spoken_word"
|
||||
ModuleKeyGrammar = "grammar"
|
||||
ModuleKeyGlossary = modulecatalog.KeyGlossary
|
||||
ModuleKeyHomophones = modulecatalog.KeyHomophones
|
||||
ModuleKeySpokenWord = modulecatalog.KeySpokenWord
|
||||
ModuleKeyGrammar = modulecatalog.KeyGrammar
|
||||
)
|
||||
|
||||
const (
|
||||
ReasonUnsupportedModule = "unsupported_module"
|
||||
)
|
||||
|
||||
var knownModuleKeys = map[string]struct{}{
|
||||
ModuleKeyGlossary: {},
|
||||
ModuleKeyHomophones: {},
|
||||
ModuleKeySpokenWord: {},
|
||||
ModuleKeyGrammar: {},
|
||||
}
|
||||
|
||||
// IsKnownModuleKey reports whether a module key is recognized by the production
|
||||
// registry scaffold.
|
||||
func IsKnownModuleKey(key string) bool {
|
||||
_, ok := knownModuleKeys[strings.TrimSpace(key)]
|
||||
return ok
|
||||
return modulecatalog.IsSupported(key)
|
||||
}
|
||||
|
||||
// Dependencies holds explicit constructor dependencies for module creation.
|
||||
@@ -69,7 +62,7 @@ type Factory struct {
|
||||
func NewFactory(deps Dependencies) *Factory {
|
||||
factory := &Factory{
|
||||
deps: deps,
|
||||
constructors: make(map[string]Constructor, len(knownModuleKeys)),
|
||||
constructors: make(map[string]Constructor, len(modulecatalog.SupportedKeys())),
|
||||
}
|
||||
_ = factory.RegisterConstructor(ModuleKeyGlossary, constructGlossaryModule)
|
||||
_ = factory.RegisterConstructor(ModuleKeyHomophones, constructHomophonesModule)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/modulecatalog"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/proposals"
|
||||
)
|
||||
@@ -26,7 +27,7 @@ func (m noopModule) Propose(ctx context.Context, req contracts.ProposalRequest)
|
||||
}
|
||||
|
||||
func TestKnownModuleKeyRecognition(t *testing.T) {
|
||||
for _, key := range []string{ModuleKeyGlossary, ModuleKeyHomophones, ModuleKeySpokenWord, ModuleKeyGrammar} {
|
||||
for _, key := range modulecatalog.SupportedKeys() {
|
||||
if !IsKnownModuleKey(key) {
|
||||
t.Fatalf("expected key %q to be recognized", key)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/config"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/modulecatalog"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/schema"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/proposals"
|
||||
stagewarnings "gitea.maximumdirect.net/eric/audita/internal/framework/warnings"
|
||||
@@ -118,13 +119,13 @@ func confidenceThresholdForModule(moduleKey string, cfg *config.Config) float64
|
||||
return 0.0
|
||||
}
|
||||
switch moduleKey {
|
||||
case "glossary":
|
||||
case modulecatalog.KeyGlossary:
|
||||
return cfg.Thresholds.Glossary
|
||||
case "grammar":
|
||||
case modulecatalog.KeyGrammar:
|
||||
return cfg.Thresholds.Grammar
|
||||
case "homophones":
|
||||
case modulecatalog.KeyHomophones:
|
||||
return cfg.Thresholds.Homophones
|
||||
case "spoken_word":
|
||||
case modulecatalog.KeySpokenWord:
|
||||
return cfg.Thresholds.SpokenWord
|
||||
default:
|
||||
return 0.0
|
||||
|
||||
@@ -2,13 +2,15 @@ package validators
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/modulecatalog"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/validators/protected_terms"
|
||||
)
|
||||
|
||||
var builtInChains = map[string][]string{
|
||||
"glossary": {
|
||||
modulecatalog.KeyGlossary: {
|
||||
KeyProposalShape,
|
||||
KeyNoEffect,
|
||||
KeyOriginalTextPresence,
|
||||
@@ -18,7 +20,7 @@ var builtInChains = map[string][]string{
|
||||
KeySpokenFormPlausibility,
|
||||
KeyMeaningReversalReview,
|
||||
},
|
||||
"homophones": {
|
||||
modulecatalog.KeyHomophones: {
|
||||
KeyProposalShape,
|
||||
KeyNoEffect,
|
||||
KeyOriginalTextPresence,
|
||||
@@ -28,7 +30,7 @@ var builtInChains = map[string][]string{
|
||||
KeySpokenFormPlausibility,
|
||||
KeyMeaningReversalReview,
|
||||
},
|
||||
"spoken_word": {
|
||||
modulecatalog.KeySpokenWord: {
|
||||
KeyProposalShape,
|
||||
KeyNoEffect,
|
||||
KeyOriginalTextPresence,
|
||||
@@ -38,7 +40,7 @@ var builtInChains = map[string][]string{
|
||||
KeyEditorialReview,
|
||||
KeyMeaningReversalReview,
|
||||
},
|
||||
"grammar": {
|
||||
modulecatalog.KeyGrammar: {
|
||||
KeyProposalShape,
|
||||
KeyNoEffect,
|
||||
KeyOriginalTextPresence,
|
||||
@@ -51,9 +53,10 @@ var builtInChains = map[string][]string{
|
||||
}
|
||||
|
||||
func BuiltInChainKeys(moduleKey string) ([]string, error) {
|
||||
keys, ok := builtInChains[moduleKey]
|
||||
key := strings.TrimSpace(moduleKey)
|
||||
keys, ok := builtInChains[key]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no built-in validator chain for module %q", moduleKey)
|
||||
return nil, fmt.Errorf("no built-in validator chain for module %q", key)
|
||||
}
|
||||
out := make([]string, len(keys))
|
||||
copy(out, keys)
|
||||
@@ -61,6 +64,7 @@ func BuiltInChainKeys(moduleKey string) ([]string, error) {
|
||||
}
|
||||
|
||||
func ResolveBuiltInChain(moduleKey string, registry *Registry) ([]contracts.Validator, error) {
|
||||
moduleKey = strings.TrimSpace(moduleKey)
|
||||
keys, err := BuiltInChainKeys(moduleKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -71,7 +75,7 @@ func ResolveBuiltInChain(moduleKey string, registry *Registry) ([]contracts.Vali
|
||||
|
||||
out := make([]contracts.Validator, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
if moduleKey == "glossary" && key == KeyProtectedTerms {
|
||||
if moduleKey == modulecatalog.KeyGlossary && key == KeyProtectedTerms {
|
||||
// Glossary stages preserve current stricter protection semantics while
|
||||
// reporting the stable protected_terms key.
|
||||
v, buildErr := protected_terms.NewGlossaryStage()
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/modulecatalog"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/core/schema"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/audita/internal/framework/proposals"
|
||||
@@ -200,7 +201,7 @@ func TestBuiltInRegistryUnknownKeyFails(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestBuiltInChainKeysResolveForProductionModules(t *testing.T) {
|
||||
for _, moduleKey := range []string{"glossary", "homophones", "spoken_word", "grammar"} {
|
||||
for _, moduleKey := range modulecatalog.SupportedKeys() {
|
||||
keys, err := BuiltInChainKeys(moduleKey)
|
||||
if err != nil {
|
||||
t.Fatalf("resolve keys for %q: %v", moduleKey, err)
|
||||
@@ -213,7 +214,7 @@ func TestBuiltInChainKeysResolveForProductionModules(t *testing.T) {
|
||||
|
||||
func TestResolveBuiltInChainUsesRegisteredKeys(t *testing.T) {
|
||||
r := NewBuiltInRegistry()
|
||||
for _, moduleKey := range []string{"glossary", "homophones", "spoken_word", "grammar"} {
|
||||
for _, moduleKey := range modulecatalog.SupportedKeys() {
|
||||
chain, err := ResolveBuiltInChain(moduleKey, r)
|
||||
if err != nil {
|
||||
t.Fatalf("resolve chain for %q: %v", moduleKey, err)
|
||||
|
||||
Reference in New Issue
Block a user