Pagination and Large-Collection Read Correctness: A Proposed Axis for Whether a Generated List Returns Every Row Exactly Once (September 2026)
A candidate BuilderProof benchmark axis that scores whether the read path an AI app builder emits returns each record of a collection exactly once while the collection is being traversed, and whether read cost stays bounded as the table grows. Seven weighted signals, four structural postures, a traversal protocol, and the point on which three independent pagination specifications agree and generated clients routinely violate.
On this page
Every collection an AI app builder generates is small on the day it is generated. The seed data is twenty rows, the list renders in one screen, and every read returns the whole table. Pagination is therefore the rare property that cannot be observed at the moment the app is judged, because the condition that exercises it does not exist yet. It arrives later, quietly, on the day the table crosses a few thousand rows and somebody scrolls to page nine. We are not scoring anyone today. We are publishing a candidate axis, its rubric, its posture levels and a reproduction protocol, and opening all of it for comment.
Quick Answer
Pagination and large-collection read correctness is a proposed BuilderProof benchmark axis, drafted September 2, 2026, that scores whether the read path an AI app builder emits returns each record of a collection exactly once while the collection is being traversed, and whether the cost of reading it stays bounded as the table grows. It is a correctness question about the result set, not a consistency question about the URL surface, and that distinction is what separates it from the axis we proposed on August 19. The failure it targets is structural rather than accidental: offset pagination over a mutating table can skip and duplicate rows without any component being wrong, and the generated client normally has no signal that it happened. Seven signals, weighted, scored from the emitted project plus a traversal probe against a seeded table. This page is an axis proposal open for community edits, not a leaderboard.
Why we are proposing this axis
The read path is the part of a generated application that gets the least scrutiny and the most traffic. A list view is usually the first screen a user lands on and the last thing anyone thinks to test, because it works immediately and keeps working for months. It keeps working because the table is small. Almost every failure mode in this axis is dormant below a few hundred rows and unavoidable above a few thousand, which means the benchmark condition and the demo condition are different conditions.
That is not a complaint about generated code specifically. It is a property of the technique. Offset pagination is correct for a static snapshot and only for a static snapshot, and this is documented at the layer the builders sit on rather than inferred. The PostgreSQL manual is direct about the weaker half of the problem: "When using LIMIT, it is important to use an ORDER BY clause that constrains the result rows into a unique order. Otherwise you will get an unpredictable subset of the query's rows." It goes further and says that using different LIMIT and OFFSET values to select different subsets "will give inconsistent results unless you enforce a predictable result ordering with ORDER BY," and closes the door on reading that as a defect: "This is not a bug; it is an inherent consequence of the fact that SQL does not promise to deliver the results of a query in any particular order."
Two distinct problems live in that paragraph and they are worth separating, because most write-ups fuse them and the fused version is not measurable. The first is the ordering problem: without a totally ordered sort key, two rows that tie can be returned in either order on either request, so a row can appear on page two and again on page three with nothing having changed in the database. The second is the mutation problem: even with a perfectly unique sort key, an insert or delete that lands before your current offset shifts every subsequent row by one position, so the traversal skips a record or serves it twice. Postgres documents the first. The second is not a database concern at all, it is an API design concern, and it is the one the specifications below address.
What this axis measures, and what it does not
Four boundaries keep the axis from swallowing its neighbours.
- It measures the result set, not the URL surface. Whether every collection expresses paging the same way is a coherence question and belongs to the axis that already owns it. Whether a traversal returns each row exactly once is a correctness question and can be true or false independently on any single endpoint.
- It measures the read path under mutation, not the write path under contention. Two clients editing the same record is a different axis. This one has a single reader and a mutating collection, and the record under observation is never the record being written.
- It measures the untouched default. Read from the emitted project of a reference build, before anyone adds a tiebreaker to the sort or swaps a range for a keyset.
- It is not a preference for cursors. Offset paging is a legitimate and often correct choice, notably where the client genuinely needs to jump to an arbitrary page. The axis asks whether the builder's choice matches the guarantees the application needs and whether the consequences are surfaced, not whether it picked the technique we like.
What this axis explicitly does not measure: whether the list has a loading or empty state, whether the query is authorized, whether input is validated, or whether the endpoint's error body matches its neighbours. Those are separate axes we already publish or already propose.
Three specifications agree on the thing generated clients get wrong
This is the part of the research that surprised us, and it is the reason we think the axis is worth writing up rather than filing as a backlog item. We went looking for a common rule across the major pagination specifications, expecting to find disagreement about cursors versus offsets. The disagreement is real but it is not the interesting part. The interesting part is a single point on which three independent specifications, written by different people for different protocols in different decades, all agree, and which generated clients routinely violate.
All three say a page may come back shorter than the client asked for, and none of them treats a short page as the end of the collection.
Google's API Improvement Proposal 158, the resource-paging specification, states that the API "may return fewer results than the number requested" and supplies a separate and explicit terminator: a
next_page_token that is simply absent when there are no subsequent pages. It even covers the pathological case directly, saying that where a page cannot be filled because of query latency over a large dataset, "the response must be 200 OK with an empty result set." An empty page is not necessarily the last page.
PostgREST , the layer that turns a Postgres schema into an HTTP API and therefore the effective API layer under a large share of builder-generated applications, says the same thing in the HTTP idiom. Its documentation notes plainly that "the server may respond with fewer if unable to meet your request," and returns the actual extent in a
Content-Range header rather than expecting the client to infer it from the array length.
The GraphQL Cursor Connections specification reaches the same conclusion from the opposite direction. It requires a
PageInfo object containing hasPreviousPage and hasNextPage, "both of which return non-null booleans," alongside opaque startCursor and endCursor fields. The existence of a mandatory boolean whose entire job is to answer "is there more" is an admission that the page contents cannot answer it.
Three specifications, three separate mechanisms, one shared premise. Now consider what a generated client typically does. It requests a page of twenty five, receives eighteen rows, and stops, because eighteen is fewer than twenty five. Under all three specifications that inference is unsound. Offset pagination as commonly generated supplies no terminator at all, which means the short-page inference is not merely the easiest option available to the generated client, it is the only option available to it. That is why we score the presence of an end-of-collection signal as its own criterion rather than folding it into the paging technique.
A note on the reference example
Our previous axis proposal noted that the canonical Next.js route reference demonstrates a JSON success body and a plain-text failure body within one document, and we were careful to present that as a place to look rather than as a cause. A similar and sharper observation applies here, and it deserves the same care.
The Supabase JavaScript reference for
.range(from, to), which is the paging primitive in the client library that a large share of builder-generated applications use, describes the method as limiting "the query result by starting at an offset from and ending at the offset to." The same entry then states the precondition explicitly: "This respects the query order and if there is no order clause the range could behave unexpectedly." That is an accurate restatement of the Postgres warning, published at exactly the layer where a developer would benefit from reading it.
The example immediately below that sentence is supabase.from('characters').select('name').range(0, 1). There is no .order() call in it.
We want to be precise about what this is and is not. It is an API reference entry, whose job is to demonstrate one method in isolation, and adding an unrelated call to a minimal example is a real documentation trade-off rather than an oversight. We are not claiming this causes anything, and we have not measured any generated project against it. We are noting that the reference surface a code-generating model is imitating states a precondition in prose and does not satisfy it in code, and that if generated projects turn out to call .range() without an accompanying total order, this is the first place to look rather than the last. That is a testable prediction, and the protocol below is designed to check it.
The proposed rubric
Seven signals, weighted, scored from the emitted project and a traversal probe against a seeded collection. Weights are a proposal and are the part we most want argued with.
Scroll to see more
| Signal | Weight | What a failing case looks like |
|---|---|---|
| Total ordering of the page key | 22 | The list is ordered by a non-unique column such as created_at or a display name, with no unique tiebreaker appended, so tied rows may be returned in different relative orders on two requests and a row appears on two adjacent pages with no data having changed |
| Boundary stability under mutation | 20 | A full traversal of the collection, run while rows are being inserted and deleted at the front, returns at least one record twice or omits at least one record that existed for the whole traversal |
| Explicit end-of-collection signal | 15 | The only way the client can learn the traversal is finished is that a page came back shorter than requested, with no terminator field, no hasNextPage equivalent and no range header carrying the extent |
| Cursor opacity and safety | 12 | The continuation token is a parseable integer or a base64 blob that decodes to a readable offset or a raw primary key, or the token itself carries any access decision rather than the request being authorized independently |
| Deep-page cost posture | 12 | Cost per page grows with page depth because every skipped row is still computed, and no keyset predicate or index-backed boundary bounds the work for a page late in the collection |
| Page-size contract | 10 | Page size has no documented default, no enforced maximum, or an unbounded value is accepted and returns the whole table; a negative or non-numeric value is accepted silently rather than rejected |
| Total-count semantics | 9 | A total is published with no indication of whether it is exact or estimated, or an exact count is computed on every page request of a large table, or a count is emitted that cannot be reconciled with the rows actually reachable by traversal |
Three notes on the weights. Total ordering carries the most because it is the only signal on the list that produces incorrect output on a completely static table, which makes it both the cheapest to test and the least excusable. Boundary stability is close behind because it is the failure the axis exists for, but it is weighted slightly lower because it is genuinely inherent to a technique that is sometimes the right choice, and a rubric should not award a heavy penalty for a defensible trade-off that the application's requirements permit. Cursor opacity carries more than its apparent surface importance because of the security clause inside it, which is discussed next.
One signal on that table is not a performance or correctness concern at all, and it is easy to miss. AIP-158 requires that page tokens "must be opaque (but URL-safe) strings, and must not be user-parseable," with a stated rationale that is purely about interface evolution: "if users are able to deconstruct these, they will do so," which "effectively makes the implementation details of your API's pagination become part of the API surface." The specification then adds a separate and much sharper requirement, that page tokens "must not provide any form of authorization to the underlying resources, and authorization must be performed on the request as with any other regardless of the presence of a page token." A continuation token that is itself treated as proof the bearer may continue reading is an access-control defect wearing the costume of a performance optimization, and it is the kind of thing a generator can produce while every visible behaviour looks correct. We score it here rather than on the access-control axis because the token is an artifact of the paging design and would not exist without it.
The postures we can describe from documentation
We are deliberately not assigning builders to these postures in this proposal. We have read one platform reference directly for this write-up and we are not going to characterize any vendor's generated output from documents we have not read, still less rank them. What we can describe is the structural shape of the options, because the shape determines what the rubric can and cannot see.
Posture one, derived range over a client library. The generated frontend calls a range or limit primitive on a client library directly against the database layer, with no first-party HTTP route in between. Paging is offset-based because the primitive is offset-based, and the ordering precondition is the caller's responsibility. The measurable consequence is that ordering totality and boundary stability are properties of the generated call site rather than of the platform, and they can differ between two list views in the same application.
Posture two, hand-authored route with limit and offset parameters. The run emits an HTTP route that accepts a page or offset parameter and passes it through. Every signal in the rubric is a fresh decision here, and nothing forces the second list endpoint to make the same decisions as the first. This posture has the widest expected variance between two runs of the same brief, which is a testable prediction rather than a criticism.
Posture three, an opinionated list method in an SDK or framework. The paging contract is supplied by the layer rather than by the generation, and it is uniform by construction. The rubric mostly measures the layer, which means two applications built on the same layer will score nearly identically on most signals. The parts that remain attributable to the generation are the ordering key and whether the terminator the layer provides is actually consumed by the generated client.
Posture four, no paging emitted. The list view selects the collection unbounded and renders whatever comes back. This is not a zero on this axis, and a rubric that scores it as one is measuring the wrong thing. An unbounded read is trivially free of skip and duplicate defects, because there is only ever one page. It fails on deep-page cost and page-size contract, and it is best reported as a distinct outcome rather than as the bottom of a scale, in the same way that a builder with no first-party HTTP surface was not a zero on our consistency axis.
How to reproduce it
The protocol is a static read plus a traversal probe, and it is cheap enough to run against your own build today.
- Generate the reference application from the standard brief, with no instruction about paging, ordering or list size in the prompt. The brief must include at least one collection that a reasonable person would expect to grow.
- Enumerate every read of that collection from the source: the query, the ordering clause if any, the paging primitive, and the page size. Record whether each was platform-derived or authored during the run.
- Determine whether the ordering key is total. A key is total only if the database can guarantee a unique order, which in practice means a unique column or a composite ending in one. Record the answer per list view rather than per application, because they can differ.
- Seed the collection to a size that exceeds several pages by a wide margin. A few thousand rows is enough for correctness and not enough for the cost signal, so seed a second collection substantially larger if you intend to score deep-page cost.
- Traverse the collection to exhaustion with no concurrent writes. Collect the identifiers. Assert that the multiset of identifiers contains every seeded row exactly once. This is the control, and it must pass before any result from step 6 means anything.
- Repeat the traversal while a second process inserts and deletes rows that sort before the traversal's current position. Collect the identifiers again. Report the count of duplicates and the count of records that existed for the entire traversal and were never returned. Report both raw counts, not a pass or fail, because the counts are the reviewable evidence.
- Record how the client determined the traversal was complete. If the only available signal was a short page, record that as the absence of a terminator rather than as a client defect.
- Probe the page-size contract: omit the parameter, send a value far above any plausible maximum, send zero, send a negative number, send a non-numeric string. Record the status and the row count for each.
- Inspect any continuation token. Attempt to decode it. Record whether it is parseable, whether it reveals an offset or a primary key, and separately whether a token minted for one authenticated principal is accepted for another. That last check is an authorization test and must be run against two real principals, not inferred from the token's shape.
- Repeat the whole protocol on a second independent generation from the same brief and report the two results separately before averaging anything.
Step 5 is the one most likely to be skipped and it is the one that makes the rest of the protocol trustworthy. A traversal that drops rows on a static table is measuring a broken harness, not a broken builder, and running step 6 without it produces numbers that cannot be attributed.
The first-page illusion
Our previous axes each named their characteristic trap. Tests that pass without asserting are the green-check illusion. Validation that exists only in front of a human is the green-form illusion. A surface whose only consumer was written by the same run that wrote the surface is the sole-consumer illusion. The trap here is the first-page illusion.
It works like this. Every observation anyone makes of a generated list view is an observation of page one of a small collection. Page one of a small collection is correct under every paging technique, with or without a total order, with or without a terminator, with or without a bounded page size. There is no configuration of this axis that page one can distinguish. The operator, reasonably, concludes the list works, because every signal available to them says so.
The illusion breaks on two independent thresholds, and they arrive at different times. The cost threshold arrives when the table gets large, and it announces itself honestly as a slow page. The correctness threshold arrives when the collection starts mutating while people read it, and it does not announce itself at all. A record that was silently skipped during a traversal produces no error, no log line and no failed request. It produces a report that is quietly missing a row, and the natural reading of a missing row is that the data was never entered.
How this relates to our existing axes
We want to be explicit that this is not a duplicate of adjacent work, because it sits close to two axes we have already proposed.
It is not the API-design consistency axis. That axis includes a collection-semantics signal covering whether listing, filtering, paging and sorting are expressed the same way on every collection, and that signal is a variance measure across the endpoint set. It is satisfied by an application whose list endpoints all page identically and all page incorrectly, because uniform wrongness is still uniform. This axis inverts the unit of observation: it scores a single collection's traversal against the set of rows that collection contains, and an application with one list view has enough surface to be scored here while having no pairwise relationships to score there. The two can move in opposite directions, which is the test we apply before proposing anything adjacent.
It is not the concurrent-write safety axis either, which is worth saying because both involve concurrency and they share almost nothing else. That axis has two writers and one record, and it asks whether both writes survive. This axis has one reader and one collection, and it asks whether every record is seen exactly once while somebody else changes the collection around the reader. The record being skipped here is typically not the record being written; it is an innocent neighbour that moved position because something else was inserted before it. An application can have flawless optimistic concurrency control on every mutation and still lose rows out of the middle of a report.
It is also not the state-handling axis, which asks whether a list view has a designed empty state. A paginated list that has drifted past the end of its collection will render an empty state, and rendering it beautifully is that axis passing while this one fails.
What we are not claiming yet
No scores today. This page proposes the axis, the rubric, the signal weights, the postures and the protocol, and it ranks nobody. The postures above are structural descriptions rather than vendor assignments, and we have deliberately not characterized any builder's generated output on this axis, because we have not run the protocol and documentation about a client library is not evidence about what a given generation does with it.
We are also not claiming that cursor paging is the correct answer for a generated application. It has a real cost, which is that arbitrary page jumps become difficult or impossible, and a list view with numbered page buttons is a legitimate product requirement that keyset paging does not serve well. The rubric is written to score whether the guarantees match the requirement, and a scoring device that always prefers cursors would be encoding a preference rather than measuring a property.
Limitations and open questions
- The mutation rate in step 6 is an unspecified parameter and it dominates the result. A traversal against a collection mutating once a minute and one against a collection mutating a hundred times a second will produce very different duplicate counts from identical code. We do not have a principled way to set that rate, and until we do, the boundary-stability number is comparable only within a single harness configuration.
- Boundary stability may be a property of the platform rather than of the generation. Where the paging primitive is inherited, two builders sitting on the same layer will produce near-identical results that say very little about either. The honest options are to score only the authored parts, to report inherited and authored sub-scores separately, or to mark the signal not applicable, and we currently favour the second without much confidence.
- The ordering signal is harder to determine statically than it looks. Whether a sort key is unique depends on schema constraints that may not be visible in the query, and a column that happens to be unique in the seeded data is not the same as a column the database guarantees to be unique. Static extraction will get this wrong in both directions, and the traversal probe is what settles it.
- Penalising an unbounded read is unresolved. Posture four is free of the defects this axis was built to find, and any weighting that lets it score respectably is measuring something other than what we intended. We have described it as a distinct outcome rather than solving it.
- Deep-page cost is not cleanly separable from general performance. A slow page late in a collection may be the paging technique or may be an unindexed filter that would have been slow anywhere, and the rubric as written does not distinguish them.
- We have read one platform reference for this proposal and no vendor documentation. That is a deliberate scope choice for an axis proposal and it is also the largest gap in this page. The posture descriptions are structural and would need documentation-derived evidence per builder before anything resembling a score.
Comments, counter-rubrics and reproduction attempts are welcome, and the most useful thing you can send us is a duplicate or skip count from a traversal of your own generated project, with the mutation rate you used. That is the correction that changes a rubric before it ever produces a score.
References
- PostgreSQL 18 documentation, "LIMIT and OFFSET," requirement for a unique
ORDER BY, the statement that different LIMIT and OFFSET values "will give inconsistent results unless you enforce a predictable result ordering," and that rows skipped by OFFSET "still have to be computed inside the server," https://www.postgresql.org/docs/current/queries-limit.html (accessed September 2, 2026) - Google API Improvement Proposals, AIP-158, "Pagination,"
page_sizeandpage_tokenrequest fields,next_page_tokenresponse field, the rule that an API "may return fewer results than the number requested," page-token opacity, the empty-result-set requirement, and the prohibition on page tokens providing authorization, https://google.aip.dev/158 (accessed September 2, 2026) - PostgREST documentation v12, "Pagination and Count,"
RangeandContent-Rangeheaders, the note that "the server may respond with fewer if unable to meet your request," and thePrefer: count=exact|planned|estimatedheader with the warning that an exact count runs more slowly as the table grows, https://docs.postgrest.org/en/v12/references/api/pagination_count.html (accessed September 2, 2026) - GraphQL Cursor Connections Specification,
PageInfowith mandatory non-nullhasNextPageandhasPreviousPage, opaquestartCursorandendCursor, https://relay.dev/graphql/connections.htm (accessed September 2, 2026) - Supabase JavaScript client reference,
range(from, to), described as limiting the result "by starting at an offset from and ending at the offset to," with the stated precondition that "if there is no order clause the range could behave unexpectedly," and the accompanying minimal example, https://supabase.com/docs/reference/javascript/range (accessed September 2, 2026) - RFC 8288, "Web Linking," Standards Track, October 2017, the
Linkheader field and its use ofrel="next"andrel="previous"relation types to convey traversal targets, https://www.rfc-editor.org/rfc/rfc8288.html (accessed September 2, 2026)
Written by
BuilderProof Editorial TeamThe BuilderProof lab publishes reproducible, community-editable benchmarks and methodology proposals for AI app builders. Axes are scored from documentation-derived rubrics and open to public revision.
Cite this benchmark
BuilderProof Editorial Team. "Pagination and Large-Collection Read Correctness: A Proposed Axis for Whether a Generated List Returns Every Row Exactly Once (September 2026)". BuilderProof, September 2026. https://www.builderproof.org/benchmarks/pagination-large-collection-correctness-axis-proposal-september-2026.
@misc{builderproof-pagination-large-collection-correctness-axis-proposal-september-2026,
title = {{Pagination and Large-Collection Read Correctness: A Proposed Axis for Whether a Generated List Returns Every Row Exactly Once (September 2026)}},
author = {{BuilderProof editorial team}},
year = {2026},
month = {sep},
howpublished = {\url{https://www.builderproof.org/benchmarks/pagination-large-collection-correctness-axis-proposal-september-2026}},
note = {BuilderProof, builderproof.org}
}Frequently asked questions
What does the pagination and large-collection read correctness axis measure?
It measures whether the read path an AI app builder emits returns each record of a collection exactly once during a full traversal, and whether the cost of reading the collection stays bounded as the table grows. It is scored from seven weighted signals: total ordering of the page key (22), boundary stability under mutation (20), an explicit end-of-collection signal (15), cursor opacity and safety (12), deep-page cost posture (12), the page-size contract (10) and total-count semantics (9). It is a proposal drafted September 2, 2026 and open for community revision. No builder is scored on it yet.
Why is offset pagination a correctness problem and not just a performance problem?
Two separate problems live in offset paging. The first is ordering: the PostgreSQL manual states that without an ORDER BY constraining rows into a unique order you get an unpredictable subset, and that different LIMIT and OFFSET values give inconsistent results, adding that this is not a bug but an inherent consequence of SQL not promising any order. The second is mutation: even with a unique sort key, an insert or delete landing before the current offset shifts every later row by one position, so a traversal can skip a record or return it twice. The first is a database concern, the second is an API design concern, and only the second is invisible when it happens.
Why does a page that comes back shorter than requested not mean the collection has ended?
Three independent specifications agree that a short page is not a terminator. Google AIP-158 states an API may return fewer results than requested and supplies an absent next_page_token as the end signal, and it requires a 200 OK with an empty result set where a page cannot be filled. PostgREST notes the server may respond with fewer rows if unable to meet the request and returns the extent in a Content-Range header. The GraphQL Cursor Connections specification requires a PageInfo object with non-null hasNextPage and hasPreviousPage booleans. Offset pagination as commonly generated supplies no terminator at all, so the short-page inference is the only option the generated client has, and it is unsound under all three specifications.
Can a page token be a security defect?
Yes, and it is the signal most easily missed. AIP-158 requires page tokens to be opaque and not user-parseable, on the grounds that if users can deconstruct them they will, which makes pagination implementation details part of the API surface. It adds a separate and sharper requirement that page tokens must not provide any form of authorization to the underlying resources, and that authorization must be performed on the request regardless of the presence of a token. A continuation token treated as proof that its bearer may keep reading is an access-control defect that looks like a performance optimization, and every visible behaviour can still appear correct.
How is this different from the API-design consistency axis and the concurrent-write safety axis?
The API-design consistency axis includes a collection-semantics signal measuring whether every collection expresses listing, filtering, paging and sorting the same way. That is a variance measure across endpoints and it is satisfied by an application whose list endpoints all page identically and all page incorrectly. This axis scores one collection's traversal against the rows that collection contains, so the two can move in opposite directions. The concurrent-write safety axis has two writers and one record and asks whether both writes survive. This axis has one reader and one collection and asks whether every record is seen exactly once while the collection changes around the reader, and the record that gets skipped is usually not the record being written.
Related benchmarks
API-Design Consistency of Emitted Routes: A Proposed Axis for Whether an AI Builder's Endpoints Agree With Each Other (August 2026)
A candidate BuilderProof benchmark axis that scores whether the HTTP surface an AI app builder emits is internally coherent across every endpoint: one error shape, uniform status-code semantics, one addressing scheme, consistent collection semantics, and a machine-readable contract. Divergence-based scoring, four documented postures, a probe-sweep protocol, and an open call for comment.
Concurrent-Write Safety Posture: A Proposed Axis for What AI App Builders Emit When Two Writes Collide (August 2026)
A candidate BuilderProof benchmark axis that scores whether the code AI app builders emit stays correct when two writes to the same record overlap. Rubric, four posture levels, a reproduction protocol, and an open call for comment.
How We Benchmark AI App Builders: The BuilderProof Methodology v1
BuilderProof methodology v1.1: the published rubric, brief OQ-7, environment standards and weights used to score AI app builders on output quality, speed, deploy quality and agency suitability. The four June 2026 result sets were withdrawn on August 21, 2026 as placeholder data, so the lab currently publishes method, not scores.