package ingest import ( "encoding/json" "fmt" "textmachine/platform/internal/money" ) // StatusReport is the ALLOWLISTED subset of `tmctl status --json` (pipeline.StatusReport) that the // platform materializes. Everything absent here is absent on purpose: snapshot ids, drift itself, // routing, content labels and the operator's flag taxonomy are engine vocabulary with no consumer // on this side. The rule the list enforces is about the WIRE, not the seam: §2.12 forbids these // words from reaching a client, and D39.84 forbids projecting money into an API response or an // INFO log — crossing the seam into the credit and consent machinery is legal and precedented // (Spend and Reserved below; the re-bill pair, D39.165 §3). An earlier edition of this header read // the rule as "must not cross the seam", which its own field comments had already corrected — the // refuter of P10 caught the two readings standing side by side. Unknown JSON fields are simply // ignored by encoding/json. type StatusReport struct { BookID string `json:"book_id"` TotalUnits int `json:"total_units"` Done int `json:"done"` InProgress int `json:"in_progress"` Flagged int `json:"flagged"` Pending int `json:"pending"` // Progress is the PER-WAVE split of the same units, landed in the engine on 09.08 (D39.122; // pipeline.PhaseProgress). Taking it is what makes the resync channel as fine-grained as the // stream: without it a repair could restore only the aggregate, and folding one aggregate over a // projection the stream had already split REPLACED "draft 7/20 ∥ edit 1/20" with one number. // Same shape as the stream's own progress event, so both paths materialize through one type. Progress Progress `json:"progress"` // ETASeconds is what the owner asked to show (K-5); the engine already computes it. ETASeconds float64 `json:"eta_seconds"` // UnsignedBankTerms backs the signing screen's "N of M decided" while a stop is standing. UnsignedBankTerms int `json:"unsigned_bank_terms"` // The engine's re-bill projection (rebill_units/rebill_usd) is deliberately NOT taken — but the // REASON changed under this comment, and both halves of what it used to say are now retired // (register row PD-427). // // ⚠ The old reason was TIMING, and it was true when it was written: status projected the STORED // memory, so right after a `bank-apply` — the one moment a consent would want the figure — it // honestly read zero (P10 errata 28.08-к). That blind window is CLOSED: the engine's read path // now folds the bank first and falls back to the stored glossary only if it cannot // (`pipeline.projectStoredMemory`, "IT IS NO LONGER THE READ PATH'S FIRST ANSWER", landing // 6ec9f8a / D39.170). `status` will price a re-pass BEFORE it is bought, and still writes nothing. // // ⚠ The old comment also predicted the SHAPE of the fix — "the pair returns with the engine verb // that can fold and price a correction outside a run" — and that is the half worth correcting // loudest, because a wrong REASON gets re-checked while a wrong EXPECTATION gets used as a map. // No such verb was built and none is planned: the existing `status` was fixed instead. // // Why the pair is still not taken TODAY: its wiring is gated together with the engine's // `translate --max-units`, which waits on a decision of its own (the volume ceiling was broken on // a book that mines its bank; the cure landed, the wiring did not). So the figure exists and the // seam simply does not carry it yet — which is a different sentence from the one this comment // used to make, and the difference is what a later session would otherwise design against. // The consents are meanwhile funded from the run's own hold. // Spend is the engine's committed spend, converted to integer micro-USD AT THE SEAM. The wire // value is a JSON decimal; binding it to a float64 would put drift one step before the integer // column that exists to prevent drift (PD-15). It is stored in the credit tables and NEVER // projected into an API response or an INFO log (D39.84). // A POINTER: absent, null and empty must not read as "the attempt cost nothing". A settlement // computed from a missing figure would release the whole hold and charge zero. Spend *money.MicroUSD `json:"committed_usd"` // Reserved is what the engine's OWN ledger has promised and not yet committed. It is NOT part of // the ceiling the platform computes — see runs.meter.bookCap: at spawn there is no other writer, // so anything reserved is a leftover of a dead process, and the engine's own write-open clears it // before judging anything. What this figure does is prove that reasoning holds, which is why the // spawn REQUIRES it and refuses without it. // A POINTER for the same reason Spend is one: absent, null and zero are three different facts. Reserved *money.MicroUSD `json:"reserved_usd"` Chapters []ChapterStatus `json:"chapters"` } // ChapterStatus is the per-chapter passport, allowlisted the same way (no cost, no verdict ranks). // // worst_flag_reason is deliberately NOT taken: contract v0 gives a chapter a note_count and nothing // about the worst reason, so materializing it would store engine vocabulary no reader asks for // (PD-19). It comes back with a column the day the chapter screen needs it. type ChapterStatus struct { Chapter int `json:"chapter"` UnitsTotal int `json:"units_total"` UnitsDone int `json:"units_done"` UnitsFlagged int `json:"units_flagged"` UnitsInProgress int `json:"units_in_progress"` UnitsPending int `json:"units_pending"` } // DecodeStatus parses a status report. func DecodeStatus(b []byte) (StatusReport, error) { var r StatusReport if err := json.Unmarshal(b, &r); err != nil { return StatusReport{}, fmt.Errorf("ingest: decode status: %w", err) } return r, nil }