Give the store its own projection type without a character count, so one number cannot live in two fields free to drift apart
This commit is contained in:
parent
6069e52aac
commit
9845098f67
3 changed files with 42 additions and 8 deletions
|
|
@ -12,9 +12,10 @@ import (
|
|||
func priced() Structure {
|
||||
in := twoChapters("k1")
|
||||
in.Structure = ingest.StructureDetected
|
||||
in.Price = &ingest.BookPrice{
|
||||
ExpectedUSD: 2_090_000, BookOnceUSD: 2_000_000, StepMaxUSD: 69_828, SourceChars: 3000,
|
||||
}
|
||||
in.Price = &Projection{Expected: 2_090_000, BookOnce: 2_000_000, StepMax: 69_828}
|
||||
// ⚠ ON THE CUT, NOT ON THE PRICE — the store keeps the two apart on purpose, and the type makes
|
||||
// that the only way to say it (see Projection).
|
||||
in.SourceChars = 3000
|
||||
// ⚠ The price lives on the UNIT and only there: a chapter's cost is the sum of the units still to
|
||||
// deliver, so a chapter-level copy would be a second carrier free to drift from it.
|
||||
in.Chapters[0].Units[0].Expected, in.Chapters[0].Units[0].SourceChars = 40_000, 1200
|
||||
|
|
@ -201,13 +202,18 @@ func TestTheEnginesCharacterCountReachesTheLibraryRowBesideItsAccuracy(t *testin
|
|||
// count too, and «Знаков» fell back to the intake's approximation, which for an EPUB counts the runes
|
||||
// of a ZIP archive. The two facts are independent: one is about the text, the other about money.
|
||||
//
|
||||
// Mutation caught: writing `source_chars` off `in.Price` again.
|
||||
// ⚠ Since Projection was cut down to money alone, writing the count off the price is no longer
|
||||
// EXPRESSIBLE — the field it would be read from does not exist. What this pin still catches is the
|
||||
// other half of the same mistake: gating the write on the price's arrival.
|
||||
//
|
||||
// Mutation caught: writing a null `source_chars` whenever `in.Price` is nil.
|
||||
func TestTheCharacterCountSurvivesAPriceThisBuildCouldNotRead(t *testing.T) {
|
||||
s, ctx := testDB(t)
|
||||
book := readingBook(t, s, ctx, "u1")
|
||||
in := priced()
|
||||
in.Price = nil // the money did not arrive…
|
||||
in.SourceChars = 3000 // …and the engine's count of the text did
|
||||
in.SourceChars = 3000 // …and the engine's count of the text did (priced() sets it; said again here
|
||||
// because this test is ABOUT that figure and a reader should not have to go look)
|
||||
if err := s.SaveStructure(ctx, book, in); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ type Structure struct {
|
|||
// whole one. A book with no projection is one the platform REFUSES to sell rather than one it
|
||||
// prices with a constant (pricing's own package comment), so nil is written through: a projection
|
||||
// that goes stale is worse than one that is honestly absent.
|
||||
Price *ingest.BookPrice
|
||||
Price *Projection
|
||||
// Structure is where the chapter boundaries came from, verbatim from the engine. Empty when the
|
||||
// engine did not say — a manifest older than the landing — and stored as such: the decision it
|
||||
// feeds (ingest.ChapterOrdersOffered) treats what it does not know as untrusted.
|
||||
|
|
@ -92,6 +92,30 @@ type Structure struct {
|
|||
SourceChars int64
|
||||
}
|
||||
|
||||
// Projection is the MONEY half of the engine's a-priori figure, as the store holds it.
|
||||
//
|
||||
// ⛔ IT CARRIES NO CHARACTER COUNT, and the absence is the whole reason this type exists rather than
|
||||
// the wire's own `ingest.BookPrice`. The engine publishes `source_chars` INSIDE its price object,
|
||||
// because for the engine both are outputs of one projection; the store keeps them apart
|
||||
// (Structure.SourceChars) because for the store they answer to different witnesses — a manifest can
|
||||
// name the text honestly and fail on a MONEY key, and a book that lost its bill has no business
|
||||
// losing its character count too.
|
||||
//
|
||||
// ⚠ Reusing the wire struct here put the same figure in TWO fields of one value, and a caller then
|
||||
// filled one and not the other — which is not a hypothetical: it is how
|
||||
// TestTheEnginesCharacterCountReachesTheLibraryRowBesideItsAccuracy went red. One fact, one carrier.
|
||||
type Projection struct {
|
||||
// Expected is the whole book's expected bill, book-level passes INCLUDED.
|
||||
Expected money.MicroUSD
|
||||
// BookOnce is the part of Expected that belongs to the BOOK and not to any chapter — a flat bond
|
||||
// on the shipped arm, which is why an ORDER is priced from the chapters' own bills instead (see
|
||||
// pricing.Hold and ingest.BookPrice.BookOnceUSD for the measurement behind it).
|
||||
BookOnce money.MicroUSD
|
||||
// StepMax is the largest single reservation any one call of this book can ask for: the floor of
|
||||
// every hold, because a ceiling below it admits nothing.
|
||||
StepMax money.MicroUSD
|
||||
}
|
||||
|
||||
// StructureChapter is one chapter and its pairs, in reading order.
|
||||
type StructureChapter struct {
|
||||
// EngineID is the engine's content-derived chapter identity; Number its dense display ordinal.
|
||||
|
|
@ -171,7 +195,7 @@ func (s *Store) SaveStructure(ctx context.Context, bookID string, in Structure)
|
|||
// tree would sell yesterday's book; an absent one refuses the sale, which is the honest half.
|
||||
var expected, bookOnce, stepMax *int64
|
||||
if in.Price != nil {
|
||||
e, b, sm := int64(in.Price.ExpectedUSD), int64(in.Price.BookOnceUSD), int64(in.Price.StepMaxUSD)
|
||||
e, b, sm := int64(in.Price.Expected), int64(in.Price.BookOnce), int64(in.Price.StepMax)
|
||||
expected, bookOnce, stepMax = &e, &b, &sm
|
||||
}
|
||||
// ⚠ INDEPENDENT OF THE PRICE — see Structure.SourceChars. A book the engine could not price is
|
||||
|
|
|
|||
|
|
@ -327,7 +327,11 @@ func (s *Service) refreshStructure(ctx context.Context, bookID, workdir string,
|
|||
// moved nothing. So `priced` gates the per-chapter numbers too, and a book the engine could not
|
||||
// price is stored as not priced rather than as free.
|
||||
if p, ok := manifest.Priced(); ok {
|
||||
in.Price = &p
|
||||
// ⚠ MAPPED FIELD BY FIELD RATHER THAN CARRIED WHOLE, and `source_chars` is deliberately not
|
||||
// among them: the store holds it on the CUT (in.SourceChars, set above), so that a book whose
|
||||
// money did not arrive keeps the honest count of its text. pgstore.Projection carries no such
|
||||
// field, which is what makes the omission impossible to make by accident.
|
||||
in.Price = &pgstore.Projection{Expected: p.ExpectedUSD, BookOnce: p.BookOnceUSD, StepMax: p.StepMaxUSD}
|
||||
} else if why := manifest.PriceRefusal(); why != "" {
|
||||
// ⛔ SAID OUT LOUD, because the consequence is a book that materializes perfectly and can never
|
||||
// be SOLD. Everything else about it lands — the tree, the text, the reading surface — and only
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue