package pipeline import "textmachine/backend/internal/store" // driftbasis.go: the config-drift verdict, and the BASIS that says what the verdict is a verdict OF. // // THE DEFECT THIS CLOSES. `config_drift` is a BOOLEAN over a THREE-valued fact. `false` means both «the // stored rows match the current config» and «the check could not run», and the two are opposite // instructions to whoever reads them. Both surfaces already knew it and neither could say it: // export.go logged «drift state unknown (reported as none)» — the one place in the engine where that // sentence appears next to a field that then reports none, while four sibling surfaces say «unknown, not // zero» and mean it (quality.go, status.go's unsigned-term count, bookbuild.go's stale check). // // It is not a silent field either: `build` GATES ON IT. staleUnits opens with `if exp.ConfigDrift { // return nil, true }`, so a failed drift check that reports `false` makes build compute staleness as // though the config were clean and publish `stale: 0` over an unknown state — a field WITHOUT a basis // corrupting the field next to it that HAS one. That propagation is why this is not cosmetic. // // The form is the zone's own, not a new invention: RebillBasis (status.go) already publishes «what are // these figures a projection of», down to a `failed` value whose doc says «the figures are zero because // they are UNKNOWN». This is that discipline applied to the second figure that needed it. // Ratified with the disclosure law, D39.181 п.3. // The values of the config-drift basis. A reader that understands only the boolean is unaffected; a reader // that wants to know whether `false` is an answer looks here. const ( // DriftBasisNone — the check RAN and the stored rows are resolved under the snapshot the current // config renders. `config_drift:false` is an ANSWER. DriftBasisNone = "none" // DriftBasisDrift — the check ran and found a difference. `config_drift:true`. DriftBasisDrift = "drift" // DriftBasisUnknown — the check could NOT run (the bank could not be materialized, a wave snapshot // could not be rendered, a re-pricing the check depends on failed). `config_drift:false` is then NOT // an answer, and a consumer must treat the book as un-judged rather than as clean. The reason is on // the WARN log beside it. DriftBasisUnknown = "unknown" ) // driftCheckable is the ONE precondition both surfaces ask before they judge drift at all: are there // stored rows carrying a snapshot to compare against. // // ⛔ IT EXISTS BECAUSE THE CURE REPRODUCED THE DISEASE. The first version let each surface decide for // itself when the check was possible — `status` required stored rows, `export` assumed it could always // run — and on ONE book with ONE config they answered `unknown` and `none`. That is backlog row 239 // exactly: two $0 read-models disagreeing about the same fact, this time inside the field built to stop // it. Found by acceptance. A shared predicate is the only form that cannot drift. func driftCheckable(statuses []store.ChunkStatus) bool { for _, cs := range statuses { if cs.SnapshotID != "" { return true } } return false } // driftBasisFor collapses the two facts a caller holds — did the check run, did it find drift — into the // published word. One definition, because status and export must never disagree about what `false` means. func driftBasisFor(ran, drifted bool) string { switch { case !ran: return DriftBasisUnknown case drifted: return DriftBasisDrift default: return DriftBasisNone } } // orphanStageRows reports whether the stored rows carry a stage the CURRENT pipeline does not run — a // stage renamed or removed since the run — and names the first such stage. // // THIS IS THE RULE `export` HAD AND `status` DID NOT, and the divergence was reproduced word for word on // the cold run: one config, one database, `status --json` answering config_drift=false with // percent_done=85 while `export --json` on the SAME config answered true. The per-wave comparison both // surfaces already did only compares the snapshots of stages that STILL EXIST, so dropping the editor // stage makes the draft rows the shipping rows and nothing fires: an unedited book reads as complete. // // ⛔ WHAT IS DELIBERATELY *NOT* SHARED. The two surfaces fold the BANK differently — status folds the // decision files because it prices the next run, export reads the stored glossary because it judges the // document that exists — and export.go says at length that the divergence is deliberate and asks the next // reader not to "fix" it. Only the orphan rule is lifted here, because only the orphan rule is the same // question on both surfaces. Sorted lookup: the reported stage must not depend on map iteration order. func orphanStageRows(statuses []store.ChunkStatus, draftNames, editNames map[string]bool) (stage string, found bool) { for _, cs := range statuses { if cs.SnapshotID == "" || draftNames[cs.Stage] || editNames[cs.Stage] { continue } if !found || cs.Stage < stage { stage, found = cs.Stage, true } } return stage, found }