package debugbundle import ( "fmt" "strings" "time" "gitea.maximumdirect.net/eric/notarius/internal/core/artifacts" "gitea.maximumdirect.net/eric/notarius/internal/core/fileio" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" ) const ( ArtifactInvocationMetadata = "invocation.json" ArtifactEffectiveConfig = "effective-config.json" ArtifactResolvedPipeline = "resolved-pipeline.json" ArtifactResolvedReferences = "resolved-references.json" ArtifactCheckpointEvents = "checkpoint-events.json" ArtifactRunManifest = "run-manifest.json" ArtifactChunkPlan = "chunk-plan.json" ArtifactRunReport = "run-report.json" ArtifactWarnings = "warnings.json" ArtifactErrorLog = "error.log" ) type RedactedSummaryPayload interface{ RedactedSummaryPayload() any } type RedactedResolvedPipelinePayload interface { RedactedResolvedPipelinePayload() pipeline.ResolvedPipeline } type Invocation struct { Operation string `json:"operation"` PipelineID string `json:"pipeline_id,omitempty"` PipelineDigest string `json:"pipeline_digest,omitempty"` Resume bool `json:"resume,omitempty"` RecomputeStep string `json:"recompute_step,omitempty"` InputPath string `json:"input_path,omitempty"` ConfigPath string `json:"config_path,omitempty"` ConfigSource string `json:"config_source,omitempty"` OnlyLanes []string `json:"only_lanes,omitempty"` ChunkCacheOverride string `json:"chunk_cache_override,omitempty"` ReasoningEffortOverride *string `json:"reasoning_effort_override,omitempty"` SessionID string `json:"session_id,omitempty"` RunID string `json:"run_id"` StartedAt time.Time `json:"started_at"` } type RunReport struct { RunID string `json:"run_id"` PipelineID string `json:"pipeline_id"` OutputPath string `json:"output_path,omitempty"` DebugPath string `json:"debug_path,omitempty"` Succeeded bool `json:"succeeded"` OutputCount int `json:"output_count"` RejectedCount int `json:"rejected_count"` WarningCount int `json:"warning_count"` ValidationStatus string `json:"validation_status,omitempty"` } type SummaryWriter struct { root, runID string createdAt time.Time } func (w *SummaryWriter) WriteInvocation(payload Invocation) error { if w == nil { return fmt.Errorf("debug summary writer must not be nil") } if payload.RunID == "" { payload.RunID = w.runID } if payload.StartedAt.IsZero() { payload.StartedAt = w.createdAt } if payload.ReasoningEffortOverride != nil { value := *payload.ReasoningEffortOverride payload.ReasoningEffortOverride = &value } return w.writeJSON(ArtifactInvocationMetadata, payload) } func (w *SummaryWriter) WriteRedactedEffectiveConfig(payload RedactedSummaryPayload) error { if payload == nil { return fmt.Errorf("redacted summary payload must not be nil") } return w.writeJSON(ArtifactEffectiveConfig, payload.RedactedSummaryPayload()) } func (w *SummaryWriter) WriteResolvedPipeline(payload RedactedResolvedPipelinePayload) error { if payload == nil { return fmt.Errorf("redacted resolved pipeline payload must not be nil") } return w.writeJSON(ArtifactResolvedPipeline, payload.RedactedResolvedPipelinePayload()) } func (w *SummaryWriter) WriteResolvedReferences(v []artifacts.ReferenceProvenance) error { return w.writeJSON(ArtifactResolvedReferences, v) } func (w *SummaryWriter) WriteCheckpointEvents(v []pipeline.CheckpointEvent) error { return w.writeJSON(ArtifactCheckpointEvents, v) } func (w *SummaryWriter) WriteRunManifest(v artifacts.RunManifest) error { return w.writeJSON(ArtifactRunManifest, v) } func (w *SummaryWriter) WriteChunkPlan(v artifacts.ChunkPlanSummary) error { return w.writeJSON(ArtifactChunkPlan, v) } func (w *SummaryWriter) WriteRunReport(v RunReport) error { return w.writeJSON(ArtifactRunReport, v) } func (w *SummaryWriter) WriteWarnings(v []contracts.Warning) error { return w.writeJSON(ArtifactWarnings, v) } func (w *SummaryWriter) WriteError(message string) error { return w.writeBytes(ArtifactErrorLog, []byte(message+"\n")) } func (w *SummaryWriter) writeJSON(name string, v any) error { if w == nil { return fmt.Errorf("debug summary writer must not be nil") } if err := fileio.WriteJSON(w.root, summaryName(name), v, 0o700, 0o600); err != nil { return fmt.Errorf("write debug summary artifact %q: %w", name, err) } return nil } func (w *SummaryWriter) writeBytes(name string, v []byte) error { if w == nil { return fmt.Errorf("debug summary writer must not be nil") } if err := fileio.WriteBytes(w.root, summaryName(name), v, 0o700, 0o600); err != nil { return fmt.Errorf("write debug summary artifact %q: %w", name, err) } return nil } func summaryName(name string) string { name = strings.TrimSpace(name) if name == "" || strings.ContainsAny(name, "/\\") { return "../invalid" } return name }