110 lines
5.5 KiB
Go
110 lines
5.5 KiB
Go
package ingest
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
)
|
|
|
|
// build.go: the ALLOWLISTED reading of `tmctl build`'s report (`tm-build-v1`, D39.175).
|
|
//
|
|
// It is the LAST of the engine's seven channels to get a reader here, and it is read for one
|
|
// ratified reason: the export door must check the REPORT and not only the file (D39.175 п.2,
|
|
// D39.178 п.3). What the report carries beyond "a file exists" is addressed to the OPERATOR and to
|
|
// the interface, never to the reader of the book — config drift and an unknowable staleness verdict
|
|
// are the operator's vocabulary, and the honesty a reader is owed is inside the file itself, on the
|
|
// title page and at every hole (D39.175 п.1).
|
|
//
|
|
// ⚠ `files` is deliberately NOT projected past this package. Absolute paths are server topology,
|
|
// the same rule that keeps the book's `workdir` off the wire; the door reads the path it ASKED the
|
|
// engine to write (`--out`), which is the one it can also serve and delete.
|
|
|
|
// KnownBuildVersion is the shape of the engine's build report this build was written against
|
|
// (backend/internal/pipeline/bookbuild.go, `buildVersion`).
|
|
//
|
|
// ⚠ Its value is NOT gated, and that is the manifest's rule read the other way round. The manifest
|
|
// is a CUT whose fields renumber a book, so a shape this platform has not seen must stop the
|
|
// intake; a build report is a receipt for a file that already exists on disk, and refusing to read
|
|
// it would throw away a completed, paid-for artifact over a version string. The PRESENCE of the
|
|
// field is required — that is the different question, "is this a build report at all" — and a value
|
|
// other than this one is logged by the caller.
|
|
const KnownBuildVersion = "tm-build-v1"
|
|
|
|
// BuildReport is what one `tmctl build` produced.
|
|
type BuildReport struct {
|
|
// Version is `build_version`; recorded so a caller can say which shape answered it.
|
|
Version string
|
|
BookID string
|
|
// Complete is true when the file has no hole of any kind — pending, withheld, incomplete, stale,
|
|
// ghost. A file written under `--partial` with Complete false carries the notice on its first
|
|
// page and a mark at every hole.
|
|
Complete bool
|
|
// The five counts behind Complete, so an operator sees WHY a copy is marked without opening it.
|
|
PendingUnits int
|
|
WithheldUnits int
|
|
IncompleteUnits int
|
|
StaleUnits int
|
|
GhostRows int
|
|
// StaleUnknown is the engine's honest third answer: for at least one shipped unit it could not
|
|
// be decided whether the source moved underneath it. Never folded into "none" — that fold is
|
|
// what the engine's own field comment forbids.
|
|
StaleUnknown bool
|
|
// ConfigDrift is NOT a hole of the file (owner's ratification, D39.175 п.2): the bytes written
|
|
// are the bytes the run shipped. It is a warning to the operator that the current configuration
|
|
// would render a different snapshot.
|
|
ConfigDrift bool
|
|
// RemovedFiles and StaleCopies are the engine's disclosure of what its housekeeping did beside
|
|
// the project database. The door builds with `--out`, where the engine does no housekeeping at
|
|
// all, so a non-empty list here means this platform asked for something it did not think it was
|
|
// asking for — which is worth a line rather than a shrug.
|
|
RemovedFiles []string
|
|
StaleCopies []string
|
|
}
|
|
|
|
type wireBuild struct {
|
|
Version string `json:"build_version"`
|
|
BookID string `json:"book_id"`
|
|
TotalUnits int `json:"total_units"`
|
|
PendingUnits int `json:"pending_units"`
|
|
WithheldUnits int `json:"withheld_units"`
|
|
IncompleteUnits int `json:"incomplete_units"`
|
|
StaleUnits int `json:"stale_units"`
|
|
StaleUnknown bool `json:"stale_unknown"`
|
|
GhostRows int `json:"ghost_rows"`
|
|
ConfigDrift bool `json:"config_drift"`
|
|
RemovedFiles []string `json:"removed_files"`
|
|
StaleCopies []string `json:"stale_copies"`
|
|
Complete bool `json:"complete"`
|
|
}
|
|
|
|
// DecodeBuild parses a build report, and refuses anything that does not identify itself as one.
|
|
//
|
|
// The guard is the same one the manifest and the bank carry, for the same reason: an empty JSON
|
|
// object decodes into a report that says "complete, no holes, nothing removed", which is the most
|
|
// reassuring thing this type can say and the one a document from a process that is not the engine
|
|
// would say for free.
|
|
func DecodeBuild(b []byte) (BuildReport, error) {
|
|
var doc wireBuild
|
|
if err := json.Unmarshal(b, &doc); err != nil {
|
|
return BuildReport{}, fmt.Errorf("ingest: decode build report: %w", err)
|
|
}
|
|
if doc.Version == "" {
|
|
return BuildReport{}, fmt.Errorf("ingest: decode build report: the document carries no build_version, so it is not a build report")
|
|
}
|
|
return BuildReport{
|
|
Version: doc.Version, BookID: doc.BookID, Complete: doc.Complete,
|
|
PendingUnits: doc.PendingUnits, WithheldUnits: doc.WithheldUnits,
|
|
IncompleteUnits: doc.IncompleteUnits, StaleUnits: doc.StaleUnits,
|
|
GhostRows: doc.GhostRows, StaleUnknown: doc.StaleUnknown, ConfigDrift: doc.ConfigDrift,
|
|
RemovedFiles: doc.RemovedFiles, StaleCopies: doc.StaleCopies,
|
|
}, nil
|
|
}
|
|
|
|
// NeedsOperator reports whether this report carries something addressed to the operator rather than
|
|
// to the reader of the book: the drift warning the door is REQUIRED to check (D39.175 п.2), the
|
|
// unknowable staleness verdict, or housekeeping the door did not expect to trigger.
|
|
//
|
|
// It exists so the door has ONE place that decides what is worth a log line, instead of a condition
|
|
// spelled out at every call site and drifting between them.
|
|
func (r BuildReport) NeedsOperator() bool {
|
|
return r.ConfigDrift || r.StaleUnknown || len(r.RemovedFiles) > 0 || len(r.StaleCopies) > 0
|
|
}
|