Repository navigation
M3: the v0.3.0 record model - #20
Merged
Merged
Conversation
Two rules run through all of it. A field the read did not look at is absent, not zero, so most of the model is pointers. A book with no page count and a book with zero pages are not the same fact, and a model that cannot tell them apart pushes the ambiguity onto every consumer downstream. Every number carries provenance. A book page and a work page publish different numbers for the same named thing, and a record that does not say which population it holds is not usable for anything quantitative. The v0.2.0 types are renamed to ScrapedBook, ScrapedReview and ScrapedUser. They are still here because every v0.2.0 command still returns them, and they go when M4 ports those commands onto the extractor. The prefix is deliberate: two types called Book in one package is how a call site ends up building the wrong one. New in the extractor: series with the position kept as written, the links block kept whole, genre URLs, the work's editions pointer and choice awards, and Extra for every cache field the model has no home for. The extractor now also records fields the page published blank, separately from fields it never mentioned, which is rows two and three of the four states of a missing field. goodread book <id> --check reads a book into the model and reconciles it. The histogram has to sum to the ratings count and the mean derived from the histogram has to match the published average. The second is the one worth having: five integers with no labels are exactly the thing that ends up reversed and nothing about the values gives it away. Measured live, both books reconcile exactly. goodread verify --sample N reads live book pages instead of the pinned captures. A capture keeps extracting cleanly forever, including on the day the page changes, which is exactly when somebody needs to hear about it.
This was referenced Aug 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #11.
What this is
The record model from
~/notes/Spec/3008/03_model.md, plus the mapper from the extractor onto it.Two rules run through all of it.
A field the read did not look at is absent, not zero.
That is why so much of
model.gois pointers.A book with no page count and a book with zero pages are not the same fact, and a model that cannot tell them apart pushes the ambiguity onto every consumer downstream.
Every number carries provenance.
A book page and a work page publish different numbers for the same named thing, edition against all editions, and a record that does not say which population it holds is not usable for anything quantitative.
Stats.Viais notomitemptyfor that reason.The rename
Book,ReviewandUserintypes.gobecomeScrapedBook,ScrapedReviewandScrapedUser.They are still here because every v0.2.0 command still returns them, and they go when M4 ports those commands onto the extractor.
The prefix is deliberate rather than cosmetic: two types called
Bookin one package is how a call site ends up building the wrong one.The four states of a missing field
Extractornow records fields the page published blank inEmpty, separately from fields it never mentioned at all.That is rows two and three of the table in section 8 of the spec, and they mean genuinely different things.
A book with a blank publisher is a book Goodreads has no publisher for.
A book with no publisher key at all, read at a depth that never fetched details, is a book nobody asked about.
TestFourStatesOfAMissingFieldhas a subtest per row, andTestEmptyListsAreRowThreeOnTheRealPageanchors the same rule to a capture: Pride and Prejudice has an empty places list where The Hunger Games has a full one.New in the extractor
series, withuserPositionkept as written. Goodreads uses2.5for novellas and1-3for omnibus editions, so the model keeps the raw string and parsesNumberonly when it is a single number.linkskept whole and untyped. Its shape moves, it is not worth a struct, and dropping it loses the ebook and audiobook availability.editionspointer andchoiceAwards.Extrafor every cache field the model has no home for, counted throughNoteUnknownsoverifyreports it. A new field appearing is Goodreads shipping something, which is a chance to model it rather than a failure.awardsWoncarries nominations as well as wins, soHasWonis derived from the designation. A nomination reported as a win is a wrong claim about somebody's book.goodread book <id> --checkReads a book into the model and reconciles it two ways.
The histogram has to sum to the ratings count, which catches a truncated or misaligned distribution.
The mean derived from the histogram has to match the published average, which catches a reversed slice: five integers with no labels are exactly the thing that ends up reversed and nothing about the values gives it away.
Measured live:
Both sums are exact and both means match to two decimals.
TestStatsCheckCatchesAReversedDistributionreverses the real distribution and asserts the check fails and says why.goodread verify --sample NCarried over from M2.
Reads live book pages instead of the pinned captures, from a deterministic spread across the id space.
Deterministic rather than random because the point is to find drift, and the same books every run means a difference is drift rather than a different book.
A capture keeps extracting cleanly forever, including on the day Goodreads changes the page, which is exactly when somebody needs to hear about it.
Gate
gofmt -l .clean,go vet ./...clean,go test ./...green for both packages.