// Package reporting owns the public report schema, separate from processing. package reporting import ( "fmt" "time" "gitea.maximumdirect.net/eric/audita/internal/core/redaction" "gopkg.in/yaml.v3" ) const ( StatusInProgress = "in_progress" StatusCompleted = "completed" StatusFailed = "failed" ) // Nil counts mean unavailable; a pointer to zero means a known empty count. type Summary struct { WorkUnitsTotal *int `yaml:"work_units_total"` WorkUnitsStarted *int `yaml:"work_units_started"` WorkUnitsCompleted *int `yaml:"work_units_completed"` WorkUnitsInterrupted *int `yaml:"work_units_interrupted"` WorkUnitsNotStarted *int `yaml:"work_units_not_started"` WorkUnitsWithErrors *int `yaml:"work_units_with_errors"` RunErrors *int `yaml:"run_errors"` CorrectionsApplied *int `yaml:"corrections_applied"` LLMOperations *int `yaml:"llm_operations"` ProposalResponses *int `yaml:"proposal_responses"` Recoveries *int `yaml:"recoveries"` ProposalResponsesWithFiltering *int `yaml:"proposal_responses_with_filtering"` } type ExecutionCounts struct { ProviderAttempts *int `yaml:"provider_attempts,omitempty"` TransientRetries *int `yaml:"transient_retries,omitempty"` StructuralRepairs *int `yaml:"structural_repairs,omitempty"` RetriesExhausted *bool `yaml:"retries_exhausted,omitempty"` HTTPStatus *int `yaml:"http_status,omitempty"` } type Failure struct { Operation string `yaml:"operation"` Validator string `yaml:"validator,omitempty"` BatchIndex *int `yaml:"batch_index,omitempty"` Reason string `yaml:"reason"` Message string `yaml:"message"` ProposalIndices []int `yaml:"proposal_indices,omitempty"` ExecutionCounts `yaml:",inline"` } type ErrorRecord struct { Scope string `yaml:"scope"` StageIndex *int `yaml:"stage_index,omitempty"` ModuleKey string `yaml:"module_key,omitempty"` ModuleInstance string `yaml:"module_instance,omitempty"` ChunkIndex *int `yaml:"chunk_index,omitempty"` StartSegmentID *int `yaml:"start_segment_id,omitempty"` EndSegmentID *int `yaml:"end_segment_id,omitempty"` CorrectionsApplied *int `yaml:"corrections_applied,omitempty"` Failures []Failure `yaml:"failures,omitempty"` Phase string `yaml:"phase,omitempty"` Reason string `yaml:"reason,omitempty"` Message string `yaml:"message,omitempty"` Operation string `yaml:"operation,omitempty"` Validator string `yaml:"validator,omitempty"` BatchIndex *int `yaml:"batch_index,omitempty"` OutputPath string `yaml:"output_path,omitempty"` } type FilteredProposal struct { ProposalIndex int `yaml:"proposal_index"` Reason string `yaml:"reason"` Count int `yaml:"count"` } type RecoveryRecord struct { StageIndex int `yaml:"stage_index"` ModuleKey string `yaml:"module_key"` ModuleInstance string `yaml:"module_instance"` ChunkIndex int `yaml:"chunk_index"` StartSegmentID int `yaml:"start_segment_id"` EndSegmentID int `yaml:"end_segment_id"` Operation string `yaml:"operation"` Validator string `yaml:"validator,omitempty"` BatchIndex *int `yaml:"batch_index,omitempty"` Reasons []string `yaml:"reasons"` Message string `yaml:"message"` ExecutionCounts `yaml:",inline"` FilteredProposals []FilteredProposal `yaml:"filtered_proposals,omitempty"` } type ErrorsDocument struct { Schema string `yaml:"schema"` Version int `yaml:"version"` RunID string `yaml:"run_id"` Status string `yaml:"status"` StartedAt string `yaml:"started_at"` CompletedAt *string `yaml:"completed_at"` Summary Summary `yaml:"summary"` Errors []ErrorRecord `yaml:"errors"` } type RecoveriesDocument struct { Schema string `yaml:"schema"` Version int `yaml:"version"` RunID string `yaml:"run_id"` Status string `yaml:"status"` StartedAt string `yaml:"started_at"` CompletedAt *string `yaml:"completed_at"` Summary Summary `yaml:"summary"` Recoveries []RecoveryRecord `yaml:"recoveries"` } // WorkUnit is payload-free matrix input for report.md. type WorkUnit struct { StageIndex int ModuleKey string ModuleInstance string ChunkIndex int StartSegmentID int EndSegmentID int State string CorrectionsApplied int ErrorCount int RecoveryCount int } // Snapshot is built once from processing state and shared by presentations. type Snapshot struct { RunID string Status string StartedAt time.Time CompletedAt *time.Time TranscriptPath string GlossaryPath string OutputPath string StageNames []string WorkUnits []WorkUnit Summary Summary Errors []ErrorRecord Recoveries []RecoveryRecord } func (s Snapshot) ErrorsDocument() ErrorsDocument { errors := s.Errors if errors == nil { errors = []ErrorRecord{} } return ErrorsDocument{Schema: "audita-errors", Version: 1, RunID: s.RunID, Status: s.Status, StartedAt: s.StartedAt.UTC().Format(time.RFC3339), CompletedAt: completionTime(s.CompletedAt), Summary: s.Summary, Errors: errors} } func (s Snapshot) RecoveriesDocument() RecoveriesDocument { recoveries := s.Recoveries if recoveries == nil { recoveries = []RecoveryRecord{} } return RecoveriesDocument{Schema: "audita-recoveries", Version: 1, RunID: s.RunID, Status: s.Status, StartedAt: s.StartedAt.UTC().Format(time.RFC3339), CompletedAt: completionTime(s.CompletedAt), Summary: s.Summary, Recoveries: recoveries} } func completionTime(t *time.Time) *string { if t == nil { return nil } value := t.UTC().Format(time.RFC3339) return &value } // YAML redacts only string nodes after typed serialization. func YAML(document any, secrets ...string) ([]byte, error) { raw, err := yaml.Marshal(document) if err != nil { return nil, fmt.Errorf("encode report YAML: %w", err) } safe, err := redaction.New(secrets).YAML(raw) if err != nil { return nil, fmt.Errorf("redact report YAML: %w", err) } return safe, nil }