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
82 lines
3.3 KiB
Go
82 lines
3.3 KiB
Go
package operations
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"orchestra/internal/domain"
|
|
)
|
|
|
|
// DebtCheck is why a debt item may or may not become work. Reasons are listed
|
|
// rather than summarised, for the same reason SubmissionCheck lists them: "not
|
|
// eligible" alone sends an operator reading code.
|
|
type DebtCheck struct {
|
|
Eligible bool `json:"eligible"`
|
|
Reasons []string `json:"reasons,omitempty"`
|
|
}
|
|
|
|
// CheckDebtEligibility is the whole promotion rule, as one pure function of a
|
|
// projected item. It decides only whether a maintenance task may be created.
|
|
// It never creates one, and it never changes what that task must then pass:
|
|
// classification as debt changes what gets scheduled, never what gets checked.
|
|
//
|
|
// The thresholds differ by class on purpose. Correctness and operational debt
|
|
// have already cost something measurable. Structural debt needs evidence that
|
|
// it is causing repeated work rather than merely offending taste. Polish never
|
|
// promotes itself, or the ledger becomes a permanent cleanup generator.
|
|
func CheckDebtEligibility(item domain.DebtItem) DebtCheck {
|
|
recurrence, tasks := item.Recurrence(), item.AffectedTasks()
|
|
blocked, manual := item.BlockedTasks(), item.ManualInterventions()
|
|
switch item.Class {
|
|
case domain.DebtCorrectness:
|
|
if recurrence >= 1 {
|
|
return DebtCheck{true, []string{fmt.Sprintf("correctness debt is eligible on first confirmed observation, and has %d", recurrence)}}
|
|
}
|
|
return DebtCheck{false, []string{"no confirmed observation"}}
|
|
case domain.DebtOperational:
|
|
var why []string
|
|
if manual >= 1 {
|
|
why = append(why, fmt.Sprintf("%d manual intervention(s) recorded", manual))
|
|
}
|
|
if recurrence >= 3 && tasks >= 2 {
|
|
why = append(why, fmt.Sprintf("recurred %d times across %d tasks", recurrence, tasks))
|
|
}
|
|
if len(why) > 0 {
|
|
return DebtCheck{true, why}
|
|
}
|
|
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",
|
|
}}
|
|
case domain.DebtStructural:
|
|
if recurrence >= 3 {
|
|
return DebtCheck{true, []string{fmt.Sprintf("%d review findings in this component", recurrence)}}
|
|
}
|
|
if blocked >= 2 {
|
|
return DebtCheck{true, []string{fmt.Sprintf("blocked %d distinct tasks in this component", blocked)}}
|
|
}
|
|
return DebtCheck{false, []string{
|
|
fmt.Sprintf("needs 3 review findings or 2 blocked tasks in one component; has %d findings and %d blocked", recurrence, blocked),
|
|
}}
|
|
case domain.DebtPolish:
|
|
return DebtCheck{false, []string{"polish never promotes itself, an operator promotes it explicitly"}}
|
|
}
|
|
return DebtCheck{false, []string{"unknown debt class " + string(item.Class)}}
|
|
}
|
|
|
|
// EligibleDebt filters a projected ledger to what policy would allow to become
|
|
// work. It returns the check alongside each item so the reasons stay visible.
|
|
func EligibleDebt(ledger domain.DebtLedger) []DebtCandidate {
|
|
out := []DebtCandidate{}
|
|
for _, item := range ledger.Items {
|
|
if check := CheckDebtEligibility(item); check.Eligible {
|
|
out = append(out, DebtCandidate{Item: item, Check: check})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
type DebtCandidate struct {
|
|
Item domain.DebtItem `json:"item"`
|
|
Check DebtCheck `json:"check"`
|
|
}
|