Files
orchestra/internal/store/debt_projection.go
T
kami a757cffc78 Project a debt ledger from canonical history, read-only
Slice one of DEBT-DESIGN.md, with the four amendments applied. It writes
nothing: no new event types, no scheduling, no clustering, no
maintenance tasks. The point is to find out whether the model can
represent debt this project already knows about, before committing to a
durable schema.

The projected type lives in domain and the fold lives in store, so the
first implementation does not bake a read model into the command layer.
operations owns the one action that exists, CheckDebtEligibility, which
is a pure function returning explicit reasons like CheckSubmission.

Signatures carry their version in the string. Normalization rules will
change, and without a version that silently regroups history and moves
the recurrence counts eligibility was already decided on.

Observations require exactly one of event_id and legacy_ref. Imported
Fxx history predates the events that would justify it, and a fabricated
event id would break the provenance rule the ledger exists to enforce.

Incompleteness is reported, not hidden. Manual interventions and worker
observations are carried by no event type, so the ledger names both as
non-durable gaps rather than reading as "no operational cost". The
operational refusal reason says the intervention count is structurally
zero on every current log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1
2026-08-29 02:00:58 +04:00

228 lines
7.4 KiB
Go

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.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 {
gaps := []domain.EvidenceGap{
{Kind: domain.ObservationManualIntervention, Durable: false,
Reason: "no event type records an operator repair, so every manual recovery is invisible to this ledger"},
{Kind: domain.ObservationWorkerFailure, Durable: false,
Reason: "worker observations live in worker memory and reach the coordinator only inside WorkerHealth, which is not persisted"},
}
for _, k := range []domain.ObservationKind{
domain.ObservationBlockReason, domain.ObservationFailureClass,
domain.ObservationReviewFinding, domain.ObservationPlanMismatch,
domain.ObservationDeferredFinding,
} {
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
}