153 lines
5.3 KiB
Go
153 lines
5.3 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 {
|
|
Terms []BankTerm
|
|
}
|
|
|
|
// The contract's TermStatus and TermOrigin values.
|
|
const (
|
|
TermProposed = "proposed"
|
|
TermInProgress = "in_progress"
|
|
TermApproved = "approved"
|
|
|
|
OriginGiven = "given"
|
|
OriginAnnotated = "annotated"
|
|
OriginFound = "found"
|
|
)
|
|
|
|
type wireBank struct {
|
|
Version string `json:"bank_version"`
|
|
BookID string `json:"book_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"`
|
|
}
|
|
|
|
// 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{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),
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
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
|
|
}
|