package store import ( "encoding/json" "sort" "strings" "orchestra/internal/domain" "orchestra/internal/review" ) // ProjectDebt folds canonical history into a debt ledger. It is a pure // function of the events so it can be tested against a real log, and it writes // nothing: this projection is a read model, not a new kind of truth. // // It deliberately reports what it cannot see. Worker observations and operator // interventions are not carried by any event type today, so a ledger that // stayed silent about them would read as "no operational cost" when it means // "operational cost is unrecorded". func ProjectDebt(events []domain.Event) domain.DebtLedger { return projectDebt(events, nil) } // ProjectDebtWithArtifacts is ProjectDebt plus the CAS reads that review // findings need. Findings are sealed as an artifact rather than inlined in the // event, and they are the only evidence structural debt has, so a projection // without them can never produce a structural item at all. func ProjectDebtWithArtifacts(events []domain.Event, read func(ref string) ([]byte, error)) domain.DebtLedger { return projectDebt(events, read) } func projectDebt(events []domain.Event, readArtifact func(string) ([]byte, error)) domain.DebtLedger { bySignature := map[string]*domain.DebtItem{} order := []string{} seenKinds := map[domain.ObservationKind]bool{} add := func(class domain.DebtClass, sig string, o domain.DebtObservation, symptom string, sev review.Severity) { o.Signature = sig if o.Validate() != nil { return } seenKinds[o.Kind] = true item, ok := bySignature[sig] if !ok { item = &domain.DebtItem{ // The id is the signature, not an Fxx number. This projection // produces candidates from history; adopting a curated id is a // later, deliberate step, and minting one here would collide // with the hand-written ledger. ID: sig, Class: class, Status: domain.DebtObserved, Symptom: symptom, Signatures: []string{sig}, Severity: sev, } bySignature[sig] = item order = append(order, sig) } item.Observations = append(item.Observations, o) for _, p := range o.Paths { if !contains(item.Paths, p) { item.Paths = append(item.Paths, p) } } } for _, e := range events { var p map[string]any if len(e.Payload) > 0 && json.Unmarshal(e.Payload, &p) != nil { continue } base := domain.DebtObservation{EventID: e.ID, TaskID: e.TaskID, At: e.At} harness, _ := p["harness_id"].(string) switch e.Type { case "TaskBlocked": reason, _ := p["block_reason"].(string) if reason == "" { reason = string(domain.InferBlockReason(str(p["blocker"]))) } class, ok := domain.DebtClassForBlockReason(domain.BlockReason(reason)) if !ok { continue } o := base o.Kind = domain.ObservationBlockReason o.Detail = str(p["blocker"]) add(class, domain.DebtSignature(class, reason, harness, "lease"), o, "tasks are blocked with "+reason, review.Important) case "TaskFailed", "TaskReleased": // A release carries a failure class only when it is a reclaim. A // handoff release is ordinary progress and never debt. failure, _ := p["failure_class"].(string) if failure == "" { failure, _ = p["reason"].(string) } if e.Type == "TaskReleased" && str(p["handoff_ref"]) != "" { continue } class, ok := domain.DebtClassForFailureClass(failure) if !ok { continue } o := base o.Kind = domain.ObservationFailureClass o.Detail = str(p["last_error"]) add(class, domain.DebtSignature(class, failure, harness, "lease"), o, "tasks end in "+failure, review.Important) case domain.EventObservationIncidentClosed: // One incident, whatever it repeated. The debt class comes from // the signature's shape rather than a failure class, because a // worker observation is a symptom the worker described, not a // lifecycle outcome Orchestra decided. var inc domain.ObservationIncident if json.Unmarshal(e.Payload, &inc) != nil || inc.Signature == "" { continue } o := base o.Kind = domain.ObservationWorkerFailure o.TaskID = inc.TaskID o.Detail = inc.Detail o.Repeats = inc.RepeatCount add(domain.DebtOperational, domain.DebtSignature(domain.DebtOperational, inc.Signature, inc.WorkerID, "worker"), o, "workers report "+inc.Signature, review.Important) case domain.EventOperatorInterventionRecorded: var in domain.OperatorIntervention if json.Unmarshal(e.Payload, &in) != nil || !in.Kind.Valid() { continue } o := base o.Kind = domain.ObservationManualIntervention o.TaskID = in.TaskID o.Detail = in.Reason o.Paths = in.Components add(domain.DebtOperational, domain.DebtSignature(domain.DebtOperational, string(in.Kind), in.WorkerID, "manual"), o, "an operator repairs this by hand ("+string(in.Kind)+")", review.Important) case domain.EventPlanMismatchRecorded: o := base o.Kind = domain.ObservationPlanMismatch o.Detail = str(p["observed"]) o.Paths = strList(p["evidence"]) component := firstComponent(o.Paths) add(domain.DebtCorrectness, domain.DebtSignature(domain.DebtCorrectness, "plan_mismatch", "", component), o, "the repository contradicts sealed plans", review.Important) case domain.EventReviewRecorded: // Repeated findings in one component are the only mechanical // evidence that structure is costing future changes. One finding // is a review doing its job. if readArtifact == nil { break } raw, err := readArtifact(str(p["artifact_ref"])) if err != nil { break } result, err := review.Decode(raw) if err != nil { break } for _, f := range result.Findings { component := domain.DebtComponent(f.File) o := base o.Kind = domain.ObservationReviewFinding o.Detail = f.Claim if f.File != "" { o.Paths = []string{f.File} } add(domain.DebtStructural, domain.DebtSignature(domain.DebtStructural, string(f.Severity), "", component), o, "review keeps finding "+string(f.Severity)+" issues in "+component, f.Severity) } case domain.EventDeferredFindingRecorded: o := base o.Kind = domain.ObservationDeferredFinding o.Detail = str(p["summary"]) // Polish until something demonstrates a cost. A discovery nobody // has paid for yet is not debt with a priority. add(domain.DebtPolish, domain.DebtSignature(domain.DebtPolish, "deferred_finding", "", ""), o, "out-of-scope discoveries recorded and unaddressed", review.Minor) } } items := make([]domain.DebtItem, 0, len(order)) for _, sig := range order { items = append(items, *bySignature[sig]) } sort.SliceStable(items, func(i, j int) bool { if items[i].Recurrence() != items[j].Recurrence() { return items[i].Recurrence() > items[j].Recurrence() } return items[i].ID < items[j].ID }) return domain.DebtLedger{Items: items, Gaps: debtGaps(seenKinds), Events: len(events)} } // debtGaps separates two different silences. A kind no event type carries is a // hole in the system. A kind the log could carry and does not is a fact about // this history. func debtGaps(seen map[domain.ObservationKind]bool) []domain.EvidenceGap { // Both of these were once permanent holes in the system. They are ordinary // evidence now, so their absence is a fact about this history rather than // about Orchestra. var gaps []domain.EvidenceGap for _, k := range []domain.ObservationKind{ domain.ObservationBlockReason, domain.ObservationFailureClass, domain.ObservationReviewFinding, domain.ObservationPlanMismatch, domain.ObservationDeferredFinding, domain.ObservationWorkerFailure, domain.ObservationManualIntervention, } { if !seen[k] { gaps = append(gaps, domain.EvidenceGap{Kind: k, Durable: true, Reason: "carried by the log, but this history contains none"}) } } return gaps } // DebtLedger projects the whole log. Read-only, like every other projection // the coordinator serves. func (s *Store) DebtLedger() domain.DebtLedger { return ProjectDebtWithArtifacts(s.Events(0), s.Artifact) } func str(v any) string { s, _ := v.(string); return s } func strList(v any) []string { list, ok := v.([]any) if !ok { return nil } out := make([]string, 0, len(list)) for _, item := range list { if s := strings.TrimSpace(str(item)); s != "" { out = append(out, s) } } return out } func firstComponent(paths []string) string { for _, p := range paths { if c := domain.DebtComponent(p); c != "" { return c } } return "" } func contains(list []string, s string) bool { for _, item := range list { if item == s { return true } } return false }