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, 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". if b.Status != "uploading" { count := b.CharacterCount out.CharacterCount = &count } 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, // 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, CeilingChapters: r.CeilingChapters, 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 }