textmachine/platform/internal/httpapi/project.go

130 lines
4.8 KiB
Go

package httpapi
import (
"textmachine/platform/internal/ingest"
"textmachine/platform/internal/pgstore"
)
// project.go: the read model as the contract's shapes.
//
// Everything here is a TRANSLATION and not a forwarding. Three vocabularies meet at this line — the
// engine's, the platform's own and the contract's — and the rule is the same for all three: what
// leaves is the contract's, and a value this build cannot map leaves as `null` rather than as a word
// a generated client's union does not contain.
func projectBook(b pgstore.Book) wireBook {
out := wireBook{
ID: b.ID, Revision: b.Revision, Title: b.Title,
SourceLang: b.SourceLang, TargetLang: b.TargetLang,
Status: b.Status,
StructureVersion: b.StructureVersion,
ShapeEpoch: b.ShapeEpoch,
ChapterCount: b.ChapterCount,
ChaptersDone: b.ChaptersDone,
AddedAt: b.AddedAt,
NoteCount: b.NoteCount,
}
// `null` while the book is still arriving: the size is counted as the bytes go past, so a book
// that is still on the wire has no size yet — which is a different fact from "zero characters".
//
// The ENGINE's figure wins whenever there is one, and the flag beside it says which was used. The
// two are not interchangeable and the difference is not a rounding: for an EPUB the intake's
// number counts the runes of a ZIP archive (books.counter). Until a manifest has been read there
// is nothing better to say, so the approximation is served and honestly labelled rather than
// withheld — a screen with no size at all is worse than one that says its size is approximate.
if b.Status != "uploading" {
count := b.CharacterCount
if b.SourceChars != nil {
count, out.CharacterCountExact = *b.SourceChars, true
}
out.CharacterCount = &count
}
if b.Structure != "" {
structure := b.Structure
out.Structure = &structure
}
if reason := ingest.ContractRejectReason(b.RejectReason); reason != "" {
out.RejectReason = &reason
}
return out
}
func projectRun(r pgstore.Run) wireRun {
out := wireRun{
ID: r.ID, BookID: r.BookID, Revision: r.Revision, Status: r.Status,
// ⚠ The wire name is the product's and the column's is the engine flag's (`--verify-bank`).
// They are deliberately different words for the same bit: what the user chose is "stop so I
// can look at the terms", and the flag is how that reaches the engine.
StopForSigning: r.VerifyBank,
OrderedChapters: r.OrderedChapters,
DeliveredChapters: r.DeliveredChapters,
TermConsistencyFunded: r.BondFunded,
// The user's own click, which no `status` answers: a stop asked for mid-translation can meet
// the run reaching the bank signature, and `awaiting_bank` then offers to continue on a click
// that meant "stop". REQUIRED and never omitted — no stop is `false`, and an absent field
// would be a second way of writing the same fact.
StopRequested: r.StopRequested,
Progress: wireProgress{
Done: r.Progress.Done, Total: r.Progress.Total, Stage: r.Progress.Stage,
ETASeconds: r.Progress.ETASeconds,
},
StartedAt: r.StartedAt, FinishedAt: r.FinishedAt,
}
if reason := ingest.ContractPausedReason(r.PausedReason); reason != "" {
out.PausedReason = &reason
}
if r.FailureReason != "" {
out.FailureReason = &r.FailureReason
}
return out
}
func projectChapter(c pgstore.Chapter) wireChapter {
return wireChapter{
ID: c.ID, Number: c.Number,
// ⚠ ALWAYS `null` today, and that is the contract being obeyed rather than a hole. The engine's
// manifest carries a `heading`, and it is a deterministic render of the pair's heading rule
// («Глава N») — the engine's own comment forbids presenting it as a label carried by the book,
// and the canon forbids a deployment to put a rendered ordinal in this field. A client with no
// label renders its own ordinal, in the language of ITS interface, which the server does not
// know. Real labels need a producer that does not exist yet (engine backlog row 160).
Heading: nil,
UnitsTotal: c.UnitsTotal,
UnitsDone: c.UnitsDone,
NoteCount: c.NoteCount,
}
}
func projectUnit(u pgstore.Unit) wireUnit {
out := wireUnit{
ID: u.ID, Source: u.Source, Target: u.Target, State: u.State,
Notes: make([]wireNote, 0, len(u.Notes)),
}
for _, n := range u.Notes {
out.Notes = append(out.Notes, projectNote(n))
}
return out
}
func projectNote(n pgstore.Note) wireNote {
code, severity := ingest.NoteCode(n.Reason)
out := wireNote{
ID: n.ID, CreatedAt: n.CreatedAt, Severity: severity, Code: code, ChapterID: n.ChapterID,
}
if n.UnitID != "" {
out.UnitID = &n.UnitID
}
return out
}
func projectTerm(t pgstore.BankTerm) wireTerm {
out := wireTerm{
ID: t.ID, Src: t.Src, Dst: t.Dst, Status: t.Status, Origin: t.Origin, Sense: t.Sense,
SinceChapter: t.Since, UntilChapter: t.Until,
}
if t.Kind != "" {
kind := t.Kind
out.Kind = &kind
}
return out
}