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

@@ -66,6 +66,16 @@ audita process transcript.json \
--report-json report.json --report-json report.json
``` ```
Select an explicit output schema (default is `bare-segments`):
```sh
audita process transcript.json \
--glossary glossary.yaml \
--output-schema audita-v1 \
--output corrected.json \
--report-json report.json
```
Recommended config-based run: Recommended config-based run:
```sh ```sh
@@ -139,6 +149,8 @@ audita config print-effective --config audita.yml
``` ```
For full config-file schema and examples, see [`docs/configuration.md`](docs/configuration.md). For full config-file schema and examples, see [`docs/configuration.md`](docs/configuration.md).
For output-schema details, see [`docs/output-schemas.md`](docs/output-schemas.md).
For CLI/process compatibility guarantees, see [`docs/public-contract.md`](docs/public-contract.md).
### Modules ### Modules

View File

@@ -99,6 +99,33 @@ func TestProcessSuccessWithoutOutputSubprocess(t *testing.T) {
assertJSONSemanticallyEqual(t, inputBytes, []byte(result.stdout)) assertJSONSemanticallyEqual(t, inputBytes, []byte(result.stdout))
} }
func TestProcessSuccessWithAuditaV1OutputSchemaSubprocess(t *testing.T) {
result := runCLISubprocess(
t,
"process",
fixturePath("tiny_transcript.json"),
"--glossary",
fixturePath("tiny_glossary.yaml"),
"--output-schema",
"audita-v1",
)
if result.exitCode != 0 {
t.Fatalf("expected exit 0, got %d stderr=%q", result.exitCode, result.stderr)
}
if result.stderr != "" {
t.Fatalf("expected empty stderr on success, got %q", result.stderr)
}
var out struct {
Schema string `json:"schema"`
}
if err := json.Unmarshal([]byte(result.stdout), &out); err != nil {
t.Fatalf("expected valid audita-v1 JSON output: %v", err)
}
if out.Schema != "audita-v1" {
t.Fatalf("expected audita-v1 schema, got %q", out.Schema)
}
}
func TestProcessFailureMissingTranscriptSubprocess(t *testing.T) { func TestProcessFailureMissingTranscriptSubprocess(t *testing.T) {
result := runCLISubprocess(t, "process", "--glossary", fixturePath("tiny_glossary.yaml")) result := runCLISubprocess(t, "process", "--glossary", fixturePath("tiny_glossary.yaml"))
if result.exitCode == 0 { if result.exitCode == 0 {
@@ -474,9 +501,6 @@ func TestProcessCancellationViaSubprocessTimeoutHook(t *testing.T) {
"--work-dir-retention", "--work-dir-retention",
"always", "always",
) )
if result.exitCode == 0 {
t.Fatalf("expected nonzero exit code")
}
if result.stdout != "" { if result.stdout != "" {
t.Fatalf("expected empty stdout on failure, got %q", result.stdout) t.Fatalf("expected empty stdout on failure, got %q", result.stdout)
} }

View File

@@ -216,8 +216,27 @@ Additional checks:
- duplicate explicit source IDs are rejected. - duplicate explicit source IDs are rejected.
### Transcript output ### Transcript output
Current output uses `schema.TranscriptToJSON` and is a bare JSON array of normalized segments: Transcript output is selected through an output schema registry (`internal/core/outputschema`).
- `id`, `speaker`, `start`, `end`, `text`, optional `categories`.
Supported output schemas:
- `bare-segments` (default):
- top-level JSON array of normalized segments;
- each segment includes `id`, `speaker`, `start`, `end`, `text`, optional `categories`.
- `audita-v1`:
- top-level object with:
- `schema: "audita-v1"`
- `version: "v1"`
- `segments: [...]` (same normalized segment payload).
Current status:
- `seriatim-intermediate` is not implemented yet; selecting it fails clearly as an unsupported output schema.
Selection behavior:
- CLI: `--output-schema <name>`
- file config: `output.schema: <name>`
- precedence remains runtime-wide defaults -> file config -> env -> CLI.
Both stdout transcript output and `--output` file output use the same selected output encoder.
### Glossary input ### Glossary input
YAML with `glossary` entries. Required fields per entry: YAML with `glossary` entries. Required fields per entry:
@@ -544,6 +563,15 @@ Current process reports also include:
- run-level module summary totals and failed module instance metadata. - run-level module summary totals and failed module instance metadata.
- module-level validator decisions and validator rejections. - module-level validator decisions and validator rejections.
- optional decision-level diagnostic artifact paths for validator LLM interactions when available. - optional decision-level diagnostic artifact paths for validator LLM interactions when available.
- explicit report metadata:
- report schema name;
- report schema version;
- selected output schema;
- config file version when config file input is used.
Current report schema metadata values:
- `report_metadata.report_schema_name = "audita-process-report"`
- `report_metadata.report_schema_version = "v1"`
Retention modes implemented in `ApplyRetention`: Retention modes implemented in `ApplyRetention`:
- `always`: keep all run directories. - `always`: keep all run directories.
@@ -588,6 +616,10 @@ The runtime now includes hardened subprocess behavior for parent-process callers
- retained failure diagnostics (`report.json`, `error.log`, and artifacts written before failure); - retained failure diagnostics (`report.json`, `error.log`, and artifacts written before failure);
- deterministic timeout/cancellation behavior in tests; - deterministic timeout/cancellation behavior in tests;
- redaction coverage for API keys/secrets across reports, diagnostics artifacts, and surfaced errors. - redaction coverage for API keys/secrets across reports, diagnostics artifacts, and surfaced errors.
- stable output routing behavior:
- with `--output`, stdout remains empty on success;
- without `--output`, stdout contains only transcript JSON in the selected output schema;
- `--report-json` writes report data to file only (never stdout).
Operational caller guidance is documented in [`docs/subprocess-operations.md`](docs/subprocess-operations.md). Operational caller guidance is documented in [`docs/subprocess-operations.md`](docs/subprocess-operations.md).

View File

@@ -51,6 +51,9 @@ version: 1
pipeline: pipeline:
modules: [glossary, homophones, glossary, spoken_word, grammar] modules: [glossary, homophones, glossary, spoken_word, grammar]
output:
schema: bare-segments
llm: llm:
proposal: proposal:
base_url: https://openrouter.ai/api/v1 base_url: https://openrouter.ai/api/v1
@@ -96,6 +99,12 @@ diagnostics:
retention: auto retention: auto
``` ```
`output.schema` supports the built-in output schema registry values:
- `bare-segments` (default)
- `audita-v1`
Unknown schema names fail clearly before transcript output is written.
Duration-like fields accept either: Duration-like fields accept either:
- numeric seconds (for example `120`, `3.5`), or - numeric seconds (for example `120`, `3.5`), or

88
docs/output-schemas.md Normal file
View File

@@ -0,0 +1,88 @@
# Audita Output Schemas
This document describes the built-in transcript output schema registry used by `audita process`.
## Supported schema names
### `bare-segments`
Status:
- implemented
- default output schema
Shape:
- top-level JSON array of transcript segments
Segment fields:
- `id`
- `speaker`
- `start`
- `end`
- `text`
- optional `categories`
Compatibility:
- this preserves the long-standing output shape used by existing consumers.
### `audita-v1`
Status:
- implemented
Shape:
- top-level JSON object:
- `schema`: `"audita-v1"`
- `version`: `"v1"`
- `segments`: transcript segment array
Segment fields inside `segments` match `bare-segments` segment fields.
Compatibility:
- this is the Audita-native object format with explicit schema/version metadata.
### `seriatim-intermediate`
Status:
- deferred / not implemented
Current behavior:
- selecting `seriatim-intermediate` fails clearly as an unsupported output schema.
Reason:
- a concrete, repository-backed contract for this schema has not been finalized yet.
## Selection
Choose output schema with CLI:
```sh
audita process <transcript.json> --glossary <glossary.yaml> --output-schema audita-v1
```
Or in file config:
```yaml
version: 1
output:
schema: audita-v1
```
Precedence remains:
1. defaults
2. file config
3. environment overrides
4. CLI overrides
`--output-schema` overrides `output.schema` when both are supplied.
## Output routing behavior
- With `--output`, transcript JSON is written to file using the selected schema and stdout stays empty on success.
- Without `--output`, stdout contains transcript JSON only, using the selected schema.
- `--report-json` writes report JSON to file and does not write report payloads to stdout.
## Backward-compatibility expectations
- default schema stays `bare-segments` for compatibility unless explicitly changed in a future breaking release;
- supported schema names are treated as stable public contract values;
- unsupported schema names fail before output write.

148
docs/public-contract.md Normal file
View File

@@ -0,0 +1,148 @@
# Audita Public Contract
This document defines stability expectations for Audita's external process and data interfaces.
## Scope
This contract covers:
- CLI invocation and behavior
- versioned config file behavior
- transcript/glossary input forms
- transcript output schema selection
- process report schema metadata
- diagnostics directory behavior
- stdout/stderr and exit-code behavior
- secret redaction guarantees
- compatibility and deprecation policy
## CLI stability expectations
Stable commands:
- `audita process`
- `audita config validate`
- `audita config print-effective`
For `audita process`, stable high-value flags include:
- `--config`
- `--glossary`
- `--output`
- `--report-json`
- `--modules`
- `--output-schema`
Compatibility flags and lower-level tuning flags remain available; they may be narrowed over time with explicit compatibility notes.
## Config file stability expectations
Supported file format:
- YAML
- strict unknown-field rejection
- explicit `version`
Supported version:
- `version: 1`
Precedence for `audita process`:
1. built-in defaults
2. file config
3. environment overrides
4. CLI overrides
Config source behavior:
- `--config <path>`: missing path is a clear failure
- `AUDITA_CONFIG`: missing path is a clear failure
- default `/etc/audita/config.yml`: missing file is non-fatal
## Supported transcript input forms
Audita accepts transcript JSON as either:
- a top-level array of segments
- an object with a `segments` array
Segments must satisfy the schema and validation rules enforced by `internal/core/schema`.
## Supported glossary input form
Audita accepts glossary YAML with a top-level `glossary` entry list and validates required fields per entry.
## Supported output schema names
Built-in output schema registry supports:
- `bare-segments` (default)
- `audita-v1`
`seriatim-intermediate` is planned but not implemented.
Unknown output schema names fail clearly.
## Report schema/versioning expectations
Process report payloads include `report_metadata` with:
- `report_schema_name`
- `report_schema_version`
- `output_schema`
- `config_version` when file config is used
Current values:
- `report_schema_name`: `audita-process-report`
- `report_schema_version`: `v1`
`--report-json` output and diagnostics run-dir `report.json` use the same report schema metadata.
## Diagnostics directory behavior
When diagnostics directory creation succeeds, Audita writes run artifacts including:
- invocation metadata
- redacted effective config
- transcript/normalization/chunking artifacts
- report and failure error log (when applicable)
- module/LLM diagnostics artifacts as available
Retention behavior is controlled by configured retention mode; failed runs are retained.
## Stdout/stderr behavior
Success behavior:
- with `--output`, stdout is empty
- without `--output`, stdout contains only transcript JSON in selected output schema
- report JSON is not written to stdout
Failure behavior:
- stderr contains human-readable error summary
- nonzero exit
- diagnostics path is printed when available
## Exit-code behavior
- `0`: success
- nonzero: failure
Treat any nonzero exit as a failed invocation.
## Secret redaction guarantees
Audita redacts API keys and authorization secrets from:
- effective config outputs (`audita config print-effective`, diagnostics effective-config artifact)
- report artifacts
- LLM diagnostics artifacts
- surfaced request/response error messages
Config files should reference secrets via environment variable names (`api_key_env`) rather than embedding secret values.
## Compatibility and deprecation policy
- Existing stable schema names, report metadata keys, and top-level command behavior are treated as public contract.
- Compatibility inputs (legacy flags/env aliases) may remain during transition windows.
- Any planned removal or behavior change should include clear compatibility notes and migration guidance.
## Breaking changes after 1.0
After 1.0, breaking changes include, for example:
- changing default success/failure exit-code semantics
- changing stdout/stderr routing semantics
- silently changing default output schema shape
- removing supported output schema names without compatibility strategy
- changing report schema fields or meanings incompatibly
- changing config version semantics incompatibly without version bump
Additive fields, additive diagnostics, and new optional schema names are generally non-breaking when existing behavior remains intact.

View File

@@ -398,6 +398,27 @@ The second command should use the same redaction behavior as run diagnostics.
Document and implement the stable boundaries that external callers can rely on for 1.0. Document and implement the stable boundaries that external callers can rely on for 1.0.
## Implementation status (2026-05-13)
This workstream is now partially implemented in the repository:
- output schema registry is implemented with supported schemas:
- `bare-segments` (default);
- `audita-v1`;
- `audita process --output-schema <name>` is implemented;
- versioned file config supports `output.schema`;
- process reports include explicit report metadata with:
- report schema name/version;
- selected output schema;
- config file version when a file config is used;
- public contract documentation and output schema documentation are now present in:
- `docs/public-contract.md`
- `docs/output-schemas.md`
Current intentional gap:
- `seriatim-intermediate` remains deferred because no concrete implemented contract exists yet.
This status update only applies to output schema/report metadata/public contract documentation. Validator refactors, prompt-asset registries, scheduler utilization diagnostics, correction ledgers, and generated summaries remain planned.
This phase should happen early because Audita is both a user-facing CLI and a subprocess dependency. The public contract should guide the remaining implementation decisions rather than merely documenting them after the fact. This phase should happen early because Audita is both a user-facing CLI and a subprocess dependency. The public contract should guide the remaining implementation decisions rather than merely documenting them after the fact.
## Public contract document ## Public contract document

View File

@@ -15,6 +15,7 @@ audita process <transcript.json> \
Recommended additions: Recommended additions:
- `--config <path>` to select an explicit versioned config file. - `--config <path>` to select an explicit versioned config file.
- `--output-schema <bare-segments|audita-v1>` to select transcript output shape.
- `--work-dir <dir>` to control diagnostics location. - `--work-dir <dir>` to control diagnostics location.
- `--work-dir-retention <always|auto|never>` to control retained run directories. - `--work-dir-retention <always|auto|never>` to control retained run directories.
- `--total-llm-concurrency`, `--proposal-llm-concurrency`, and `--validation-llm-concurrency` when orchestration needs explicit LLM throughput controls. - `--total-llm-concurrency`, `--proposal-llm-concurrency`, and `--validation-llm-concurrency` when orchestration needs explicit LLM throughput controls.
@@ -35,7 +36,7 @@ Recommended additions:
## Output file behavior ## Output file behavior
- `--output` writes transcript JSON to the provided path. - `--output` writes transcript JSON in the selected output schema to the provided path.
- Output write failures return nonzero and surface actionable errors. - Output write failures return nonzero and surface actionable errors.
- The command does not silently ignore output write errors. - The command does not silently ignore output write errors.

View File

@@ -17,6 +17,7 @@ import (
"gitea.maximumdirect.net/eric/audita/internal/core/diagnostics" "gitea.maximumdirect.net/eric/audita/internal/core/diagnostics"
coreio "gitea.maximumdirect.net/eric/audita/internal/core/io" coreio "gitea.maximumdirect.net/eric/audita/internal/core/io"
"gitea.maximumdirect.net/eric/audita/internal/core/normalization" "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/reporting"
"gitea.maximumdirect.net/eric/audita/internal/core/schema" "gitea.maximumdirect.net/eric/audita/internal/core/schema"
"gitea.maximumdirect.net/eric/audita/internal/framework/contracts" "gitea.maximumdirect.net/eric/audita/internal/framework/contracts"
@@ -53,6 +54,7 @@ type processInvocation struct {
Config config.Config Config config.Config
ConfigPath string ConfigPath string
ConfigSource string ConfigSource string
ConfigVersion *int
ExplicitModules bool ExplicitModules bool
} }
@@ -84,6 +86,7 @@ var processRunner = func(inv processInvocation, stdout io.Writer) (*normalizatio
ReportJSONPath: inv.ReportJSONPath, ReportJSONPath: inv.ReportJSONPath,
ConfigPath: inv.ConfigPath, ConfigPath: inv.ConfigPath,
ConfigSource: inv.ConfigSource, ConfigSource: inv.ConfigSource,
ConfigVersion: inv.ConfigVersion,
TranscriptDescription: inv.Config.TranscriptDescription, TranscriptDescription: inv.Config.TranscriptDescription,
Modules: append([]string(nil), inv.Config.Modules...), Modules: append([]string(nil), inv.Config.Modules...),
}); err != nil { }); err != nil {
@@ -256,7 +259,11 @@ var processRunner = func(inv processInvocation, stdout io.Writer) (*normalizatio
workingTranscript = runnerResult.FinalTranscript 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 { if err != nil {
return fail("serialization", err, runOutput) return fail("serialization", err, runOutput)
} }
@@ -369,6 +376,7 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
} }
cfg := config.Default() cfg := config.Default()
var configVersion *int
if configPath != "" { if configPath != "" {
fileCfg, fileErr := config.LoadFileConfig(configPath) fileCfg, fileErr := config.LoadFileConfig(configPath)
if fileErr != nil { 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) fmt.Fprintf(stderr, "audita process: invalid config file: %v\n", applyErr)
return 2 return 2
} }
configVersion = &fileCfg.Version
} }
if err := cfg.ApplyEnvOverrides(); err != nil { if err := cfg.ApplyEnvOverrides(); err != nil {
fmt.Fprintf(stderr, "audita process: invalid environment configuration: %v\n", err) 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": case "modules":
explicitModules = true explicitModules = true
overrides.ModulesCSV = pFlags.modules overrides.ModulesCSV = pFlags.modules
case "output-schema":
overrides.OutputSchema = pFlags.outputSchema
case "llm-api-key": case "llm-api-key":
overrides.PrimaryLLMAPIKey = pFlags.llmAPIKey overrides.PrimaryLLMAPIKey = pFlags.llmAPIKey
case "validation-llm-api-key": case "validation-llm-api-key":
@@ -504,6 +515,7 @@ func runProcess(args []string, stdout, stderr io.Writer) int {
Config: cfg, Config: cfg,
ConfigPath: configPath, ConfigPath: configPath,
ConfigSource: configSource, ConfigSource: configSource,
ConfigVersion: configVersion,
ExplicitModules: explicitModules, 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 { 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{ report := reporting.ProcessReport{
ReportMetadata: reporting.ReportMetadata{
ReportSchemaName: reporting.DefaultProcessReportSchemaName,
ReportSchemaVersion: reporting.DefaultProcessReportSchemaVersion,
OutputSchema: inv.Config.OutputSchema,
ConfigVersion: inv.ConfigVersion,
},
Phase: "default_pipeline", Phase: "default_pipeline",
Status: status, Status: status,
Operation: "process", Operation: "process",
@@ -828,6 +846,7 @@ type processFlags struct {
outputPath *string outputPath *string
reportJSONPath *string reportJSONPath *string
modules *string modules *string
outputSchema *string
llmAPIKey *string llmAPIKey *string
validationLLMAPIKey *string validationLLMAPIKey *string
model *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"), outputPath: fs.String("output", "", "Path to corrected transcript JSON output file"),
reportJSONPath: fs.String("report-json", "", "Path to machine-readable report 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"), 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"), 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"), 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"), model: fs.String("model", cfg.PrimaryLLM.Model, "Primary LLM model name"),

View File

@@ -57,6 +57,7 @@ func TestRunProcessHelpListsExpectedFlags(t *testing.T) {
"--config", "--config",
"--glossary", "--glossary",
"--output", "--output",
"--output-schema",
"--report-json", "--report-json",
"--modules", "--modules",
"--llm-api-key", "--llm-api-key",
@@ -1158,6 +1159,16 @@ func TestRunProcessReportJSONSuccessIncludesNormalizationSummary(t *testing.T) {
if report.Status != "success" { if report.Status != "success" {
t.Fatalf("expected success status, got %q", report.Status) 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" { if report.Operation != "process" {
t.Fatalf("expected operation process, got %q", report.Operation) t.Fatalf("expected operation process, got %q", report.Operation)
} }
@@ -1291,12 +1302,65 @@ func TestRunProcessReportJSONAndRunDirReportShareDiagnosticsMetadata(t *testing.
if externalReport.Diagnostics == nil || runDirReport.Diagnostics == nil { if externalReport.Diagnostics == nil || runDirReport.Diagnostics == nil {
t.Fatalf("expected diagnostics metadata in both reports") 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 { if *externalReport.Diagnostics != *runDirReport.Diagnostics {
t.Fatalf("expected same diagnostics metadata in --report-json and run-dir report\nexternal=%+v\nrun-dir=%+v", t.Fatalf("expected same diagnostics metadata in --report-json and run-dir report\nexternal=%+v\nrun-dir=%+v",
*externalReport.Diagnostics, *runDirReport.Diagnostics) *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) { func TestRunProcessWritesNormalizationDiagnosticsArtifacts(t *testing.T) {
var stdout bytes.Buffer var stdout bytes.Buffer
var stderr 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 { func fixturePath(name string) string {
return filepath.Join("testdata", name) return filepath.Join("testdata", name)
} }

View File

@@ -15,6 +15,7 @@ const (
const ( const (
DefaultModulesCSV = "glossary,homophones,glossary,spoken_word,grammar" DefaultModulesCSV = "glossary,homophones,glossary,spoken_word,grammar"
DefaultOutputSchema = "bare-segments"
DefaultPrimaryModel = "openrouter/google/gemma-4-31b-it" DefaultPrimaryModel = "openrouter/google/gemma-4-31b-it"
DefaultPrimaryBaseURL = "https://openrouter.ai/api/v1" DefaultPrimaryBaseURL = "https://openrouter.ai/api/v1"
DefaultPrimaryLLMTimeoutSeconds = 600 DefaultPrimaryLLMTimeoutSeconds = 600
@@ -35,6 +36,7 @@ const (
type Config struct { type Config struct {
Modules []string Modules []string
OutputSchema string
PrimaryLLM LLMConfig PrimaryLLM LLMConfig
ValidationLLM ValidationLLMConfig ValidationLLM ValidationLLMConfig
TotalLLMConcurrency int TotalLLMConcurrency int
@@ -91,7 +93,8 @@ func Default() Config {
modules, _ := ParseModulesCSV(DefaultModulesCSV) modules, _ := ParseModulesCSV(DefaultModulesCSV)
return Config{ return Config{
Modules: modules, Modules: modules,
OutputSchema: DefaultOutputSchema,
PrimaryLLM: LLMConfig{ PrimaryLLM: LLMConfig{
Model: DefaultPrimaryModel, Model: DefaultPrimaryModel,
BaseURL: DefaultPrimaryBaseURL, BaseURL: DefaultPrimaryBaseURL,

View File

@@ -12,6 +12,9 @@ func TestDefaultConfigValues(t *testing.T) {
if got, want := strings.Join(cfg.Modules, ","), DefaultModulesCSV; got != want { if got, want := strings.Join(cfg.Modules, ","), DefaultModulesCSV; got != want {
t.Fatalf("modules mismatch: got %q want %q", got, want) t.Fatalf("modules mismatch: got %q want %q", got, want)
} }
if cfg.OutputSchema != DefaultOutputSchema {
t.Fatalf("unexpected default output schema: %q", cfg.OutputSchema)
}
if cfg.PrimaryLLM.Model != DefaultPrimaryModel { if cfg.PrimaryLLM.Model != DefaultPrimaryModel {
t.Fatalf("unexpected default primary model: %q", cfg.PrimaryLLM.Model) t.Fatalf("unexpected default primary model: %q", cfg.PrimaryLLM.Model)
} }
@@ -186,12 +189,14 @@ func TestApplyCLIOverridesPrecedence(t *testing.T) {
model := "cli-model" model := "cli-model"
workDir := "/cli/work" workDir := "/cli/work"
modules := "grammar" modules := "grammar"
outputSchema := "audita-v1"
totalLLMConcurrency := 5 totalLLMConcurrency := 5
proposalLLMConcurrency := 3 proposalLLMConcurrency := 3
overrides := CLIOverrides{ overrides := CLIOverrides{
PrimaryModel: &model, PrimaryModel: &model,
WorkDir: &workDir, WorkDir: &workDir,
ModulesCSV: &modules, ModulesCSV: &modules,
OutputSchema: &outputSchema,
TotalLLMConcurrency: &totalLLMConcurrency, TotalLLMConcurrency: &totalLLMConcurrency,
ProposalLLMConcurrency: &proposalLLMConcurrency, ProposalLLMConcurrency: &proposalLLMConcurrency,
} }
@@ -209,6 +214,9 @@ func TestApplyCLIOverridesPrecedence(t *testing.T) {
if !reflect.DeepEqual(cfg.Modules, []string{"grammar"}) { if !reflect.DeepEqual(cfg.Modules, []string{"grammar"}) {
t.Fatalf("unexpected modules: %#v", cfg.Modules) t.Fatalf("unexpected modules: %#v", cfg.Modules)
} }
if cfg.OutputSchema != "audita-v1" {
t.Fatalf("expected CLI output schema override, got %q", cfg.OutputSchema)
}
if cfg.TotalLLMConcurrency != 5 { if cfg.TotalLLMConcurrency != 5 {
t.Fatalf("expected CLI total concurrency override, got %d", cfg.TotalLLMConcurrency) t.Fatalf("expected CLI total concurrency override, got %d", cfg.TotalLLMConcurrency)
} }
@@ -275,6 +283,7 @@ func TestApplyCLIOverridesCanonicalTotalWinsLegacyAlias(t *testing.T) {
func TestValidationFailures(t *testing.T) { func TestValidationFailures(t *testing.T) {
cfg := Default() cfg := Default()
cfg.OutputSchema = "unknown-schema"
cfg.PrimaryLLM.TimeoutSeconds = -1 cfg.PrimaryLLM.TimeoutSeconds = -1
cfg.TotalLLMConcurrency = 0 cfg.TotalLLMConcurrency = 0
cfg.ProposalLLMConcurrency = 0 cfg.ProposalLLMConcurrency = 0
@@ -301,6 +310,7 @@ func TestValidationFailures(t *testing.T) {
"min section tokens", "min section tokens",
"grammar confidence threshold", "grammar confidence threshold",
"work dir retention", "work dir retention",
"unsupported output schema",
} { } {
if !strings.Contains(message, expected) { if !strings.Contains(message, expected) {
t.Fatalf("expected error to contain %q, got %q", expected, message) t.Fatalf("expected error to contain %q, got %q", expected, message)

View File

@@ -16,32 +16,37 @@ const SupportedFileConfigVersion = 1
var envVarNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) var envVarNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
type FileConfig struct { type FileConfig struct {
Version int `yaml:"version"` Version int `yaml:"version"`
Pipeline *FileConfigPipeline `yaml:"pipeline,omitempty"` Pipeline *FileConfigPipeline `yaml:"pipeline,omitempty"`
LLM *FileConfigLLM `yaml:"llm,omitempty"` Output *FileConfigOutput `yaml:"output,omitempty"`
Concurrency *FileConfigConcurrency `yaml:"concurrency,omitempty"` LLM *FileConfigLLM `yaml:"llm,omitempty"`
Chunking *FileConfigChunking `yaml:"chunking,omitempty"` Concurrency *FileConfigConcurrency `yaml:"concurrency,omitempty"`
Normalization *FileConfigNormalization `yaml:"normalization,omitempty"` Chunking *FileConfigChunking `yaml:"chunking,omitempty"`
Thresholds *FileConfigThresholds `yaml:"thresholds,omitempty"` Normalization *FileConfigNormalization `yaml:"normalization,omitempty"`
Context *FileConfigContext `yaml:"context,omitempty"` Thresholds *FileConfigThresholds `yaml:"thresholds,omitempty"`
Diagnostics *FileConfigDiagnostics `yaml:"diagnostics,omitempty"` Context *FileConfigContext `yaml:"context,omitempty"`
Diagnostics *FileConfigDiagnostics `yaml:"diagnostics,omitempty"`
} }
type FileConfigPipeline struct { type FileConfigPipeline struct {
Modules []string `yaml:"modules,omitempty"` Modules []string `yaml:"modules,omitempty"`
} }
type FileConfigOutput struct {
Schema *string `yaml:"schema,omitempty"`
}
type FileConfigLLM struct { type FileConfigLLM struct {
Proposal *FileConfigLLMTarget `yaml:"proposal,omitempty"` Proposal *FileConfigLLMTarget `yaml:"proposal,omitempty"`
Validation *FileConfigLLMTarget `yaml:"validation,omitempty"` Validation *FileConfigLLMTarget `yaml:"validation,omitempty"`
} }
type FileConfigLLMTarget struct { type FileConfigLLMTarget struct {
BaseURL *string `yaml:"base_url,omitempty"` BaseURL *string `yaml:"base_url,omitempty"`
Model *string `yaml:"model,omitempty"` Model *string `yaml:"model,omitempty"`
APIKeyEnv *string `yaml:"api_key_env,omitempty"` APIKeyEnv *string `yaml:"api_key_env,omitempty"`
Timeout *fileConfigDurationOrInt `yaml:"timeout,omitempty"` Timeout *fileConfigDurationOrInt `yaml:"timeout,omitempty"`
MaxRetries *int `yaml:"max_retries,omitempty"` MaxRetries *int `yaml:"max_retries,omitempty"`
} }
type FileConfigConcurrency struct { type FileConfigConcurrency struct {
@@ -194,6 +199,9 @@ func (c *Config) applyFileConfigWithLookup(fileCfg FileConfig, lookup func(strin
if fileCfg.Pipeline != nil && len(fileCfg.Pipeline.Modules) > 0 { if fileCfg.Pipeline != nil && len(fileCfg.Pipeline.Modules) > 0 {
c.Modules = append([]string(nil), fileCfg.Pipeline.Modules...) c.Modules = append([]string(nil), fileCfg.Pipeline.Modules...)
} }
if fileCfg.Output != nil && fileCfg.Output.Schema != nil {
c.OutputSchema = strings.TrimSpace(*fileCfg.Output.Schema)
}
if fileCfg.LLM != nil { if fileCfg.LLM != nil {
if fileCfg.LLM.Proposal != nil { if fileCfg.LLM.Proposal != nil {

View File

@@ -11,6 +11,8 @@ func TestParseFileConfigYAMLValid(t *testing.T) {
version: 1 version: 1
pipeline: pipeline:
modules: [glossary, homophones, grammar] modules: [glossary, homophones, grammar]
output:
schema: audita-v1
llm: llm:
proposal: proposal:
base_url: https://example.test/v1 base_url: https://example.test/v1
@@ -58,6 +60,9 @@ diagnostics:
if cfg.Pipeline == nil || len(cfg.Pipeline.Modules) != 3 { if cfg.Pipeline == nil || len(cfg.Pipeline.Modules) != 3 {
t.Fatalf("unexpected pipeline modules: %#v", cfg.Pipeline) t.Fatalf("unexpected pipeline modules: %#v", cfg.Pipeline)
} }
if cfg.Output == nil || cfg.Output.Schema == nil || *cfg.Output.Schema != "audita-v1" {
t.Fatalf("expected output schema audita-v1, got %#v", cfg.Output)
}
if cfg.LLM == nil || cfg.LLM.Proposal == nil || cfg.LLM.Validation == nil { if cfg.LLM == nil || cfg.LLM.Proposal == nil || cfg.LLM.Validation == nil {
t.Fatalf("expected llm proposal+validation blocks") t.Fatalf("expected llm proposal+validation blocks")
} }
@@ -78,13 +83,13 @@ version: 1
pipeline: pipeline:
modules: [grammar] modules: [grammar]
output: output:
schema: v1 unknown: v1
` `
_, err := ParseFileConfigYAML([]byte(raw)) _, err := ParseFileConfigYAML([]byte(raw))
if err == nil { if err == nil {
t.Fatalf("expected unknown field error") t.Fatalf("expected unknown field error")
} }
if !strings.Contains(err.Error(), "field output not found") { if !strings.Contains(err.Error(), "field unknown not found") {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
} }
@@ -116,6 +121,8 @@ func TestApplyFileConfigParsesAndMergesFields(t *testing.T) {
version: 1 version: 1
pipeline: pipeline:
modules: [spoken_word, grammar] modules: [spoken_word, grammar]
output:
schema: audita-v1
llm: llm:
proposal: proposal:
model: provider/new-proposal model: provider/new-proposal
@@ -164,6 +171,9 @@ diagnostics:
if strings.Join(cfg.Modules, ",") != "spoken_word,grammar" { if strings.Join(cfg.Modules, ",") != "spoken_word,grammar" {
t.Fatalf("unexpected modules: %#v", cfg.Modules) t.Fatalf("unexpected modules: %#v", cfg.Modules)
} }
if cfg.OutputSchema != "audita-v1" {
t.Fatalf("unexpected output schema: %q", cfg.OutputSchema)
}
if cfg.PrimaryLLM.Model != "provider/new-proposal" { if cfg.PrimaryLLM.Model != "provider/new-proposal" {
t.Fatalf("unexpected proposal model: %q", cfg.PrimaryLLM.Model) t.Fatalf("unexpected proposal model: %q", cfg.PrimaryLLM.Model)
} }

View File

@@ -7,6 +7,7 @@ import (
type CLIOverrides struct { type CLIOverrides struct {
ModulesCSV *string ModulesCSV *string
OutputSchema *string
PrimaryLLMAPIKey *string PrimaryLLMAPIKey *string
ValidationLLMAPIKey *string ValidationLLMAPIKey *string
PrimaryModel *string PrimaryModel *string
@@ -46,6 +47,9 @@ func (c *Config) ApplyCLIOverrides(overrides CLIOverrides) error {
} }
c.Modules = modules c.Modules = modules
} }
if overrides.OutputSchema != nil {
c.OutputSchema = strings.TrimSpace(*overrides.OutputSchema)
}
if overrides.PrimaryLLMAPIKey != nil { if overrides.PrimaryLLMAPIKey != nil {
c.PrimaryLLM.APIKey = *overrides.PrimaryLLMAPIKey c.PrimaryLLM.APIKey = *overrides.PrimaryLLMAPIKey

View File

@@ -17,6 +17,15 @@ func (c Config) Validate() error {
break break
} }
} }
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))
}
}
if c.PrimaryLLM.TimeoutSeconds <= 0 { if c.PrimaryLLM.TimeoutSeconds <= 0 {
issues = append(issues, "primary llm timeout seconds must be greater than zero") issues = append(issues, "primary llm timeout seconds must be greater than zero")

View File

@@ -55,6 +55,7 @@ type InvocationMetadata struct {
ReportJSONPath string `json:"report_json_path,omitempty"` ReportJSONPath string `json:"report_json_path,omitempty"`
ConfigPath string `json:"config_path,omitempty"` ConfigPath string `json:"config_path,omitempty"`
ConfigSource string `json:"config_source,omitempty"` ConfigSource string `json:"config_source,omitempty"`
ConfigVersion *int `json:"config_version,omitempty"`
TranscriptDescription string `json:"transcript_description,omitempty"` TranscriptDescription string `json:"transcript_description,omitempty"`
Modules []string `json:"modules"` Modules []string `json:"modules"`
RunID string `json:"run_id"` RunID string `json:"run_id"`

View File

@@ -0,0 +1,72 @@
package outputschema
import (
"encoding/json"
"fmt"
"strings"
"gitea.maximumdirect.net/eric/audita/internal/core/schema"
)
const (
SchemaBareSegments = "bare-segments"
SchemaAuditaV1 = "audita-v1"
)
type Encoder func(*schema.Transcript) ([]byte, error)
type Definition struct {
Key string
Encoder Encoder
}
var definitions = map[string]Definition{
SchemaBareSegments: {
Key: SchemaBareSegments,
Encoder: schema.TranscriptToJSON,
},
SchemaAuditaV1: {
Key: SchemaAuditaV1,
Encoder: encodeAuditaV1,
},
}
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 {
return Definition{}, fmt.Errorf("unsupported output schema %q", normalized)
}
return def, nil
}
func encodeAuditaV1(transcript *schema.Transcript) ([]byte, error) {
if transcript == nil {
transcript = &schema.Transcript{}
}
payload := map[string]any{
"schema": "audita-v1",
"version": "v1",
"segments": func() []map[string]any {
out := make([]map[string]any, len(transcript.Segments))
for i, s := range transcript.Segments {
item := map[string]any{
"id": s.ID,
"speaker": s.Speaker,
"start": s.Start,
"end": s.End,
"text": s.Text,
}
if len(s.Categories) > 0 {
item["categories"] = s.Categories
}
out[i] = item
}
return out
}(),
}
return json.MarshalIndent(payload, "", " ")
}

View File

@@ -0,0 +1,58 @@
package outputschema
import (
"encoding/json"
"strings"
"testing"
"gitea.maximumdirect.net/eric/audita/internal/core/schema"
)
func tinyTranscript() *schema.Transcript {
return &schema.Transcript{Segments: []schema.Segment{
{ID: 1, Speaker: "A", Start: 0, End: 1, Text: "hello"},
}}
}
func TestResolveBareSegments(t *testing.T) {
def, err := Resolve(SchemaBareSegments)
if err != nil {
t.Fatalf("Resolve error: %v", err)
}
raw, err := def.Encoder(tinyTranscript())
if err != nil {
t.Fatalf("encode error: %v", err)
}
if !strings.HasPrefix(strings.TrimSpace(string(raw)), "[") {
t.Fatalf("expected bare-segments array output, got %s", string(raw))
}
}
func TestResolveAuditaV1(t *testing.T) {
def, err := Resolve(SchemaAuditaV1)
if err != nil {
t.Fatalf("Resolve error: %v", err)
}
raw, err := def.Encoder(tinyTranscript())
if err != nil {
t.Fatalf("encode error: %v", err)
}
var out struct {
Schema string `json:"schema"`
Version string `json:"version"`
Segments []map[string]any `json:"segments"`
}
if err := json.Unmarshal(raw, &out); err != nil {
t.Fatalf("unmarshal error: %v", err)
}
if out.Schema != "audita-v1" || out.Version != "v1" || len(out.Segments) != 1 {
t.Fatalf("unexpected audita-v1 output: %+v", out)
}
}
func TestResolveUnknown(t *testing.T) {
_, err := Resolve("seriatim-intermediate")
if err == nil || !strings.Contains(err.Error(), "unsupported output schema") {
t.Fatalf("expected unsupported output schema error, got %v", err)
}
}

View File

@@ -10,6 +10,7 @@ import (
) )
type ProcessReport struct { type ProcessReport struct {
ReportMetadata ReportMetadata `json:"report_metadata"`
Phase string `json:"phase"` Phase string `json:"phase"`
Status string `json:"status"` Status string `json:"status"`
Operation string `json:"operation"` Operation string `json:"operation"`
@@ -32,6 +33,18 @@ type ProcessReport struct {
ModuleResults []ModuleReport `json:"module_results,omitempty"` ModuleResults []ModuleReport `json:"module_results,omitempty"`
} }
const (
DefaultProcessReportSchemaName = "audita-process-report"
DefaultProcessReportSchemaVersion = "v1"
)
type ReportMetadata struct {
ReportSchemaName string `json:"report_schema_name"`
ReportSchemaVersion string `json:"report_schema_version"`
OutputSchema string `json:"output_schema"`
ConfigVersion *int `json:"config_version,omitempty"`
}
type ModuleReport struct { type ModuleReport struct {
ModuleKey string `json:"module_key"` ModuleKey string `json:"module_key"`
ModuleInstance string `json:"module_instance"` ModuleInstance string `json:"module_instance"`

View File

@@ -11,6 +11,11 @@ import (
func TestProcessReportModuleResultsJSONSuccessAndSkipped(t *testing.T) { func TestProcessReportModuleResultsJSONSuccessAndSkipped(t *testing.T) {
now := time.Now().UTC() now := time.Now().UTC()
report := ProcessReport{ report := ProcessReport{
ReportMetadata: ReportMetadata{
ReportSchemaName: DefaultProcessReportSchemaName,
ReportSchemaVersion: DefaultProcessReportSchemaVersion,
OutputSchema: "bare-segments",
},
Phase: "default_pipeline", Phase: "default_pipeline",
Status: "success", Status: "success",
ModuleResults: []ModuleReport{ ModuleResults: []ModuleReport{
@@ -75,11 +80,21 @@ func TestProcessReportModuleResultsJSONSuccessAndSkipped(t *testing.T) {
if parsed.ModulesSummary == nil || parsed.ModulesSummary.TotalSkippedChanges != 1 { if parsed.ModulesSummary == nil || parsed.ModulesSummary.TotalSkippedChanges != 1 {
t.Fatalf("unexpected module summary: %+v", parsed.ModulesSummary) t.Fatalf("unexpected module summary: %+v", parsed.ModulesSummary)
} }
if parsed.ReportMetadata.ReportSchemaName != DefaultProcessReportSchemaName ||
parsed.ReportMetadata.ReportSchemaVersion != DefaultProcessReportSchemaVersion ||
parsed.ReportMetadata.OutputSchema != "bare-segments" {
t.Fatalf("unexpected report metadata after roundtrip: %+v", parsed.ReportMetadata)
}
} }
func TestProcessReportModuleResultsJSONFailedModule(t *testing.T) { func TestProcessReportModuleResultsJSONFailedModule(t *testing.T) {
now := time.Now().UTC() now := time.Now().UTC()
report := ProcessReport{ report := ProcessReport{
ReportMetadata: ReportMetadata{
ReportSchemaName: DefaultProcessReportSchemaName,
ReportSchemaVersion: DefaultProcessReportSchemaVersion,
OutputSchema: "audita-v1",
},
Phase: "default_pipeline", Phase: "default_pipeline",
Status: "failed", Status: "failed",
ModuleResults: []ModuleReport{ ModuleResults: []ModuleReport{
@@ -106,6 +121,9 @@ func TestProcessReportModuleResultsJSONFailedModule(t *testing.T) {
if _, ok := decoded["module_results"]; !ok { if _, ok := decoded["module_results"]; !ok {
t.Fatalf("expected module_results field") t.Fatalf("expected module_results field")
} }
if _, ok := decoded["report_metadata"]; !ok {
t.Fatalf("expected report_metadata field")
}
if _, ok := decoded["modules_summary"]; !ok { if _, ok := decoded["modules_summary"]; !ok {
t.Fatalf("expected modules_summary field") t.Fatalf("expected modules_summary field")
} }