93 lines
5 KiB
Go
93 lines
5 KiB
Go
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
|
|
}
|