Add output schema registry and public contract docs
This commit is contained in:
@@ -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"),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user