Let an honest character count survive a price this build could not read, so a book whose money key moved still shows the size it really has
This commit is contained in:
parent
bb6f3f4841
commit
32bbadbc96
5 changed files with 123 additions and 9 deletions
|
|
@ -816,7 +816,8 @@ type Run struct {
|
|||
OrderedChapters int
|
||||
// DeliveredChapters is NIL for a run whose order does not close whole chapters: such a run has
|
||||
// delivered no CHAPTER, and reporting `0` would read as «nothing happened» rather than as «this
|
||||
// is not the unit this run is measured in». DeliveredUnits carries its progress instead.
|
||||
// is not the unit this run is measured in». Progress carries what it HAS done instead, and for
|
||||
// such a run its `done`/`total` are counted in units rather than chapters.
|
||||
DeliveredChapters *int
|
||||
// BondFunded is whether this run's hold has room for the book-wide consistency pass on top of
|
||||
// what it bought. It is the order form's promise, kept where it can still be read after the
|
||||
|
|
|
|||
|
|
@ -194,3 +194,83 @@ func TestTheEnginesCharacterCountReachesTheLibraryRowBesideItsAccuracy(t *testin
|
|||
t.Errorf("the cut's provenance did not land: %q", after.Structure)
|
||||
}
|
||||
}
|
||||
|
||||
// ⛔ THE HONEST CHARACTER COUNT SURVIVES A PRICE THIS BUILD COULD NOT READ, and it did not until an
|
||||
// audit asked. `source_chars` was written only when the whole projection passed its witness — so a
|
||||
// manifest that named the text perfectly and failed on a MONEY key cost the screen its character
|
||||
// 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.
|
||||
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
|
||||
if err := s.SaveStructure(ctx, book, in); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, _, err := s.GetBook(ctx, "u1", book)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.SourceChars == nil || *got.SourceChars != 3000 {
|
||||
t.Fatalf("the engine's rune count was discarded with the price: %v", got.SourceChars)
|
||||
}
|
||||
// …and the book is still refused for SALE, which is the other half: the two facts are independent
|
||||
// in both directions.
|
||||
order, err := s.ReadBookForOrder(ctx, "u1", book)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if order.Priced {
|
||||
t.Error("a book whose price did not arrive reads as priced")
|
||||
}
|
||||
}
|
||||
|
||||
// ⛔ WORK DELIVERED BEYOND THE ORDER'S BOUNDARY MUST NOT CANCEL WORK STILL OWED INSIDE IT. The
|
||||
// allowance handed to the engine is «ordered minus delivered», and counting the delivered book-WIDE
|
||||
// while counting the ordered only up to the boundary makes the two describe different sets: a run
|
||||
// ordered through chapter 2 whose chapters 1 and 3 came back delivered fell to its floor of one unit
|
||||
// and half-delivered what was bought. It costs no money — settlement is by fact — and it breaks «you
|
||||
// bought N», which is the promise this pack exists to make true.
|
||||
//
|
||||
// Mutation caught: `deliveredWithinOrder` counting the book instead of the order.
|
||||
func TestDeliveryBeyondTheOrderDoesNotCancelWorkOwedInsideIt(t *testing.T) {
|
||||
s, ctx := testDB(t)
|
||||
book := readingBook(t, s, ctx, "u1")
|
||||
if err := s.SaveStructure(ctx, book, priced()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The order runs through chapter ONE (two units). Chapter TWO — beyond it — comes back delivered.
|
||||
var c1 string
|
||||
if err := s.pool.QueryRow(ctx,
|
||||
`select id from chapters where book_id = $1 and number = 1`, book).Scan(&c1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := s.pool.Exec(ctx, `
|
||||
update books set ordered_at = now(), ordered_through_chapter_id = $2,
|
||||
ordered_through_chapter_number = 1 where id = $1`, book, c1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := s.pool.Exec(ctx, `
|
||||
insert into unit_resolutions (book_id, chapter, unit, wave, shipped, flagged, at)
|
||||
values ($1, 2, 0, 'edit', true, false, now())`, book); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var ordered, delivered int
|
||||
var resolved bool
|
||||
if err := s.pool.QueryRow(ctx, `
|
||||
select `+orderedUnits+`, `+orderResolved+`, `+deliveredWithinOrder+`
|
||||
from books b where b.id = $1`, book).Scan(&ordered, &resolved, &delivered); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !resolved || ordered != 2 {
|
||||
t.Fatalf("the order through chapter one resolves to %d units (resolved %v), want 2", ordered, resolved)
|
||||
}
|
||||
if delivered != 0 {
|
||||
t.Errorf("a chapter delivered BEYOND the order counted as %d units of it: the allowance would "+
|
||||
"fall to its floor and half-deliver what was bought", delivered)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,6 +81,15 @@ type Structure struct {
|
|||
// 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.
|
||||
Structure string
|
||||
// SourceChars is the engine's count of the book's ingested text in runes.
|
||||
//
|
||||
// ⛔ CARRIED APART FROM Price, AND THAT IS THE POINT. It is a property of the CUT, not of the
|
||||
// money: a manifest can name the text honestly and still fail the price witness (a renamed money
|
||||
// key, a chapter that lost its bill). Read off `Price`, such a book lost its honest character
|
||||
// count too and fell back to the intake's approximation — a number that for an EPUB counts the
|
||||
// runes of a ZIP archive — for a reason that has nothing to do with counting. Zero means the
|
||||
// engine did not say.
|
||||
SourceChars int64
|
||||
}
|
||||
|
||||
// StructureChapter is one chapter and its pairs, in reading order.
|
||||
|
|
@ -161,11 +170,17 @@ func (s *Store) SaveStructure(ctx context.Context, bookID string, in Structure)
|
|||
// Nulls are written through when the book is not priced. A stale projection kept beside a fresh
|
||||
// tree would sell yesterday's book; an absent one refuses the sale, which is the honest half.
|
||||
var expected, bookOnce, stepMax *int64
|
||||
var sourceChars *int64
|
||||
if in.Price != nil {
|
||||
e, b, sm := int64(in.Price.ExpectedUSD), int64(in.Price.BookOnceUSD), int64(in.Price.StepMaxUSD)
|
||||
sc := in.Price.SourceChars
|
||||
expected, bookOnce, stepMax, sourceChars = &e, &b, &sm, &sc
|
||||
expected, bookOnce, stepMax = &e, &b, &sm
|
||||
}
|
||||
// ⚠ INDEPENDENT OF THE PRICE — see Structure.SourceChars. A book the engine could not price is
|
||||
// still a book whose text the engine counted, and the screen's «Знаков» has no business
|
||||
// falling back to the intake's approximation because a MONEY key was renamed.
|
||||
var sourceChars *int64
|
||||
if in.SourceChars > 0 {
|
||||
sc := in.SourceChars
|
||||
sourceChars = &sc
|
||||
}
|
||||
var structureWord *string
|
||||
if in.Structure != "" {
|
||||
|
|
@ -632,9 +647,12 @@ const (
|
|||
// top for the same reason the bar is: free and carried units ride outside the grant (volume.go),
|
||||
// so a run can finish more than it was sold, and a figure above what was bought would read as an
|
||||
// overdelivery rather than as the rounding it is. Zero for a re-pass, which buys no chapters.
|
||||
// ⚠ AND WHAT SUCH A RUN HAS DELIVERED IS COUNTED IN UNITS TOO — reported to the client as
|
||||
// `delivered_units` beside a null `delivered_chapters`, because a run that bought a prefix of a
|
||||
// chapter has delivered no CHAPTER and saying «0» would read as «nothing happened».
|
||||
// ⚠ NULL — NOT ZERO — FOR A UNIT-SHAPED ORDER, and there is no second figure beside it. A run
|
||||
// that bought a prefix of a chapter has delivered no CHAPTER, and «0» would read as «nothing
|
||||
// happened» when work was done and paid for. What such a run has delivered is carried by the
|
||||
// BAR instead, whose `done`/`total` are counted in units for exactly this run (see runDone) —
|
||||
// so the two figures a client gets are a null chapter count and a unit-shaped fraction, and
|
||||
// nothing on the wire is called `delivered_units`.
|
||||
runDelivered = `(case when r.ordered_units is not null then null
|
||||
when r.ceiling_chapters = 0 then 0
|
||||
else least(greatest((case when ` + editWave + ` then ` + editChapters + ` - r.chapters_before
|
||||
|
|
|
|||
|
|
@ -316,7 +316,10 @@ func (s *Service) refreshStructure(ctx context.Context, bookID, workdir string,
|
|||
// the old rows are deleted rather than left alone and the text is gone for good.
|
||||
in := pgstore.Structure{ManifestKey: manifest.Key, TextRead: textErr == nil,
|
||||
Structure: manifest.Structure,
|
||||
Chapters: make([]pgstore.StructureChapter, 0, len(manifest.Chapters))}
|
||||
// The engine's own rune count, carried whether or not the book could be PRICED: it is a fact
|
||||
// about the text, and the price witness is about money (pgstore.Structure.SourceChars).
|
||||
SourceChars: manifestSourceChars(manifest),
|
||||
Chapters: make([]pgstore.StructureChapter, 0, len(manifest.Chapters))}
|
||||
// ⚠ ALL OR NOTHING, and the decision is Priced's rather than this loop's. A projection is written
|
||||
// only when the whole of it was read — the book figure, a price on every chapter, and the two
|
||||
// agreeing — because the half-read shape is the dangerous one: a `step_max_usd` that decoded as
|
||||
|
|
@ -380,3 +383,15 @@ func (s *Service) refreshBank(ctx context.Context, bookID, bankExport string) er
|
|||
}
|
||||
return s.Store.SaveBank(ctx, bookID, bank.Terms)
|
||||
}
|
||||
|
||||
// manifestSourceChars is the book's ingested text in runes as the engine counted it, or 0.
|
||||
//
|
||||
// It reads the figure off the price block because that is where the engine publishes it — but it
|
||||
// deliberately does NOT ask whether the projection as a whole was readable. A renamed money key must
|
||||
// not cost the screen its honest character count.
|
||||
func manifestSourceChars(m ingest.Manifest) int64 {
|
||||
if m.Price == nil {
|
||||
return 0
|
||||
}
|
||||
return m.Price.SourceChars
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ func TestAWholeBookOrderCarriesNoVolumeBound(t *testing.T) {
|
|||
// respawned after a restart would be handed its whole order a second time and the promise «you
|
||||
// bought N units» would stop being about N.
|
||||
//
|
||||
// Mutation caught: maxUnitsFor ignoring DeliveredUnits.
|
||||
// Mutation caught: maxUnitsFor ignoring SpawnOrder.Delivered.
|
||||
func TestTheVolumeAllowanceIsWhatIsLeftOfTheOrderAtEverySpawn(t *testing.T) {
|
||||
f := newFixture(t, "10", 500)
|
||||
run, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: f.bookID(t), Chapters: order(10)})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue