a757cffc78
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
228 lines
7.4 KiB
Go
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
|
|
}
|