From 0aefe021b0d210cd8ee4035eba5a5c4ebf20f5a4 Mon Sep 17 00:00:00 2001 From: kami Date: Sun, 30 Aug 2026 14:57:25 +0400 Subject: [PATCH] Explain a debt refusal with facts about the item Reasons are the auditable explanation of a pure decision, so one that describes the architecture rots the moment the architecture moves. The operational refusal asserted that manual interventions were recorded by no event type: true when written, false the day OperatorInterventionRecorded landed, and still printed under every refusal after that. It now reports the counts and the thresholds they missed. Why a count is zero is not this function's business, since no intervention happening, none being recorded, and none being migrated all read the same from here. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CVbaKucEYBjMqVeUgJUsc1 --- cmd/ledgerrun/main.go | 56 ++++++++++++++++++++++++++++++++ internal/operations/debt.go | 12 +++++-- internal/operations/debt_test.go | 14 ++++++-- 3 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 cmd/ledgerrun/main.go diff --git a/cmd/ledgerrun/main.go b/cmd/ledgerrun/main.go new file mode 100644 index 0000000..0c10ad0 --- /dev/null +++ b/cmd/ledgerrun/main.go @@ -0,0 +1,56 @@ +// Runs the debt projection over a captured event log and prints one row per +// item, so two builds can be compared on identical input. The point of the +// comparison is the signature fix: cross-task incidents must collapse into one +// item while genuinely different failures stay separate. +package main + +import ( + "encoding/json" + "fmt" + "os" + "sort" + + "orchestra/internal/domain" + "orchestra/internal/operations" + "orchestra/internal/store" +) + +func main() { + b, err := os.ReadFile(os.Args[1]) + if err != nil { + panic(err) + } + var events []domain.Event + if err := json.Unmarshal(b, &events); err != nil { + panic(err) + } + ledger := store.ProjectDebt(events) + eligible := map[string]operations.DebtCandidate{} + for _, c := range operations.EligibleDebt(ledger) { + eligible[c.Item.ID] = c + } + rows := make([]map[string]any, 0, len(ledger.Items)) + for _, item := range ledger.Items { + tasks := map[string]bool{} + intensity := 0 + for _, o := range item.Observations { + if o.TaskID != "" { + tasks[o.TaskID] = true + } + intensity += o.Repeats + } + row := map[string]any{ + "signature": item.ID, "class": string(item.Class), + "recurrence": len(item.Observations), "intensity": intensity, + "tasks": len(tasks), "eligible": false, "reasons": []string{}, + } + check := operations.CheckDebtEligibility(item) + row["eligible"] = check.Eligible + row["reasons"] = check.Reasons + _ = eligible + rows = append(rows, row) + } + sort.Slice(rows, func(i, j int) bool { return rows[i]["signature"].(string) < rows[j]["signature"].(string) }) + out, _ := json.MarshalIndent(map[string]any{"items": rows, "gaps": ledger.Gaps}, "", " ") + fmt.Println(string(out)) +} diff --git a/internal/operations/debt.go b/internal/operations/debt.go index 7b4e217..9be0937 100644 --- a/internal/operations/debt.go +++ b/internal/operations/debt.go @@ -43,9 +43,17 @@ func CheckDebtEligibility(item domain.DebtItem) DebtCheck { if len(why) > 0 { return DebtCheck{true, why} } + // Reasons state facts about this item and nothing about the + // architecture around it. The second line here used to say manual + // interventions were recorded by no event type, which was true when it + // was written and false the moment OperatorInterventionRecorded + // landed. A decision that is right for a reason that has become a lie + // cannot be audited, and why a count is zero is not this function's + // business: no intervention happened, none was recorded, or none was + // migrated all read the same from here. return DebtCheck{false, []string{ - fmt.Sprintf("needs 3 occurrences across 2 tasks, or 1 manual intervention; has %d across %d tasks with %d interventions", recurrence, tasks, manual), - "manual interventions are not recorded by any event type, so that count reads 0 on every current log", + fmt.Sprintf("breadth threshold not met: %d occurrences across %d tasks, needs 3 across 2", recurrence, tasks), + fmt.Sprintf("manual intervention threshold not met: %d recorded, needs 1", manual), }} case domain.DebtStructural: if recurrence >= 3 { diff --git a/internal/operations/debt_test.go b/internal/operations/debt_test.go index b4128f9..363974f 100644 --- a/internal/operations/debt_test.go +++ b/internal/operations/debt_test.go @@ -62,7 +62,17 @@ func TestDebtRefusalNamesTheMissingEvidence(t *testing.T) { t.Fatalf("want a refusal with reasons, got %+v", check) } joined := strings.Join(check.Reasons, " ") - if !strings.Contains(joined, "manual interventions are not recorded") { - t.Fatalf("the refusal must say the intervention count is structurally zero: %v", check.Reasons) + // Every reason is a fact about this item. The old text asserted that no + // event type recorded manual interventions, which stopped being true the + // day one did, leaving a correct decision explained by a lie. + for _, want := range []string{"1 occurrences across 1 tasks", "0 recorded, needs 1"} { + if !strings.Contains(joined, want) { + t.Fatalf("the refusal does not state %q: %v", want, check.Reasons) + } + } + for _, forbidden := range []string{"event type", "every current log"} { + if strings.Contains(joined, forbidden) { + t.Fatalf("a reason describes the architecture instead of the item: %v", check.Reasons) + } } }