Update Notarius integration and plan references

This commit is contained in:
2026-08-29 14:41:22 +00:00
parent e433c86203
commit 42ed81cbe1
11 changed files with 1450 additions and 114 deletions

View File

@@ -6,7 +6,7 @@ import (
"time"
)
const ReceiptSchemaVersion = "notarius.run-result.v1"
const ReceiptSchemaVersion = "notarius.run-result.v2"
// Runner is the adapter boundary for a complete Notarius pipeline invocation.
type Runner interface {
@@ -28,16 +28,36 @@ type RunRequest struct {
// Receipt is the transport-neutral successful run receipt.
type Receipt struct {
SchemaVersion string
RunID string
PipelineID string
OutputDirectory string
IndexFile string
NormalizedOutputCount int
RejectedOutputCount int
WarningCount int
ValidationStatus string
DebugDirectory string
SchemaVersion string
RunID string
PipelineID string
OutputDirectory string
IndexFile string
NormalizedOutputCount int
RejectedOutputCount int
WarningGroupCount int
WarningOccurrenceCount int
DiagnosticGroupCount int
DiagnosticOccurrenceCount int
DiagnosticsTruncated bool
ValidationStatus string
ValidationSummaries []ValidationSummary
DebugDirectory string
}
// ValidationSummary retains the bounded outcome of one Notarius producer result.
type ValidationSummary struct {
Stage string
StepID string
LaneID string
ModuleKey string
ChunkID string
Status string
RejectingValidators []string
ReasonCodes []string
IncompleteValidators []string
ProducerAttemptCount int
TerminalAction string
}
// LaneDescriptor identifies one normalized lane payload discovered through the index.
@@ -72,6 +92,8 @@ type Index struct {
RejectedPath string
WarningsFile string
WarningsPath string
DiagnosticsFile string
DiagnosticsPath string
Lanes []LaneDescriptor
ChunkMap *PipelineDescriptor
EvidenceContext *PipelineDescriptor
@@ -90,8 +112,29 @@ type RejectionSummary struct {
// WarningSummary retains structured warning identity without free-form messages.
type WarningSummary struct {
Scope string
ReasonCode string
Disposition string
Category string
ReasonCode string
Origin DiagnosticOrigin
OccurrenceCount int
}
// DiagnosticOrigin identifies the framework-owned pipeline location of a finding.
type DiagnosticOrigin struct {
Stage string
StepID string
LaneID string
ModuleKey string
ValidatorKey string
}
// DiagnosticSummary retains bounded advisory or observation group metadata.
type DiagnosticSummary struct {
Disposition string
Category string
ReasonCode string
Origin DiagnosticOrigin
OccurrenceCount int
}
// RunResult describes a successfully decoded and validated Notarius bundle.
@@ -105,4 +148,5 @@ type RunResult struct {
Duration time.Duration
Rejections []RejectionSummary
Warnings []WarningSummary
Diagnostics []DiagnosticSummary
}

View File

@@ -15,13 +15,19 @@ import (
)
const (
maxReceiptBytes = 1 << 20
maxIndexBytes = 4 << 20
maxSummaryBytes = 4 << 20
canonicalIndexFile = "index.json"
canonicalManifestFile = "manifest.json"
canonicalRejectedFile = "rejected.json"
canonicalWarningsFile = "warnings.json"
maxReceiptBytes = 1 << 20
maxIndexBytes = 4 << 20
maxSummaryBytes = 4 << 20
canonicalIndexFile = "index.json"
canonicalManifestFile = "manifest.json"
canonicalRejectedFile = "rejected.json"
canonicalWarningsFile = "warnings.json"
canonicalDiagnosticsFile = "diagnostics.json"
warningsSchemaVersion = "notarius.warnings.v2"
diagnosticsSchemaVersion = "notarius.diagnostics.v1"
maxWarningGroups = 128
maxDiagnosticGroups = 256
maxFindingSamples = 3
)
type subprocessRun func(context.Context, subprocess.RunRequest) (subprocess.RunResult, error)
@@ -95,12 +101,30 @@ func (r *SubprocessRunner) Run(ctx context.Context, req RunRequest) (RunResult,
if err != nil {
return baseResult, err
}
diagnostics, diagnosticOccurrences, diagnosticsTruncated, err := loadDiagnostics(index.DiagnosticsPath)
if err != nil {
return baseResult, err
}
if receipt.NormalizedOutputCount != len(index.Lanes) || receipt.RejectedOutputCount != len(rejections) ||
receipt.WarningGroupCount != len(warnings) || receipt.DiagnosticGroupCount != len(diagnostics) {
return baseResult, fmt.Errorf("notarius receipt counts do not match published bundle")
}
warningOccurrences, err := sumWarningOccurrences(warnings)
if err != nil {
return baseResult, err
}
if receipt.WarningOccurrenceCount != warningOccurrences ||
receipt.DiagnosticOccurrenceCount != diagnosticOccurrences ||
receipt.DiagnosticsTruncated != diagnosticsTruncated {
return baseResult, fmt.Errorf("notarius receipt occurrence counts do not match published bundle")
}
baseResult.Receipt = receipt
baseResult.Index = index
baseResult.BundleRoot = bundleRoot
baseResult.Rejections = rejections
baseResult.Warnings = warnings
baseResult.Diagnostics = diagnostics
return baseResult, nil
}
@@ -154,16 +178,74 @@ func validateRunRequest(req RunRequest) error {
}
type receiptDocument struct {
SchemaVersion string `json:"schema_version"`
RunID string `json:"run_id"`
PipelineID string `json:"pipeline_id"`
OutputDirectory string `json:"output_directory"`
IndexFile string `json:"index_file"`
NormalizedOutputCount *int `json:"normalized_output_count"`
RejectedOutputCount *int `json:"rejected_output_count"`
WarningCount *int `json:"warning_count"`
ValidationStatus string `json:"validation_status"`
DebugDirectory string `json:"debug_directory"`
SchemaVersion string `json:"schema_version"`
RunID string `json:"run_id"`
PipelineID string `json:"pipeline_id"`
OutputDirectory string `json:"output_directory"`
IndexFile string `json:"index_file"`
NormalizedOutputCount *int `json:"normalized_output_count"`
RejectedOutputCount *int `json:"rejected_output_count"`
WarningGroupCount *int `json:"warning_group_count"`
WarningOccurrenceCount *int `json:"warning_occurrence_count"`
DiagnosticGroupCount *int `json:"diagnostic_group_count"`
DiagnosticOccurrenceCount *int `json:"diagnostic_occurrence_count"`
DiagnosticsTruncated *bool `json:"diagnostics_truncated"`
ValidationStatus string `json:"validation_status"`
ValidationSummaries []validationSummaryDocument `json:"validation_summaries"`
DebugDirectory string `json:"debug_directory"`
}
type validationSummaryDocument struct {
Stage string `json:"stage"`
StepID string `json:"step_id"`
LaneID string `json:"lane_id"`
ModuleKey string `json:"module_key"`
ChunkID string `json:"chunk_id"`
Status string `json:"status"`
RejectingValidators []string `json:"rejecting_validators"`
ReasonCodes []string `json:"reason_codes"`
IncompleteValidators []string `json:"incomplete_validators"`
ProducerAttemptCount *int `json:"producer_attempt_count"`
TerminalAction string `json:"terminal_action"`
}
func validValidationStatus(value string) bool {
switch value {
case "approved", "rejected", "incomplete":
return true
default:
return false
}
}
func validateValidationSummaries(documents []validationSummaryDocument) ([]ValidationSummary, error) {
summaries := make([]ValidationSummary, 0, len(documents))
for _, document := range documents {
if document.Status != "complete" && document.Status != "rejected" && document.Status != "incomplete" {
return nil, fmt.Errorf("notarius validation summary status %q is invalid", document.Status)
}
if document.ProducerAttemptCount == nil || *document.ProducerAttemptCount <= 0 || !validTerminalAction(document.TerminalAction) {
return nil, fmt.Errorf("notarius validation summary is missing required fields")
}
summaries = append(summaries, ValidationSummary{
Stage: document.Stage, StepID: document.StepID, LaneID: document.LaneID,
ModuleKey: document.ModuleKey, ChunkID: document.ChunkID, Status: document.Status,
RejectingValidators: append([]string(nil), document.RejectingValidators...),
ReasonCodes: append([]string(nil), document.ReasonCodes...),
IncompleteValidators: append([]string(nil), document.IncompleteValidators...),
ProducerAttemptCount: *document.ProducerAttemptCount, TerminalAction: document.TerminalAction,
})
}
return summaries, nil
}
func validTerminalAction(value string) bool {
switch value {
case "accepted", "reject_output", "warn_continue", "fail_run":
return true
default:
return false
}
}
func loadReceipt(path, pipelineID string) (Receipt, error) {
@@ -177,7 +259,9 @@ func loadReceipt(path, pipelineID string) (Receipt, error) {
if strings.TrimSpace(document.RunID) == "" || strings.TrimSpace(document.PipelineID) == "" ||
strings.TrimSpace(document.OutputDirectory) == "" || strings.TrimSpace(document.ValidationStatus) == "" ||
document.NormalizedOutputCount == nil ||
document.RejectedOutputCount == nil || document.WarningCount == nil {
document.RejectedOutputCount == nil || document.WarningGroupCount == nil ||
document.WarningOccurrenceCount == nil || document.DiagnosticGroupCount == nil ||
document.DiagnosticOccurrenceCount == nil || document.DiagnosticsTruncated == nil {
return Receipt{}, fmt.Errorf("notarius receipt is missing required fields")
}
if document.IndexFile != canonicalIndexFile {
@@ -186,9 +270,18 @@ func loadReceipt(path, pipelineID string) (Receipt, error) {
if document.PipelineID != pipelineID {
return Receipt{}, fmt.Errorf("notarius receipt pipeline id %q does not match requested pipeline %q", document.PipelineID, pipelineID)
}
if *document.NormalizedOutputCount < 0 || *document.RejectedOutputCount < 0 || *document.WarningCount < 0 {
if *document.NormalizedOutputCount < 0 || *document.RejectedOutputCount < 0 ||
*document.WarningGroupCount < 0 || *document.WarningOccurrenceCount < 0 ||
*document.DiagnosticGroupCount < 0 || *document.DiagnosticOccurrenceCount < 0 {
return Receipt{}, fmt.Errorf("notarius receipt counts must be non-negative")
}
if !validValidationStatus(document.ValidationStatus) {
return Receipt{}, fmt.Errorf("notarius receipt validation_status %q is invalid", document.ValidationStatus)
}
validationSummaries, err := validateValidationSummaries(document.ValidationSummaries)
if err != nil {
return Receipt{}, err
}
if !filepath.IsAbs(document.OutputDirectory) {
return Receipt{}, fmt.Errorf("notarius receipt output directory must be absolute")
}
@@ -196,16 +289,21 @@ func loadReceipt(path, pipelineID string) (Receipt, error) {
return Receipt{}, fmt.Errorf("notarius receipt debug directory must be absolute when present")
}
return Receipt{
SchemaVersion: document.SchemaVersion,
RunID: document.RunID,
PipelineID: document.PipelineID,
OutputDirectory: filepath.Clean(document.OutputDirectory),
IndexFile: document.IndexFile,
NormalizedOutputCount: *document.NormalizedOutputCount,
RejectedOutputCount: *document.RejectedOutputCount,
WarningCount: *document.WarningCount,
ValidationStatus: document.ValidationStatus,
DebugDirectory: document.DebugDirectory,
SchemaVersion: document.SchemaVersion,
RunID: document.RunID,
PipelineID: document.PipelineID,
OutputDirectory: filepath.Clean(document.OutputDirectory),
IndexFile: document.IndexFile,
NormalizedOutputCount: *document.NormalizedOutputCount,
RejectedOutputCount: *document.RejectedOutputCount,
WarningGroupCount: *document.WarningGroupCount,
WarningOccurrenceCount: *document.WarningOccurrenceCount,
DiagnosticGroupCount: *document.DiagnosticGroupCount,
DiagnosticOccurrenceCount: *document.DiagnosticOccurrenceCount,
DiagnosticsTruncated: *document.DiagnosticsTruncated,
ValidationStatus: document.ValidationStatus,
ValidationSummaries: validationSummaries,
DebugDirectory: document.DebugDirectory,
}, nil
}
@@ -214,6 +312,7 @@ type indexDocument struct {
OutputFiles *[]laneDocument `json:"output_files"`
RejectedFile string `json:"rejected_file"`
WarningsFile string `json:"warnings_file"`
DiagnosticsFile string `json:"diagnostics_file"`
ChunkMap *pipelineDocument `json:"chunk_map"`
EvidenceContext *pipelineDocument `json:"evidence_context"`
}
@@ -250,6 +349,7 @@ func loadIndex(bundleRoot, indexPath string) (Index, error) {
{name: "manifest_file", got: document.ManifestFile, want: canonicalManifestFile},
{name: "rejected_file", got: document.RejectedFile, want: canonicalRejectedFile},
{name: "warnings_file", got: document.WarningsFile, want: canonicalWarningsFile},
{name: "diagnostics_file", got: document.DiagnosticsFile, want: canonicalDiagnosticsFile},
} {
if field.got != field.want {
return Index{}, fmt.Errorf("notarius index %s %q is incompatible; want %q", field.name, field.got, field.want)
@@ -260,10 +360,11 @@ func loadIndex(bundleRoot, indexPath string) (Index, error) {
}
index := Index{
Path: indexPath,
ManifestFile: document.ManifestFile,
RejectedFile: document.RejectedFile,
WarningsFile: document.WarningsFile,
Path: indexPath,
ManifestFile: document.ManifestFile,
RejectedFile: document.RejectedFile,
WarningsFile: document.WarningsFile,
DiagnosticsFile: document.DiagnosticsFile,
}
var err error
if index.ManifestPath, err = resolveRegularFile(bundleRoot, index.ManifestFile); err != nil {
@@ -275,6 +376,9 @@ func loadIndex(bundleRoot, indexPath string) (Index, error) {
if index.WarningsPath, err = resolveRegularFile(bundleRoot, index.WarningsFile); err != nil {
return Index{}, fmt.Errorf("resolve notarius warning file: %w", err)
}
if index.DiagnosticsPath, err = resolveRegularFile(bundleRoot, index.DiagnosticsFile); err != nil {
return Index{}, fmt.Errorf("resolve notarius diagnostics file: %w", err)
}
seenLanes := make(map[string]struct{}, len(*document.OutputFiles))
for _, lane := range *document.OutputFiles {
@@ -362,12 +466,34 @@ func loadRejections(path string) ([]RejectionSummary, error) {
return summaries, nil
}
type warningDocument struct {
Warnings *[]struct {
type findingGroupDocument struct {
Disposition string `json:"disposition"`
Category string `json:"category"`
ReasonCode string `json:"reason_code"`
Origin diagnosticOriginDocument `json:"origin"`
OccurrenceCount *int `json:"occurrence_count"`
Samples *[]struct {
Scope string `json:"scope"`
ReasonCode string `json:"reason_code"`
Message string `json:"message"`
} `json:"warnings"`
ChunkID string `json:"chunk_id"`
ChunkIndex *int `json:"chunk_index"`
} `json:"samples"`
OmittedSampleCount *int `json:"omitted_sample_count"`
}
type diagnosticOriginDocument struct {
Stage string `json:"stage"`
StepID string `json:"step_id"`
LaneID string `json:"lane_id"`
ModuleKey string `json:"module_key"`
ValidatorKey string `json:"validator_key"`
}
type warningDocument struct {
SchemaVersion string `json:"schema_version"`
GroupCount *int `json:"group_count"`
OccurrenceCount *int `json:"occurrence_count"`
Groups *[]findingGroupDocument `json:"groups"`
}
func loadWarnings(path string) ([]WarningSummary, error) {
@@ -375,19 +501,151 @@ func loadWarnings(path string) ([]WarningSummary, error) {
if err := decodeBoundedJSON(path, maxSummaryBytes, &document); err != nil {
return nil, fmt.Errorf("decode notarius warnings: %w", err)
}
if document.Warnings == nil {
return nil, fmt.Errorf("notarius warning document is missing warnings array")
if document.SchemaVersion != warningsSchemaVersion || document.GroupCount == nil ||
document.OccurrenceCount == nil || document.Groups == nil {
return nil, fmt.Errorf("notarius warning document is missing or incompatible required fields")
}
summaries := make([]WarningSummary, 0, len(*document.Warnings))
for _, item := range *document.Warnings {
if strings.TrimSpace(item.ReasonCode) == "" || strings.TrimSpace(item.Message) == "" {
return nil, fmt.Errorf("notarius warning entries require reason_code and message")
if *document.GroupCount < 0 || *document.GroupCount > maxWarningGroups || *document.OccurrenceCount < 0 ||
*document.GroupCount != len(*document.Groups) {
return nil, fmt.Errorf("notarius warning document counts are inconsistent")
}
summaries := make([]WarningSummary, 0, len(*document.Groups))
occurrences := 0
for _, group := range *document.Groups {
if err := validateFindingGroup(group); err != nil {
return nil, fmt.Errorf("notarius warning group: %w", err)
}
summaries = append(summaries, WarningSummary{Scope: item.Scope, ReasonCode: item.ReasonCode})
if group.Disposition != "warning" {
return nil, fmt.Errorf("notarius warning group disposition %q is invalid", group.Disposition)
}
if *group.OccurrenceCount > int(^uint(0)>>1)-occurrences {
return nil, fmt.Errorf("notarius warning occurrence count overflows")
}
occurrences += *group.OccurrenceCount
summaries = append(summaries, WarningSummary{
Disposition: group.Disposition, Category: group.Category, ReasonCode: group.ReasonCode,
Origin: diagnosticOrigin(group.Origin), OccurrenceCount: *group.OccurrenceCount,
})
}
if occurrences != *document.OccurrenceCount {
return nil, fmt.Errorf("notarius warning document occurrence count is inconsistent")
}
return summaries, nil
}
type diagnosticDocument struct {
SchemaVersion string `json:"schema_version"`
GroupCount *int `json:"group_count"`
OccurrenceCount *int `json:"occurrence_count"`
Truncated *bool `json:"truncated"`
UnrepresentedOccurrenceCount *int `json:"unrepresented_occurrence_count"`
Groups *[]findingGroupDocument `json:"groups"`
}
func loadDiagnostics(path string) ([]DiagnosticSummary, int, bool, error) {
var document diagnosticDocument
if err := decodeBoundedJSON(path, maxSummaryBytes, &document); err != nil {
return nil, 0, false, fmt.Errorf("decode notarius diagnostics: %w", err)
}
if document.SchemaVersion != diagnosticsSchemaVersion || document.GroupCount == nil ||
document.OccurrenceCount == nil || document.Truncated == nil ||
document.UnrepresentedOccurrenceCount == nil || document.Groups == nil {
return nil, 0, false, fmt.Errorf("notarius diagnostics document is missing or incompatible required fields")
}
if *document.GroupCount < 0 || *document.GroupCount > maxDiagnosticGroups || *document.OccurrenceCount < 0 ||
*document.UnrepresentedOccurrenceCount < 0 || *document.GroupCount != len(*document.Groups) {
return nil, 0, false, fmt.Errorf("notarius diagnostics document counts are inconsistent")
}
if !*document.Truncated && *document.UnrepresentedOccurrenceCount != 0 {
return nil, 0, false, fmt.Errorf("notarius diagnostics document has unrepresented occurrences without truncation")
}
summaries := make([]DiagnosticSummary, 0, len(*document.Groups))
representedOccurrences := 0
for _, group := range *document.Groups {
if err := validateFindingGroup(group); err != nil {
return nil, 0, false, fmt.Errorf("notarius diagnostic group: %w", err)
}
if group.Disposition != "advisory" && group.Disposition != "observation" {
return nil, 0, false, fmt.Errorf("notarius diagnostic group disposition %q is invalid", group.Disposition)
}
if *group.OccurrenceCount > int(^uint(0)>>1)-representedOccurrences {
return nil, 0, false, fmt.Errorf("notarius diagnostic occurrence count overflows")
}
representedOccurrences += *group.OccurrenceCount
summaries = append(summaries, DiagnosticSummary{
Disposition: group.Disposition, Category: group.Category, ReasonCode: group.ReasonCode,
Origin: diagnosticOrigin(group.Origin), OccurrenceCount: *group.OccurrenceCount,
})
}
if *document.UnrepresentedOccurrenceCount > int(^uint(0)>>1)-representedOccurrences ||
representedOccurrences+*document.UnrepresentedOccurrenceCount != *document.OccurrenceCount {
return nil, 0, false, fmt.Errorf("notarius diagnostics document occurrence count is inconsistent")
}
return summaries, *document.OccurrenceCount, *document.Truncated, nil
}
func validateFindingGroup(group findingGroupDocument) error {
if strings.TrimSpace(group.Disposition) == "" || strings.TrimSpace(group.Category) == "" ||
strings.TrimSpace(group.ReasonCode) == "" || !validDiagnosticOriginStage(group.Origin.Stage) ||
!validDiagnosticCategory(group.Disposition, group.Category) ||
group.OccurrenceCount == nil || *group.OccurrenceCount <= 0 || group.Samples == nil ||
group.OmittedSampleCount == nil || *group.OmittedSampleCount < 0 {
return fmt.Errorf("missing required fields")
}
if len(*group.Samples) == 0 || len(*group.Samples) > maxFindingSamples ||
*group.OmittedSampleCount != *group.OccurrenceCount-len(*group.Samples) {
return fmt.Errorf("sample counts are inconsistent")
}
for _, sample := range *group.Samples {
if strings.TrimSpace(sample.Scope) == "" || strings.TrimSpace(sample.Message) == "" ||
(sample.ChunkIndex != nil && *sample.ChunkIndex < 0) {
return fmt.Errorf("samples require scope and message")
}
}
return nil
}
func validDiagnosticCategory(disposition, category string) bool {
switch disposition {
case "warning":
return category == "configuration" || category == "degradation" ||
category == "validation_incomplete" || category == "fallback"
case "advisory":
return category == "data_quality"
case "observation":
return category == "normalization"
default:
return false
}
}
func validDiagnosticOriginStage(stage string) bool {
switch stage {
case "references", "chunk", "extract", "merge", "normalize":
return true
default:
return false
}
}
func diagnosticOrigin(document diagnosticOriginDocument) DiagnosticOrigin {
return DiagnosticOrigin{
Stage: document.Stage, StepID: document.StepID, LaneID: document.LaneID,
ModuleKey: document.ModuleKey, ValidatorKey: document.ValidatorKey,
}
}
func sumWarningOccurrences(values []WarningSummary) (int, error) {
total := 0
for _, value := range values {
if value.OccurrenceCount > int(^uint(0)>>1)-total {
return 0, fmt.Errorf("notarius warning occurrence count overflows")
}
total += value.OccurrenceCount
}
return total, nil
}
func decodeBoundedJSON(path string, limit int64, destination any) error {
data, err := fileops.ReadRegularFile(path, limit)
if err != nil {

View File

@@ -52,6 +52,10 @@ func TestSubprocessRunnerBuildsExactInvocationAndDiscoversBundle(t *testing.T) {
if result.Receipt.SchemaVersion != ReceiptSchemaVersion || result.Receipt.RunID != "notarius-run-1" {
t.Fatalf("receipt = %#v", result.Receipt)
}
if len(result.Receipt.ValidationSummaries) != 1 || result.Receipt.ValidationSummaries[0].LaneID != "npc-registry" ||
result.Receipt.ValidationSummaries[0].Status != "complete" {
t.Fatalf("validation summaries = %#v", result.Receipt.ValidationSummaries)
}
if len(result.Index.Lanes) != 1 || result.Index.Lanes[0].LaneID != "npc-registry" {
t.Fatalf("lanes = %#v", result.Index.Lanes)
}
@@ -64,9 +68,12 @@ func TestSubprocessRunnerBuildsExactInvocationAndDiscoversBundle(t *testing.T) {
if len(result.Rejections) != 1 || result.Rejections[0].LaneID != "spells" || result.Rejections[0].ReasonCode != "invalid_spell" {
t.Fatalf("rejections = %#v", result.Rejections)
}
if len(result.Warnings) != 1 || result.Warnings[0].Scope != "lane:npc-registry" || result.Warnings[0].ReasonCode != "normalized_name" {
if len(result.Warnings) != 1 || result.Warnings[0].Category != "degradation" || result.Warnings[0].ReasonCode != "normalized_name" {
t.Fatalf("warnings = %#v", result.Warnings)
}
if len(result.Diagnostics) != 1 || result.Diagnostics[0].Category != "data_quality" || result.Diagnostics[0].ReasonCode != "low_confidence" {
t.Fatalf("diagnostics = %#v", result.Diagnostics)
}
}
func TestSubprocessRunnerUsesMinimalEnvironmentAndSeparatesStreams(t *testing.T) {
@@ -171,8 +178,10 @@ func TestLoadReceiptValidation(t *testing.T) {
valid := map[string]any{
"schema_version": ReceiptSchemaVersion, "run_id": "run-1", "pipeline_id": "pipeline-1",
"output_directory": filepath.Join(root, "outputs", "run-1"), "index_file": "index.json",
"normalized_output_count": 1, "rejected_output_count": 0, "warning_count": 0,
"validation_status": "approved", "future_field": true,
"normalized_output_count": 1, "rejected_output_count": 0,
"warning_group_count": 0, "warning_occurrence_count": 0,
"diagnostic_group_count": 0, "diagnostic_occurrence_count": 0,
"diagnostics_truncated": false, "validation_status": "approved", "future_field": true,
}
tests := []struct {
name string
@@ -183,11 +192,12 @@ func TestLoadReceiptValidation(t *testing.T) {
}{
{name: "unknown fields tolerated", wantOK: true},
{name: "malformed", raw: []byte("{")},
{name: "unsupported version", mutate: func(v map[string]any) { v["schema_version"] = "notarius.run-result.v2" }},
{name: "unsupported version", mutate: func(v map[string]any) { v["schema_version"] = "notarius.run-result.v1" }},
{name: "missing field", mutate: func(v map[string]any) { delete(v, "run_id") }},
{name: "pipeline mismatch", mutate: func(v map[string]any) { v["pipeline_id"] = "other" }},
{name: "relative output", mutate: func(v map[string]any) { v["output_directory"] = "run-1" }},
{name: "negative count", mutate: func(v map[string]any) { v["warning_count"] = -1 }},
{name: "negative count", mutate: func(v map[string]any) { v["warning_group_count"] = -1 }},
{name: "invalid validation status", mutate: func(v map[string]any) { v["validation_status"] = "valid" }},
{
name: "nested index", mutate: func(v map[string]any) { v["index_file"] = "nested/index.json" },
wantError: `index_file "nested/index.json"`,
@@ -369,22 +379,26 @@ func TestLoadDiagnosticSummariesValidateBoundsAndTolerateUnknownFields(t *testin
root := t.TempDir()
rejectedPath := filepath.Join(root, "rejected.json")
warningsPath := filepath.Join(root, "warnings.json")
diagnosticsPath := filepath.Join(root, "diagnostics.json")
writeJSONFile(t, rejectedPath, map[string]any{"rejected": []any{map[string]any{
"stage": "validate", "lane_id": "spells", "reason_code": "invalid", "message": "do not retain this", "future": true,
}}, "future": true})
writeJSONFile(t, warningsPath, map[string]any{"warnings": []any{map[string]any{
"scope": "lane:spells", "reason_code": "bounded", "message": "do not retain this", "future": true,
}}, "future": true})
writeJSONFile(t, warningsPath, findingEnvelope(warningsSchemaVersion, []any{findingGroup("warning", "degradation", "bounded", "normalize", 2)}, 2, false, 0))
writeJSONFile(t, diagnosticsPath, findingEnvelope(diagnosticsSchemaVersion, []any{findingGroup("advisory", "data_quality", "low_confidence", "normalize", 3)}, 4, true, 1))
rejections, err := loadRejections(rejectedPath)
if err != nil || len(rejections) != 1 || rejections[0].ReasonCode != "invalid" {
t.Fatalf("loadRejections() = %#v, %v", rejections, err)
}
warnings, err := loadWarnings(warningsPath)
if err != nil || len(warnings) != 1 || warnings[0].Scope != "lane:spells" {
if err != nil || len(warnings) != 1 || warnings[0].Category != "degradation" || warnings[0].OccurrenceCount != 2 {
t.Fatalf("loadWarnings() = %#v, %v", warnings, err)
}
diagnostics, occurrences, truncated, err := loadDiagnostics(diagnosticsPath)
if err != nil || len(diagnostics) != 1 || occurrences != 4 || !truncated || diagnostics[0].Category != "data_quality" {
t.Fatalf("loadDiagnostics() = %#v, %d, %t, %v", diagnostics, occurrences, truncated, err)
}
for name, path := range map[string]string{"rejections": rejectedPath, "warnings": warningsPath} {
for name, path := range map[string]string{"rejections": rejectedPath, "warnings": warningsPath, "diagnostics": diagnosticsPath} {
t.Run("malformed "+name, func(t *testing.T) {
if err := os.WriteFile(path, []byte("{"), 0o644); err != nil {
t.Fatalf("WriteFile() error = %v", err)
@@ -392,8 +406,10 @@ func TestLoadDiagnosticSummariesValidateBoundsAndTolerateUnknownFields(t *testin
var err error
if name == "rejections" {
_, err = loadRejections(path)
} else {
} else if name == "warnings" {
_, err = loadWarnings(path)
} else {
_, _, _, err = loadDiagnostics(path)
}
if err == nil {
t.Fatal("summary decoder error = nil")
@@ -410,6 +426,9 @@ func TestLoadDiagnosticSummariesValidateBoundsAndTolerateUnknownFields(t *testin
if _, err := loadRejections(oversized); err == nil || !strings.Contains(err.Error(), "exceeds") {
t.Fatalf("loadRejections(oversized) error = %v", err)
}
if _, _, _, err := loadDiagnostics(oversized); err == nil || !strings.Contains(err.Error(), "exceeds") {
t.Fatalf("loadDiagnostics(oversized) error = %v", err)
}
}
func TestFakeRunnerCapturesRequestsAndHonorsContextAndError(t *testing.T) {
@@ -478,13 +497,12 @@ func writeValidBundleAndReceipt(t *testing.T, req RunRequest, includeUnknown boo
}
}
rejection := map[string]any{"stage": "validate", "lane_id": "spells", "reason_code": "invalid_spell", "message": strings.Repeat("external detail", 20)}
warning := map[string]any{"scope": "lane:npc-registry", "reason_code": "normalized_name", "message": strings.Repeat("external warning", 20)}
if includeUnknown {
rejection["future"] = true
warning["future"] = true
}
writeJSONFile(t, filepath.Join(bundle, "rejected.json"), map[string]any{"rejected": []any{rejection}, "future": true})
writeJSONFile(t, filepath.Join(bundle, "warnings.json"), map[string]any{"warnings": []any{warning}, "future": true})
writeJSONFile(t, filepath.Join(bundle, "warnings.json"), findingEnvelope(warningsSchemaVersion, []any{findingGroup("warning", "degradation", "normalized_name", "normalize", 2)}, 2, false, 0))
writeJSONFile(t, filepath.Join(bundle, "diagnostics.json"), findingEnvelope(diagnosticsSchemaVersion, []any{findingGroup("advisory", "data_quality", "low_confidence", "normalize", 3)}, 4, true, 1))
index := validIndexValue([]any{map[string]any{
"lane_id": "npc-registry", "file": "lanes/npc.json", "media_type": "application/json",
"module_key": "dnd/npc-registry", "schema_id": "notarius.dnd.npc_registry",
@@ -503,7 +521,13 @@ func writeValidBundleAndReceipt(t *testing.T, req RunRequest, includeUnknown boo
receipt := map[string]any{
"schema_version": ReceiptSchemaVersion, "run_id": "notarius-run-1", "pipeline_id": req.PipelineID,
"output_directory": bundle, "index_file": "index.json", "normalized_output_count": 1,
"rejected_output_count": 1, "warning_count": 1, "validation_status": "rejected",
"rejected_output_count": 1, "warning_group_count": 1, "warning_occurrence_count": 2,
"diagnostic_group_count": 1, "diagnostic_occurrence_count": 4,
"diagnostics_truncated": true, "validation_status": "rejected",
"validation_summaries": []any{map[string]any{
"stage": "normalize", "lane_id": "npc-registry", "status": "complete",
"producer_attempt_count": 1, "terminal_action": "accepted",
}},
}
if includeUnknown {
receipt["future"] = true
@@ -517,7 +541,7 @@ func createBundleSkeleton(t *testing.T) string {
if err := os.MkdirAll(filepath.Join(bundle, "lanes"), 0o755); err != nil {
t.Fatalf("MkdirAll(bundle) error = %v", err)
}
for _, name := range []string{"manifest.json", "rejected.json", "warnings.json", "lanes/npc.json", "chunk-map.json"} {
for _, name := range []string{"manifest.json", "rejected.json", "warnings.json", "diagnostics.json", "lanes/npc.json", "chunk-map.json"} {
if err := os.WriteFile(filepath.Join(bundle, filepath.FromSlash(name)), []byte("{}"), 0o644); err != nil {
t.Fatalf("WriteFile(%q) error = %v", name, err)
}
@@ -528,10 +552,31 @@ func createBundleSkeleton(t *testing.T) string {
func validIndexValue(lanes []any) map[string]any {
return map[string]any{
"manifest_file": "manifest.json", "output_files": lanes,
"rejected_file": "rejected.json", "warnings_file": "warnings.json",
"rejected_file": "rejected.json", "warnings_file": "warnings.json", "diagnostics_file": "diagnostics.json",
}
}
func findingGroup(disposition, category, reasonCode, origin string, occurrences int) map[string]any {
return map[string]any{
"disposition": disposition, "category": category, "reason_code": reasonCode,
"origin": map[string]any{"stage": origin, "lane_id": "npc-registry"}, "occurrence_count": occurrences,
"samples": []any{map[string]any{"scope": "lane:npc-registry", "message": "external detail"}},
"omitted_sample_count": occurrences - 1,
}
}
func findingEnvelope(schema string, groups []any, occurrences int, truncated bool, unrepresented int) map[string]any {
value := map[string]any{
"schema_version": schema, "group_count": len(groups), "occurrence_count": occurrences,
"groups": groups,
}
if schema == diagnosticsSchemaVersion {
value["truncated"] = truncated
value["unrepresented_occurrence_count"] = unrepresented
}
return value
}
func writeJSONFile(t *testing.T, path string, value any) {
t.Helper()
data, err := json.Marshal(value)