66 lines
3.3 KiB
Go
66 lines
3.3 KiB
Go
package ingest
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
)
|
|
|
|
// Manifest is the ALLOWLISTED summary of the engine's chapter manifest (`tmctl manifest --json`,
|
|
// unified backlog row 100, form ratified with D39.122 as `tm-manifest-v2`).
|
|
//
|
|
// What is deliberately absent is most of the document: the per-chapter and per-unit arrays carry the
|
|
// engine's stable ids, its dense ordinals and its rendered headings, and none of them has a reader
|
|
// on this side yet — the platform's chapter tree is written by the materializer from the stream, not
|
|
// from here. Taking them now would store engine vocabulary nobody asks for and would have to be
|
|
// maintained against a document that re-cuts itself.
|
|
//
|
|
// What IS taken is what intake decides with: how many chapters the book has (the ceiling scale is
|
|
// built on it), and the two identifiers that say WHICH cut of WHICH bytes produced that number.
|
|
type Manifest struct {
|
|
// Version is `manifest_version`. Its VALUE is recorded and logged rather than gated — the fields
|
|
// below are counts and identities whose meaning is stable, and pinning the value here would make
|
|
// every engine release a platform release. Its PRESENCE is required (DecodeManifest), which is a
|
|
// different question: not "which manifest is this" but "is this a manifest at all".
|
|
Version string `json:"manifest_version"`
|
|
ChaptersTotal int `json:"chapters_total"`
|
|
UnitsTotal int `json:"units_total"`
|
|
// SourceSHA256 is the digest of the ingested source, hex. It answers "is the file on disk still
|
|
// the one that was cut" without the platform reading the file again.
|
|
SourceSHA256 string `json:"source_sha256"`
|
|
SourceBytes int64 `json:"source_bytes"`
|
|
// ChunkerVersion is one of the inputs of the manifest's validity key: a change re-numbers
|
|
// chapters, so a stored tree is only comparable within one of these (register row PD-166).
|
|
ChunkerVersion string `json:"chunker_version"`
|
|
}
|
|
|
|
// SourceSHA256Bytes is the digest as the column stores it. An unparsable value is stored as nothing
|
|
// rather than as garbage: the field is evidence, and evidence that cannot be decoded is absence.
|
|
func (m Manifest) SourceSHA256Bytes() []byte {
|
|
b, err := hex.DecodeString(m.SourceSHA256)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return b
|
|
}
|
|
|
|
// DecodeManifest parses a manifest document, and refuses anything that does not identify itself as
|
|
// one.
|
|
//
|
|
// ⚠ The identity check is not decoration, and what it protects is the one path in this zone that
|
|
// DESTROYS a user's file. `{}`, `null` and any JSON object of fields this build has never heard of
|
|
// all decode into a Manifest of zeros, and intake reads a zero chapter count as "the engine read the
|
|
// source and there is no book in it" — the single verdict that removes the upload (books.Parse,
|
|
// books.reject). So a document that is not a manifest must not be able to arrive as an EMPTY one:
|
|
// the two are opposite facts, and only one of them is destructive. Raised by the seam lens of the
|
|
// dofix review.
|
|
func DecodeManifest(b []byte) (Manifest, error) {
|
|
var m Manifest
|
|
if err := json.Unmarshal(b, &m); err != nil {
|
|
return Manifest{}, fmt.Errorf("ingest: decode manifest: %w", err)
|
|
}
|
|
if m.Version == "" {
|
|
return Manifest{}, fmt.Errorf("ingest: decode manifest: the document carries no manifest_version, so it is not a manifest")
|
|
}
|
|
return m, nil
|
|
}
|