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
This commit is contained in:
2026-08-29 02:00:58 +04:00
parent 2f61a99986
commit a757cffc78
8 changed files with 846 additions and 11 deletions
+58 -11
View File
@@ -98,7 +98,9 @@ No new subsystem. Two event types on the existing spine, one projection beside
// DebtObservation is one piece of counted evidence, always pointing at the
// event that produced it. Provenance is an event id, never prose.
type DebtObservation struct {
EventID string `json:"event_id"`
// Exactly one of EventID and LegacyRef. See section 8.
EventID string `json:"event_id,omitempty"`
LegacyRef string `json:"legacy_ref,omitempty"`
TaskID string `json:"task_id,omitempty"`
Kind string `json:"kind"` // block_reason, failure_class, review_finding,
// plan_mismatch, manual_intervention, deferred_finding
@@ -152,6 +154,18 @@ structural class + finding severity + normalized component
Exact signature match attaches automatically. That is the only automatic path.
**Signatures are versioned, in the string itself.**
```text
v1:operational:lease_not_renewed:workpc-opencode:federation
```
Normalization rules will change. Without a version, changing them silently
regroups every historical observation, and the recurrence counts that drive
eligibility move underneath the items that already used them. A `v2` signature
never matches a `v1` one, so old evidence keeps the grouping it was counted
under.
**A model may propose, never merge.** Clustering emits `DebtMergeSuggested`
carrying both item ids and its reasoning. The merge is an operator action or a
policy threshold, recorded as `DebtItemUpdated`.
@@ -245,6 +259,19 @@ One-time import, by hand, with a class assigned per row. Each becomes a
`DebtItem` with `Status: repaired`, its commit as `RepairCommit`, and one
observation citing the ledger.
**Imported observations carry legacy provenance, never a fabricated event id.**
The Fxx history predates the capability that would have produced an event, and
inventing one would break the provenance rule the ledger exists to enforce.
```go
EventID string `json:"event_id,omitempty"`
LegacyRef string `json:"legacy_ref,omitempty"` // "BURNIN.md:F18"
```
Exactly one is required. A reader can then tell a counted fact from an imported
claim at a glance, and the counts that drive eligibility can exclude imported
evidence if that turns out to matter.
**Keep the namespace.** New items continue at F62. Two numbering schemes would
be the first structural debt the ledger itself creates.
@@ -271,21 +298,41 @@ represent the history this project already has.
schema commitment.**
```text
internal/operations/debt.go DebtItem, signature, projection over s.Events(0)
CheckDebtEligibility
GET /v1/debt read-only, tui surface
internal/domain/debt.go DebtItem, DebtObservation, signature, classification
internal/store/debt_projection.go the fold over s.Events(0), and the gap report
internal/operations/debt.go CheckDebtEligibility, and later the actions
GET /v1/debt read-only, tui surface
```
The proof is that the current log already contains runs 10 to 14. The projection
must independently surface, from history alone:
The projected type stays out of `operations`. Baking a read model into the
command layer in the first slice is the mistake that would be hardest to undo
later. `operations` owns actions: `CheckDebtEligibility` now, and
`SuggestDebtMerge`, `PromoteDebtItem` and `ScheduleDebtRepair` when they exist.
- the release loop as operational debt, with its 409 recurrence and two manual
cleanups
- the opencode adapter gap as operational debt, across three failed attempts on
one harness
The proof is what the projection can and cannot recover from canonical history.
Section 2 already says worker observations are not durable and operator
interventions are unrecorded, so demanding their reconstruction would be asking
the projection to invent evidence.
The first burn-in is therefore three requirements, not one:
1. Recover every debt signal that canonical history actually encodes.
2. Report, explicitly and per kind, where known debt cannot be reconstructed.
3. Never infer a missing observation from `BURNIN.md`.
Incompleteness is part of the result, not a failure. The 409 loop lived in the
F18 worker ring rather than in an event, so the ledger should say it has no
durable evidence for that shape. That statement is what makes slice two
necessary, measurably rather than by assertion.
What the projection should recover from the log alone:
- repeated `lease_expired` and `retry_limit` on one harness, which is the
opencode failure shape across four tasks
- the retry-idleness dynamic, attached to the tasks that expired
- review findings grouped by component, if any repeat
A model that cannot reproduce debt already known is wrong, and no schema has
A model that cannot represent debt already known is wrong, and no schema has
been committed to yet. That is the cheapest place to find out.
**Slice two** makes worker observations durable, because that is the input the