441 lines
20 KiB
Go
441 lines
20 KiB
Go
package ingest
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
)
|
|
|
|
// bank.go: the ALLOWLISTED reading of the engine's whole-bank sidecar (`<project_db>.bank.json`,
|
|
// engine backlog row 125, landed D39.122).
|
|
//
|
|
// The bank lives in the engine's private SQLite, which this side must never open (D39.85), so the
|
|
// sidecar is the only channel there is. It is written atomically (temp + rename), which is what makes
|
|
// it readable while a run is going — the one engine artifact of the three that does not have to wait
|
|
// for a boundary.
|
|
//
|
|
// ⚠ Every vocabulary crossing here is TRANSLATED and none is passed through. `ruby` is Japanese
|
|
// furigana — pair-specific data in a shared layer — `mined` is the name of a pipeline stage and
|
|
// `draft` the name of a wave; the contract renamed all six values in 0.3.0 precisely so that none of
|
|
// them reaches a client (canon §TermStatus/§TermOrigin, companion §2.8). Doing the mapping at the
|
|
// seam rather than at projection time is deliberate: what is STORED is then already what a client
|
|
// reads, and no later path can leak a word by forgetting to translate it.
|
|
|
|
// BankTerm is one row of the bank, in the contract's own words.
|
|
type BankTerm struct {
|
|
// ID is the engine's derived key over the row's uniqueness key (src, sense, window). Stable
|
|
// across the bank being rebuilt — which it is on every run — and NOT across the book being cut
|
|
// differently, because the window is in chapter numbers.
|
|
ID string
|
|
Src string
|
|
Dst string
|
|
Kind string // "" when the engine did not decide: a legal state the row still needs signing in
|
|
Status string // proposed | in_progress | approved
|
|
Origin string // given | annotated | found
|
|
Sense string
|
|
// SinceChapter / UntilChapter are nil for "no boundary". The engine's own sentinel is 0, which is
|
|
// a value chapter numbering cannot produce (it starts at 1) and which meant two different things
|
|
// in two fields.
|
|
SinceChapter *int
|
|
UntilChapter *int
|
|
}
|
|
|
|
// Bank is the whole read-out.
|
|
type Bank struct {
|
|
// Boundary is which of the run's five publishing moments wrote this document, in the contract's
|
|
// words. It is read WITH the sections and not instead of them: the engine publishes `offered` at
|
|
// the signing stop and nowhere else, so a section standing at any other boundary is a document
|
|
// this build does not understand, and serving its rows would invent a stop that is not there.
|
|
Boundary string
|
|
// RunID is the anchor's other half — the id the engine announces its event stream under, which is
|
|
// the id this platform minted for the attempt (pgstore.EngineStreamID). Empty means the write had
|
|
// no run identity: unknown, never "current". Read and stored, never served: it is an engine
|
|
// identity, and the seam does not export those.
|
|
RunID string
|
|
Terms []BankTerm
|
|
// Offered is what the signing stop is asking about — mined rows NOT in the bank yet, in the stop's
|
|
// own ranking. A pointer to the slice because nil ("the document carried no section") and an empty
|
|
// slice ("the stop asked nothing") are different answers to a person looking at an empty screen,
|
|
// and a plain slice decodes both to nil.
|
|
Offered *[]OfferedTerm
|
|
// Consolidation is how complete the bank being signed is, nil when nothing measured it. The engine
|
|
// makes the section absent rather than zeroed where the paid pass never ran, because a zeroed one
|
|
// answers "consolidated 0, unanswered 0" — which reads as "nothing is missing".
|
|
Consolidation *Consolidation
|
|
}
|
|
|
|
// OfferedTerm is one row of a signing stop's table, in the contract's own words.
|
|
//
|
|
// ⛔ The numbers are pointers and the strings are not. For a field the engine writes ALWAYS, absence
|
|
// means a build without that field wrote the document — and for these four the zero value is itself a
|
|
// measurement (`freq: 0` is the engine's "only the draft side saw it"; a stated `conf: 0` is the row a
|
|
// signer must look at), so decoding absence to zero publishes a figure nobody took. Measured, not
|
|
// hypothetical: `conventions` is absent from all 69 rows of one bought stop read-out and present on
|
|
// all 66 of the other, because the field was added between the two runs. The empty string carries no
|
|
// such second meaning, so the strings lose nothing by letting absence land on them.
|
|
type OfferedTerm struct {
|
|
Src string
|
|
// Dst is the consolidated rendering, "" when this read-out carries none — and it does NOT say why.
|
|
// The engine's own sheet spends four facts on that glyph, one of which ("the bank already renders
|
|
// this surface, nothing to decide") it distinguishes as BankStopRow.SettledByBank and deliberately
|
|
// does not project here. Nothing downstream may read "" as "the service could not translate it".
|
|
Dst string
|
|
// Kind and Channel are "" for what this build cannot name — a kind the engine did not decide, a
|
|
// detector outside the closed set. Never a guess: Channel is how a person judges how well
|
|
// corroborated a row is, and invented corroboration is what reading further cannot undo.
|
|
Kind, Channel string
|
|
// Conventions is NOT re-derived from len(Variants) when absent, although the engine's projection
|
|
// makes them equal today: deriving is this side re-implementing engine law, and the engine's own
|
|
// comment records that reading one of these numbers as the other already cost two sessions a
|
|
// misfiled defect.
|
|
Freq, Spread, Conventions *int
|
|
// Confidence is nil when the service stated none — INCLUDING the engine's own spelling for it, a
|
|
// negative number. Folded here at the seam rather than at projection time, like every other
|
|
// crossing in this file and for the reason the note at the top gives: a translation left to a
|
|
// later path is one a later path can forget, and this one WAS forgotten — the canon promises
|
|
// `0..100` or null and a build that carried the minus through would have shown a person "-1 %".
|
|
//
|
|
// ⚠ It folds "the read-out carried no `conf` at all" into the same nil, and that is measured
|
|
// rather than assumed: `conf` entered the projection in the SAME commit as the section it lives
|
|
// in, so no build emitting the section omits it (0 of 135 rows across two bought read-outs).
|
|
// THE CONDITION THAT ENDS THAT: a build that starts omitting it — then absence stops being
|
|
// unreachable and the two causes need telling apart again.
|
|
Confidence *int
|
|
// Invented — the rendering is not one the drafts proposed, the class the engine names as the one to
|
|
// read first. A plain bool because the engine omits the field when false. ⚠ THE CONDITION THAT ENDS
|
|
// THAT: the day `omitempty` leaves the engine's field, absence stops meaning false and this has to
|
|
// become a pointer. Nothing here goes red on that day.
|
|
Invented bool
|
|
// Two lists and never one: "this run disagreed with itself" and "the book already calls it
|
|
// something else" are different decisions.
|
|
Contradicts, BankHolds []string
|
|
// The drafts' renderings, best-ranked first, in the engine's own labelling — opaque here and never
|
|
// parsed. It keeps a reader beside that writer precisely because a re-derived parser "would keep
|
|
// working until the day the label gains a field, and then it would report numbers rather than an
|
|
// error".
|
|
Variants []string
|
|
}
|
|
|
|
// Consolidation is what the paid terminology pass MANAGED. Six numbers and not one, because a cut
|
|
// RENDER pass leaves the bank partial while a cut CLASSIFY pass leaves it whole with the types
|
|
// unrefined — a different budget and a different remedy — and rows the role was never asked about are
|
|
// a saving rather than a gap.
|
|
type Consolidation struct {
|
|
// Complete comes from the engine ready-made: it is computed off the render pass alone, and a
|
|
// second mechanism answering that here is how the two come to disagree.
|
|
Complete bool
|
|
// Two cuts and never one sum: an operator raises one budget or the other, never "the" budget.
|
|
RenderBatchesDropped, ClassifyBatchesDropped int
|
|
Consolidated, Declined int
|
|
// Unanswered conflates the role's silence with the budget's cut — it means "the role stayed
|
|
// silent" only where Complete is true.
|
|
Unanswered int
|
|
// NeverAsked is the opposite of a gap: the bank already renders the surface and every draft agreed.
|
|
NeverAsked int
|
|
}
|
|
|
|
// The contract's TermStatus and TermOrigin values.
|
|
const (
|
|
TermProposed = "proposed"
|
|
TermInProgress = "in_progress"
|
|
TermApproved = "approved"
|
|
|
|
OriginGiven = "given"
|
|
OriginAnnotated = "annotated"
|
|
OriginFound = "found"
|
|
)
|
|
|
|
// The contract's ReadOutBoundary values, stored but never served. The engine's own names are pipeline
|
|
// vocabulary (`bank-mining` is a stage, `redrive` an operator's verb), which the note at the top of
|
|
// this file refuses to pass through.
|
|
//
|
|
// ⛔ BoundaryUnknown covers both a name this build does not know and the empty one a document from
|
|
// before the anchor carries. Safe to collapse because the consequence is identical — nothing says this
|
|
// is a stop — and the one thing neither may become is BoundarySignatureRequested.
|
|
const (
|
|
BoundaryRunStarted = "run_started"
|
|
BoundaryTermsTakenUnsigned = "terms_taken_unsigned"
|
|
BoundarySignatureRequested = "signature_requested"
|
|
BoundaryRunFinished = "run_finished"
|
|
BoundaryBookReseeded = "book_reseeded"
|
|
BoundaryUnknown = "unknown"
|
|
)
|
|
|
|
// The contract's OfferedTermChannel values. The engine's four — `mined`, `banknote`, `both`, `alias` —
|
|
// are pipeline vocabulary, one of them the very word the contract renamed in 0.3.0 so it could not
|
|
// reach a client.
|
|
const (
|
|
ChannelSourceText = "source_text"
|
|
ChannelTranslatedText = "translated_text"
|
|
ChannelBoth = "both"
|
|
ChannelAlias = "alias"
|
|
)
|
|
|
|
type wireBank struct {
|
|
Version string `json:"bank_version"`
|
|
BookID string `json:"book_id"`
|
|
// Additive on tm-bank-v1 and absent in documents written before the anchor existed, so an absent
|
|
// AsOf lands on BoundaryUnknown rather than refusing the document: such a read-out is old, not
|
|
// malformed.
|
|
AsOf string `json:"as_of"`
|
|
RunID string `json:"run_id"`
|
|
Terms []struct {
|
|
ID string `json:"id"`
|
|
Src string `json:"src"`
|
|
Dst string `json:"dst"`
|
|
Kind string `json:"kind"`
|
|
Status string `json:"status"`
|
|
Origin string `json:"origin"`
|
|
Sense string `json:"sense"`
|
|
SinceChapter int `json:"since_chapter"`
|
|
UntilChapter int `json:"until_chapter"`
|
|
} `json:"terms"`
|
|
// Pointers, so that an absent member stays distinguishable from an empty one — see Bank.Offered.
|
|
Proposed *[]wireOffered `json:"proposed"`
|
|
Consolidation *wireConsolidation `json:"consolidation"`
|
|
}
|
|
|
|
type wireOffered struct {
|
|
Src string `json:"src"`
|
|
Dst string `json:"dst"`
|
|
Kind string `json:"kind"`
|
|
Channel string `json:"channel"`
|
|
Freq *int `json:"freq"`
|
|
Spread *int `json:"spread"`
|
|
Conventions *int `json:"conventions"`
|
|
Conf *int `json:"conf"`
|
|
Invented bool `json:"invented"`
|
|
Contradicts []string `json:"contradicts"`
|
|
BankHolds []string `json:"bank_holds"`
|
|
Variants []string `json:"variants"`
|
|
}
|
|
|
|
type wireConsolidation struct {
|
|
Complete bool `json:"complete"`
|
|
RenderBatchesDropped int `json:"render_batches_dropped"`
|
|
ClassifyBatchesDropped int `json:"classify_batches_dropped"`
|
|
Consolidated int `json:"consolidated"`
|
|
Declined int `json:"declined"`
|
|
Unanswered int `json:"unanswered"`
|
|
NeverAsked int `json:"never_asked"`
|
|
}
|
|
|
|
// DecodeBank parses a bank read-out, and refuses anything that does not identify itself as one — the
|
|
// same guard the manifest carries, for the same reason: an empty bank and a document this build
|
|
// cannot read decode identically, and one of them would replace a book's whole bank with nothing.
|
|
func DecodeBank(b []byte) (Bank, error) {
|
|
var doc wireBank
|
|
if err := json.Unmarshal(b, &doc); err != nil {
|
|
return Bank{}, fmt.Errorf("ingest: decode bank: %w", err)
|
|
}
|
|
if doc.Version == "" {
|
|
return Bank{}, fmt.Errorf("ingest: decode bank: the document carries no bank_version, so it is not a bank")
|
|
}
|
|
out := Bank{
|
|
Boundary: readOutBoundary(doc.AsOf), RunID: doc.RunID,
|
|
Terms: make([]BankTerm, 0, len(doc.Terms)),
|
|
}
|
|
for _, t := range doc.Terms {
|
|
status, ok := termStatus(t.Status)
|
|
if !ok {
|
|
// A status this build has never heard of. Dropping the row is the wrong answer — it would
|
|
// silently shrink a bank somebody has to sign — and guessing `approved` would carry an
|
|
// unsigned term into the book as canon, so it is read as the state that asks for a
|
|
// decision.
|
|
status = TermProposed
|
|
}
|
|
origin, ok := termOrigin(t.Origin)
|
|
if !ok {
|
|
// Provenance is what the person signing judges trust by, and there is no safe guess: a row
|
|
// whose origin this build cannot name is reported as the one that claims the least about
|
|
// where it came from.
|
|
origin = OriginFound
|
|
}
|
|
out.Terms = append(out.Terms, BankTerm{
|
|
ID: t.ID, Src: t.Src, Dst: t.Dst, Kind: termKind(t.Kind),
|
|
Status: status, Origin: origin, Sense: t.Sense,
|
|
SinceChapter: chapterBound(t.SinceChapter), UntilChapter: chapterBound(t.UntilChapter),
|
|
})
|
|
}
|
|
out.Offered = decodeOffered(doc.Proposed)
|
|
out.Consolidation = decodeConsolidation(doc.Consolidation)
|
|
return out, nil
|
|
}
|
|
|
|
// decodeOffered reads the stop's section. The nil check IS the section's contract: no member means no
|
|
// section, a section with no rows means a stop that asked nothing.
|
|
func decodeOffered(rows *[]wireOffered) *[]OfferedTerm {
|
|
if rows == nil {
|
|
return nil
|
|
}
|
|
out := make([]OfferedTerm, 0, len(*rows))
|
|
for _, r := range *rows {
|
|
out = append(out, OfferedTerm{
|
|
Src: r.Src, Dst: r.Dst, Kind: termKind(r.Kind), Channel: offeredChannel(r.Channel),
|
|
Freq: statedCount(r.Freq), Spread: statedCount(r.Spread),
|
|
Conventions: statedCount(r.Conventions),
|
|
Confidence: statedConfidence(r.Conf), Invented: r.Invented,
|
|
Contradicts: r.Contradicts, BankHolds: r.BankHolds, Variants: r.Variants,
|
|
})
|
|
}
|
|
return &out
|
|
}
|
|
|
|
// decodeConsolidation copies the numbers and derives none of them.
|
|
//
|
|
// ⚠ A section that is PRESENT and empty decodes to "not complete, nothing consolidated" — the
|
|
// cautious direction, and the opposite of the rule for the numbers above, where a zero would have
|
|
// invented a measurement. The two look contradictory until you ask which way each zero errs.
|
|
//
|
|
// ⛔ AND THE CONDITION UNDER WHICH THAT STOPS HOLDING, because it holds only by an accident of how
|
|
// the writer grew: the seven fields entered the engine's projection in ONE commit, so no build emits
|
|
// a partial section and "present" always means all seven. The day the engine adds an eighth number,
|
|
// a document written by the older build decodes it as zero — and `unanswered: 0` says "the role
|
|
// stayed silent about nothing", which is the false zero this whole reader exists to refuse. It is
|
|
// cautious for `complete` and a lie for the counters, and the difference appears the moment the
|
|
// field set moves. Then these become pointers, like the four above.
|
|
func decodeConsolidation(c *wireConsolidation) *Consolidation {
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
return &Consolidation{
|
|
Complete: c.Complete,
|
|
Consolidated: c.Consolidated, Declined: c.Declined,
|
|
Unanswered: c.Unanswered, NeverAsked: c.NeverAsked,
|
|
RenderBatchesDropped: c.RenderBatchesDropped,
|
|
ClassifyBatchesDropped: c.ClassifyBatchesDropped,
|
|
}
|
|
}
|
|
|
|
// BoundaryOrUnknown is the closed set as a GUARD, for a caller that did not come through DecodeBank.
|
|
// The zero value of a Bank carries no boundary, and a read-out whose moment nobody named is exactly
|
|
// what BoundaryUnknown is for — refusing instead would fail a whole bank save over a label.
|
|
func BoundaryOrUnknown(s string) string {
|
|
switch s {
|
|
case BoundaryRunStarted, BoundaryTermsTakenUnsigned, BoundarySignatureRequested,
|
|
BoundaryRunFinished, BoundaryBookReseeded:
|
|
return s
|
|
}
|
|
return BoundaryUnknown
|
|
}
|
|
|
|
// KindOrNone and ChannelOrNone are the same guard for the two closed vocabularies an offered term
|
|
// carries, whose "cannot name it" is "" rather than a value.
|
|
//
|
|
// ⛔ They are NOT what `nullif($, ”)` does in the statement, and the difference is a live failure
|
|
// rather than a nicety: `nullif` turns the EMPTY value into null, while these turn an UNKNOWN
|
|
// non-empty one into the empty value first. Measured — an engine word that leaked this far without a
|
|
// guard does not land as "not named", it violates the column's CHECK and takes the WHOLE bank save
|
|
// down, at the one boundary a person is standing at.
|
|
func KindOrNone(s string) string { return termKind(s) }
|
|
|
|
func ChannelOrNone(s string) string {
|
|
switch s {
|
|
case ChannelSourceText, ChannelTranslatedText, ChannelBoth, ChannelAlias:
|
|
return s
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// ⛔ A NUMBER OUTSIDE THE RANGE THE CONTRACT DECLARES IS READ AS «NOT STATED» — the same discipline
|
|
// the vocabularies above follow, one floor down: a value this build cannot name becomes the one that
|
|
// claims the least, never a guess and never a pass-through.
|
|
//
|
|
// It is a CLASS and not one field, which is how it was found: the engine spells "the reply carried no
|
|
// confidence" as a negative, and a build that let that through would show a person "-1 %" on the one
|
|
// screen that asks them to decide. But the canon bounds `freq`, `spread` and `conventions` too, and
|
|
// nothing there is a sentinel — a negative count is simply a document this build does not understand,
|
|
// and publishing it would be the same lie in a quieter place.
|
|
//
|
|
// ⚠ The bounds are mirrored from the canon rather than read from it at runtime, like the closed
|
|
// vocabularies above, and like them they are gated against it:
|
|
// TestEveryRangeTheCanonDeclaresIsOneThisReaderEnforces reads `minimum`/`maximum` out of the canon and
|
|
// fails on any ranged member this file does not bound. THE CONDITION THAT ENDS THE MIRRORING: a canon
|
|
// that gives a member a range this file has no branch for — which is exactly what that gate says.
|
|
func statedCount(v *int) *int { return inRange(v, 0, -1) }
|
|
|
|
func statedConfidence(v *int) *int { return inRange(v, 0, 100) }
|
|
|
|
// inRange keeps a value the contract would accept and drops one it would not. A negative max means
|
|
// "no ceiling", which is what the canon says by declaring `minimum` alone.
|
|
func inRange(v *int, min, max int) *int {
|
|
if v == nil || *v < min || (max >= 0 && *v > max) {
|
|
return nil
|
|
}
|
|
return v
|
|
}
|
|
|
|
func readOutBoundary(engine string) string {
|
|
switch engine {
|
|
case "run-start/seeded":
|
|
return BoundaryRunStarted
|
|
case "bank-mining/auto-continue":
|
|
return BoundaryTermsTakenUnsigned
|
|
case "bank-mining/signature-stop":
|
|
return BoundarySignatureRequested
|
|
case "run-finished":
|
|
return BoundaryRunFinished
|
|
case "redrive/re-seeded":
|
|
return BoundaryBookReseeded
|
|
}
|
|
return BoundaryUnknown
|
|
}
|
|
|
|
// offeredChannel returns "" for a detector outside the closed set — the same spelling termKind uses
|
|
// for a value this build cannot name, so one convention carries "not nameable" all the way out.
|
|
func offeredChannel(engine string) string {
|
|
switch engine {
|
|
case "mined":
|
|
return ChannelSourceText
|
|
case "banknote":
|
|
return ChannelTranslatedText
|
|
case "both":
|
|
return ChannelBoth
|
|
case "alias":
|
|
return ChannelAlias
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func termStatus(engine string) (string, bool) {
|
|
switch engine {
|
|
case "auto":
|
|
return TermProposed, true
|
|
case "draft":
|
|
return TermInProgress, true
|
|
case "approved":
|
|
return TermApproved, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func termOrigin(engine string) (string, bool) {
|
|
switch engine {
|
|
case "seed":
|
|
return OriginGiven, true
|
|
case "ruby":
|
|
return OriginAnnotated, true
|
|
case "mined":
|
|
return OriginFound, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// termKind passes the engine's classification through the contract's closed vocabulary. A kind
|
|
// outside it — including the empty one the engine legitimately produces for a candidate it could not
|
|
// classify — becomes "kind not decided", which is a state the contract has and a client must render.
|
|
// Passing an unknown value through instead would hand a generated client a value its union does not
|
|
// contain.
|
|
func termKind(engine string) string {
|
|
switch engine {
|
|
case "name", "place", "title", "term", "nickname":
|
|
return engine
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func chapterBound(n int) *int {
|
|
if n <= 0 {
|
|
return nil
|
|
}
|
|
return &n
|
|
}
|