Add output schema registry and public contract docs

This commit is contained in:
2026-05-13 12:59:10 +00:00
parent 9a77a0cd0b
commit 1bc5936681
21 changed files with 791 additions and 24 deletions

View File

@@ -17,6 +17,7 @@ import (
"gitea.maximumdirect.net/eric/audita/internal/core/diagnostics"
coreio "gitea.maximumdirect.net/eric/audita/internal/core/io"
"gitea.maximumdirect.net/eric/audita/internal/core/normalization"
"gitea.maximumdirect.net/eric/audita/internal/core/outputschema"
"gitea.maximumdirect.net/eric/audita/internal/core/reporting"
"gitea.maximumdirect.net/eric/audita/internal/core/schema"
"gitea.maximumdirect.net/eric/audita/internal/framework/contracts"
@@ -53,6 +54,7 @@ type processInvocation struct {
Config config.Config
ConfigPath string
ConfigSource string
ConfigVersion *int
ExplicitModules bool
}
@@ -84,6 +86,7 @@ var processRunner = func(inv processInvocation, stdout io.Writer) (*normalizatio
ReportJSONPath: inv.ReportJSONPath,
ConfigPath: inv.ConfigPath,
ConfigSource: inv.ConfigSource,
ConfigVersion: inv.ConfigVersion,
TranscriptDescription: inv.Config.TranscriptDescription,
Modules: append([]string(nil), inv.Config.Modules...),
}); err != nil {
@@ -256,7 +259,11 @@ var processRunner = func(inv processInvocation, stdout io.Writer) (*normalizatio
workingTranscript = runnerResult.FinalTranscript
}
outputBytes, err := schema.TranscriptToJSON(workingTranscript)
encoderDef, err := outputschema.Resolve(inv.Config.OutputSchema)
if err != nil {
return fail("output_schema", err, runOutput)
}
outputBytes, err := encoderDef.Encoder(workingTranscript)
if err != nil {
return fail("serialization", err, runOutput)
}
@@ -369,6 +376,7 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
}
cfg := config.Default()
var configVersion *int
if configPath != "" {
fileCfg, fileErr := config.LoadFileConfig(configPath)
if fileErr != nil {
@@ -379,6 +387,7 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
fmt.Fprintf(stderr, "audita process: invalid config file: %v\n", applyErr)
return 2
}
configVersion = &fileCfg.Version
}
if err := cfg.ApplyEnvOverrides(); err != nil {
fmt.Fprintf(stderr, "audita process: invalid environment configuration: %v\n", err)
@@ -414,6 +423,8 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
case "modules":
explicitModules = true
overrides.ModulesCSV = pFlags.modules
case "output-schema":
overrides.OutputSchema = pFlags.outputSchema
case "llm-api-key":
overrides.PrimaryLLMAPIKey = pFlags.llmAPIKey
case "validation-llm-api-key":
@@ -504,6 +515,7 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
Config: cfg,
ConfigPath: configPath,
ConfigSource: configSource,
ConfigVersion: configVersion,
ExplicitModules: explicitModules,
}
@@ -694,6 +706,12 @@ func extractErrorPhase(err error) (phase string, message string) {
func buildProcessReport(status string, inv processInvocation, runDir *diagnostics.RunDirectory, startedAt, completedAt time.Time, errorMessage string, errorPhase string, normalizationSummary *normalization.NormalizationSummary, chunkingSummary *chunking.Summary, runOutput *runner.RunOutput) reporting.ProcessReport {
report := reporting.ProcessReport{
ReportMetadata: reporting.ReportMetadata{
ReportSchemaName: reporting.DefaultProcessReportSchemaName,
ReportSchemaVersion: reporting.DefaultProcessReportSchemaVersion,
OutputSchema: inv.Config.OutputSchema,
ConfigVersion: inv.ConfigVersion,
},
Phase: "default_pipeline",
Status: status,
Operation: "process",
@@ -828,6 +846,7 @@ type processFlags struct {
outputPath *string
reportJSONPath *string
modules *string
outputSchema *string
llmAPIKey *string
validationLLMAPIKey *string
model *string
@@ -889,6 +908,7 @@ func newProcessFlagSet(cfg config.Config, stderr io.Writer) (*flag.FlagSet, proc
outputPath: fs.String("output", "", "Path to corrected transcript JSON output file"),
reportJSONPath: fs.String("report-json", "", "Path to machine-readable report JSON output file"),
modules: fs.String("modules", strings.Join(cfg.Modules, ","), "Comma-separated module sequence override"),
outputSchema: fs.String("output-schema", cfg.OutputSchema, "Output schema: bare-segments|audita-v1"),
llmAPIKey: fs.String("llm-api-key", cfg.PrimaryLLM.APIKey, "Primary LLM API key"),
validationLLMAPIKey: fs.String("validation-llm-api-key", cfg.ValidationLLM.APIKey, "Validation LLM API key"),
model: fs.String("model", cfg.PrimaryLLM.Model, "Primary LLM model name"),

View File

@@ -57,6 +57,7 @@ func TestRunProcessHelpListsExpectedFlags(t *testing.T) {
"--config",
"--glossary",
"--output",
"--output-schema",
"--report-json",
"--modules",
"--llm-api-key",
@@ -1158,6 +1159,16 @@ func TestRunProcessReportJSONSuccessIncludesNormalizationSummary(t *testing.T) {
if report.Status != "success" {
t.Fatalf("expected success status, got %q", report.Status)
}
if report.ReportMetadata.ReportSchemaName != reporting.DefaultProcessReportSchemaName ||
report.ReportMetadata.ReportSchemaVersion != reporting.DefaultProcessReportSchemaVersion {
t.Fatalf("unexpected report schema metadata: %+v", report.ReportMetadata)
}
if report.ReportMetadata.OutputSchema != "bare-segments" {
t.Fatalf("expected bare-segments output schema metadata, got %q", report.ReportMetadata.OutputSchema)
}
if report.ReportMetadata.ConfigVersion != nil {
t.Fatalf("expected nil config version without config file, got %v", *report.ReportMetadata.ConfigVersion)
}
if report.Operation != "process" {
t.Fatalf("expected operation process, got %q", report.Operation)
}
@@ -1291,12 +1302,65 @@ func TestRunProcessReportJSONAndRunDirReportShareDiagnosticsMetadata(t *testing.
if externalReport.Diagnostics == nil || runDirReport.Diagnostics == nil {
t.Fatalf("expected diagnostics metadata in both reports")
}
if externalReport.ReportMetadata != runDirReport.ReportMetadata {
t.Fatalf("expected same report metadata in --report-json and run-dir report\nexternal=%+v\nrun-dir=%+v",
externalReport.ReportMetadata, runDirReport.ReportMetadata)
}
if *externalReport.Diagnostics != *runDirReport.Diagnostics {
t.Fatalf("expected same diagnostics metadata in --report-json and run-dir report\nexternal=%+v\nrun-dir=%+v",
*externalReport.Diagnostics, *runDirReport.Diagnostics)
}
}
func TestRunProcessReportJSONIncludesConfigVersionWhenConfigFileUsed(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
workDir := t.TempDir()
reportPath := filepath.Join(t.TempDir(), "report.json")
cfgPath := writeFile(t, "config.yml", "version: 1\noutput:\n schema: audita-v1\n")
exitCode := Run([]string{
"process",
fixturePath("tiny_transcript.json"),
"--glossary",
fixturePath("tiny_glossary.yaml"),
"--config",
cfgPath,
"--report-json",
reportPath,
"--work-dir",
workDir,
"--work-dir-retention",
"always",
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d with stderr %q", exitCode, stderr.String())
}
report := readProcessReport(t, reportPath)
if report.ReportMetadata.ConfigVersion == nil || *report.ReportMetadata.ConfigVersion != 1 {
t.Fatalf("expected config version 1 in report metadata, got %+v", report.ReportMetadata)
}
if report.ReportMetadata.OutputSchema != "audita-v1" {
t.Fatalf("expected output schema from config in report metadata, got %q", report.ReportMetadata.OutputSchema)
}
runDirReport := readProcessReport(t, filepath.Join(onlyRunDir(t, workDir), "report.json"))
if runDirReport.ReportMetadata.ReportSchemaName != report.ReportMetadata.ReportSchemaName ||
runDirReport.ReportMetadata.ReportSchemaVersion != report.ReportMetadata.ReportSchemaVersion ||
runDirReport.ReportMetadata.OutputSchema != report.ReportMetadata.OutputSchema {
t.Fatalf("expected same report metadata in run-dir report, got external=%+v run-dir=%+v",
report.ReportMetadata, runDirReport.ReportMetadata)
}
if runDirReport.ReportMetadata.ConfigVersion == nil ||
report.ReportMetadata.ConfigVersion == nil ||
*runDirReport.ReportMetadata.ConfigVersion != *report.ReportMetadata.ConfigVersion {
t.Fatalf("expected same config version in run-dir report metadata, got external=%+v run-dir=%+v",
report.ReportMetadata, runDirReport.ReportMetadata)
}
}
func TestRunProcessWritesNormalizationDiagnosticsArtifacts(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
@@ -3859,6 +3923,148 @@ func TestRunProcessReportJSONNotPrintedToStdout(t *testing.T) {
}
}
func TestRunProcessDefaultOutputMatchesBareSegmentsSchema(t *testing.T) {
var stdoutDefault, stderrDefault bytes.Buffer
var stdoutBare, stderrBare bytes.Buffer
transcriptPath := fixturePath("tiny_transcript.json")
glossaryPath := fixturePath("tiny_glossary.yaml")
exitDefault := Run([]string{"process", transcriptPath, "--glossary", glossaryPath}, &stdoutDefault, &stderrDefault)
if exitDefault != 0 {
t.Fatalf("default run failed: %d stderr=%q", exitDefault, stderrDefault.String())
}
exitBare := Run([]string{"process", transcriptPath, "--glossary", glossaryPath, "--output-schema", "bare-segments"}, &stdoutBare, &stderrBare)
if exitBare != 0 {
t.Fatalf("bare-segments run failed: %d stderr=%q", exitBare, stderrBare.String())
}
if stdoutDefault.String() != stdoutBare.String() {
t.Fatalf("expected default output to match bare-segments output")
}
}
func TestRunProcessOutputSchemaAuditaV1ToStdout(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"process",
fixturePath("tiny_transcript.json"),
"--glossary", fixturePath("tiny_glossary.yaml"),
"--output-schema", "audita-v1",
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected success, got %d stderr=%q", exitCode, stderr.String())
}
var out struct {
Schema string `json:"schema"`
Version string `json:"version"`
Segments []schema.Segment `json:"segments"`
}
if err := json.Unmarshal(stdout.Bytes(), &out); err != nil {
t.Fatalf("expected audita-v1 JSON object output: %v", err)
}
if out.Schema != "audita-v1" || out.Version != "v1" {
t.Fatalf("unexpected schema metadata: schema=%q version=%q", out.Schema, out.Version)
}
if len(out.Segments) == 0 {
t.Fatalf("expected non-empty segments in audita-v1 output")
}
}
func TestRunProcessOutputSchemaAuditaV1ToFile(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
outputPath := filepath.Join(t.TempDir(), "out.json")
exitCode := Run([]string{
"process",
fixturePath("tiny_transcript.json"),
"--glossary", fixturePath("tiny_glossary.yaml"),
"--output-schema", "audita-v1",
"--output", outputPath,
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected success, got %d stderr=%q", exitCode, stderr.String())
}
if stdout.Len() != 0 {
t.Fatalf("expected empty stdout with --output, got %q", stdout.String())
}
var out map[string]any
if err := json.Unmarshal(readFile(t, outputPath), &out); err != nil {
t.Fatalf("expected valid audita-v1 JSON file: %v", err)
}
if out["schema"] != "audita-v1" {
t.Fatalf("expected audita-v1 schema in file output, got %#v", out["schema"])
}
}
func TestRunProcessOutputSchemaFromConfig(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
cfgPath := writeFile(t, "config.yml", "version: 1\noutput:\n schema: audita-v1\n")
exitCode := Run([]string{
"process",
fixturePath("tiny_transcript.json"),
"--glossary", fixturePath("tiny_glossary.yaml"),
"--config", cfgPath,
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected success, got %d stderr=%q", exitCode, stderr.String())
}
var out map[string]any
if err := json.Unmarshal(stdout.Bytes(), &out); err != nil {
t.Fatalf("expected audita-v1 object from config output schema: %v", err)
}
if out["schema"] != "audita-v1" {
t.Fatalf("expected audita-v1 schema from config output, got %#v", out["schema"])
}
}
func TestRunProcessCLIOutputSchemaOverridesConfig(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
cfgPath := writeFile(t, "config.yml", "version: 1\noutput:\n schema: audita-v1\n")
exitCode := Run([]string{
"process",
fixturePath("tiny_transcript.json"),
"--glossary", fixturePath("tiny_glossary.yaml"),
"--config", cfgPath,
"--output-schema", "bare-segments",
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("expected success, got %d stderr=%q", exitCode, stderr.String())
}
if strings.HasPrefix(strings.TrimSpace(stdout.String()), "{") {
t.Fatalf("expected bare-segments array output from CLI override, got object")
}
}
func TestRunProcessUnknownOutputSchemaFailsClearly(t *testing.T) {
var stdout bytes.Buffer
var stderr bytes.Buffer
exitCode := Run([]string{
"process",
fixturePath("tiny_transcript.json"),
"--glossary", fixturePath("tiny_glossary.yaml"),
"--output-schema", "seriatim-intermediate",
}, &stdout, &stderr)
if exitCode == 0 {
t.Fatalf("expected failure for unknown output schema")
}
if stdout.Len() != 0 {
t.Fatalf("expected empty stdout on failure, got %q", stdout.String())
}
if !strings.Contains(stderr.String(), "unsupported output schema") {
t.Fatalf("expected unsupported output schema error, got %q", stderr.String())
}
}
func fixturePath(name string) string {
return filepath.Join("testdata", name)
}