Repository navigation
Phase 7c: pagination — documentation and phase record - #86
Conversation
Phase 7c, chapter 12 (PAGE-1..PAGE-36), in dexpace-core under Dexpace::Page, a class that is also the namespace. The page value owns exactly one live response behind Closeable's latch, with .build and the public resolution branch .next_request_from (a blank, unresolvable or non-dispatchable target is end-of-stream). Info carries a strategy's answer, its nil next_request the one end-of-stream signal. QueryRewriter splices the raw query byte-for-byte over phase 1's PercentEncoding and never Query. The three built-in strategies take a caller-supplied extractor and never a codec; LinkHeader is a character- level RFC 8288 state machine with no Regexp. The private Walk owns every live page (the cap, the exhaustion latch, the current and look-ahead slots) over a per-walk drive, and Closing holds the two close disciplines both views share, built without $! because it is the caller's inside a caller's rescue. Items eager-closes each page before yielding; Pages is single-use (PageStateError, a state error) with a one-slot look-ahead. Paginator and Fetchers are frozen Data engines; AsyncPaginator is a re-arm trampoline over Future#on_settle whose every callback is total, with an optional #post executor that runs the first dispatch too. URL.resolve is added beside .parse! for PAGE-19's RFC 3986 resolution. The smoke suite pins the layer and the surface manifest gains 77 rows.
Phase 7c, review round 0 (R0-1, R0-3). Page#initialize now holds next_link and continuation_token to the rule Info already applied to the same two members -- nil, or a String copied frozen, anything else a named Dexpace::InvalidArgumentError -- so Page.build(next_link: 42) is refused at construction instead of surfacing one page later as a NoMethodError in Fetchers::Drive#key_of, and the checklist's "Info and Page validate next_link and continuation_token" is true as written (P7-107). LinkHeader#next_target reads only the FIRST rel parameter of each link-value: RFC 8288 section 3.3 says rel MUST NOT appear more than once and occurrences after the first MUST be ignored, so <u>; rel="prev"; rel="next" is a prev link and no longer a next one (P7-116). The sig mirror gains the private helper's line; no public signature and no manifest row changes.
…ne is Phase 7c, review round 1 (R1-2). Page.next_request_from screened the empty and whitespace-only target before resolution (P7-5) but resolved `<#>; rel=next` and `<#frag>; rel=next` to the current page plus a fragment the wire never carries, so a server emitting either was re-fetched until the page cap, which defaults to unbounded. RFC 3986 section 4.4 names the empty reference and the fragment-only one together as the same-document forms whose dereference "should not result in a new retrieval action", so the guard now reads both off the raw target, surrounding whitespace aside, through a private same_document? (P7-117). The screen stays syntactic on purpose: `<?>`, `<//>` (which uri resolves to the base itself) and the current URL spelled out are next requests like any other, bounded by PAGE-9's cap -- a screen on the RESOLVED URL would silently end a walk against an endpoint that advances server-side state under one URL, with no knob to turn it off. The sig mirror gains the private helper's line; no public signature and no manifest row changes.
… tree Phase 7b built the serde-boundary gate with the pagination layer's two globs on its printed PENDING list, because 7c's files did not exist on its base; its own comment says the rows move to GUARDED in the change that lands the files. This is that change: 7c's tree is now rebased onto main, which holds 7b, so lib/dexpace/page/**/*.rb and its sig/ mirror move to GUARDED, each carrying "spec-forced boundary 5" as the requirement the violation names, and page.rb / page.rbs gain rows of their own for the reason sse.rb has one -- a `**` under a directory cannot match the file beside it, and 7c's own token scan in page_test.rb counts the entry file among the fifteen it keeps clean. PENDING is empty and stays as the mechanism. The gate's test asserts nothing pending, the four pagination rows and their label, "8 guarded globs clean" from the task, and that the workspace fixture fails on reader.rb's require alone; both fixture workspaces gain a clean page.rb, page/info.rb and their sig mirrors so the empty_glob fixture still reports exactly one empty glob. Proven to bite by hand: a `Dexpace::Serde` read appended to page/info.rb and a `require "json"` appended to page.rb each ran the gate red, and the guard passes again with both reverted.
Sixteen suites under test/dexpace/page and page_test.rb, one per lib file plus the two with no lib mirror: lifetime_test.rb, which drives a walk from inside a caller's rescue and proves a bare ensure or a $! read would invert PAGE-13's primary, and matrix_facts_test.rb, the plan's eight facts and the cross-check's as a standing test on every CI row. Every response is a real Dexpace::Response over FakeResponseBody or RecordingBody; no Response double is written. Phase 1's url_test.rb gains a nested ResolveTest for URL.resolve. Two top-level doubles: PageFixtures (the requests, real responses, the scripted strategy reading the page index off the executed request, the two engines and the walk) and ProbeExecutor (the queued executor whose drain runs on a fresh joined thread, and the rejecting one).
Phase 7c, review round 0 (R0-1, R0-3, R0-5). page_test.rb gains the
refusal of an Integer next_link and a Symbol continuation_token, each
named, and the XCUT-15 frozen-copy case; fetchers_test.rb drives a
fetcher whose page carries a non-String key through the walk and reads
the named InvalidArgumentError where key_of would have died with
NoMethodError; link_header_test.rb pins RFC 8288 section 3.3 -- a second
rel parameter is ignored, in either order and beside a later link-value
(P7-116). Reverting either fix runs these red on 4.0.6 and 3.2.11
(guards 46 and 47). TrampolineTest loses the unused
Array.new(3_000) { [1] } that preceded the real allocation.
Phase 7c, review round 1 (R1-1, R1-2). Walk#release clears both slots before anything can raise -- the YARD said so and nothing asserted it, so the reviewer's mutation 73 (the two assignments moved past the raise) survived every suite. walk_test.rb's two-slot failure case now asserts walk.current and walk.buffered are nil after the raising close, and pages_test.rb's surfaced-close case asserts a closed view answers more? false and yields nothing when driven (guard 48, red on 4.0.6, 3.2.11). For the same-document widening (P7-117): page_test.rb and link_strategy_test.rb pin that "#", "#top" and " #x " are end-of-stream before resolution like the empty target, and pin what is deliberately NOT screened -- "?", "//" and the current URL spelled out resolve and are followed, bounded by the cap -- so a later screen on the resolved URL turns the suite red (guard 49, the fragment half dropped, red on both rows).
The checklist at docs/work/mvp/phase7/phase7c/, written from the build: thirty-six own rows, all implemented (PAGE-35 vacuous by construction), the boundary-5 row citing 7b's Task 11, thirteen cross-reference rows, forty-five guards run red on 4.0.6 and 3.2.11, thirty departures from the plan's text, findings routed. The design's ledger gains an as-built addendum, P7-101 to P7-115. docs/sdk-documentation/pagination.md is the fifteenth as-built page, every example run on 4.0.6 and 3.2.11; architecture.md, both READMEs and docs/README.md point at it (the root README's built-phases sentence had omitted 6b, and docs/README.md's layer sentence too; both now name it). CLAUDE.md's claims move to 199 lib files, nineteen private constants without a test mirror and fifteen checklists, and gain four constraints. The roadmap gains the 7c status note; docs/knowledge/notes/pagination.md four Reference entries; 8b's plan the interface file's real path.
Phase 7c, review round 0 (R0-1, R0-2, R0-3, R0-4). The checklist's "What was built" now says which branch carries each of the two existing tests that changed -- the smoke suite on the code branch as a pin the gates read, phase 1's http/url_test.rb on the tests branch as a new nested ResolveTest -- where it had said three, all on the code branch; the roadmap's 7c status note says the same. Deviation 20's "Info and Page validate next_link and continuation_token" is true since the round-1 fix and says so; deviation 31 and the design's as-built row P7-116 record LinkHeader's first-rel-only rule (RFC 8288 section 3.3), and P7-107 gains Page.build's key check. The PAGE-3, PAGE-18 and PAGE-34 rows cite the two new guards, 46 and 47, added to "Guards run red" with their messages; every P7-101-P7-115 range reads P7-116 and the test counts read 2,923 runs / 68,964 assertions. The as-built page docs/sdk-documentation/pagination.md defines the `strategy` its engine examples share (it was undefined on the page), states the two keys' shape and the first-rel rule; every example still prints identically on 4.0.6 and 3.2.11. The probe is clean.
Phase 7c, review round 1's findings landed. The checklist's PAGE-15 row says the slots-cleared-before-raise invariant is now pinned (R1-1) and its PAGE-19 row that a fragment-only rel=next target is end-of-stream before resolution, as the empty one already was, with the screen kept syntactic on purpose (R1-2); guards 48 and 49 join the table (forty-nine in all, none surviving; the reviewer's 5, 8, 26 and 43 re-run too), the round-2 departure is item 32, and every P7-101..P7-116 range reads P7-117. The design's As-built addendum gains P7-117 -- P7-5 widened to RFC 3986 section 4.4's other same-document form, and why a screen on the resolved URL was refused. docs/sdk-documentation/pagination.md states the rule in the LinkStrategy paragraph with one more runnable line; CLAUDE.md's pagination sentences and the roadmap's 7c status note say the same, with the docs tip's run counts refreshed.
Phase 7b's three PRs merged first, so the 7c stack was rebased onto main 34f52e8 and the files both lanes changed were reconciled inside the rebased 7c commits. This commit carries the record no 7c commit could: the roadmap's dated reconciliation paragraph naming the old and new tips, the reconciled files and the re-derived counts; the sentences in CLAUDE.md and docs/sdk-documentation/quality-gates.md that described the serde-boundary gate's pagination rows as PENDING, now that the code branch's chore commit has moved them to GUARDED; a dated one-line note at 7b's checklist item 15 saying the move happened; and, in 7c's own checklist, a dated paragraph stating which of its count sentences describe its base and what the combined tree's figures are, the boundary-5 row's note that the flip is built, and one line at item 30. Nothing built on the old base is rewritten: 7c's record says what it counted and on which base.
Review record for the phase 7c stack (#84 → #85 → #86)3 independent reviews, each by a fresh agent with no memory of the previous one, each re-running every gate itself on every tip (4.0.6 every gate individually at the code tip and the full
Nothing was skipped. Round 0 → fixed in round 1
Round 1 → fixed in round 2
Round 2 (final) — approve
What the final reviewer verified by experiment, both interpreters
Tips reviewed at the final round: code Reconciliation after reviewThe three reviews above ran on the stack as built off |
Closes #28. Third PR of phase 7c's stack — the phase record and the documentation — on top of #85.
What lands
13 files on top of 7b's
main(the 7c record plus the reconcile pass's dated paragraphs).docs/work/mvp/phase7/phase7c/2026-09-10-phase7c-pagination-checklist.md— 36 own rows, all ✅ (PAGE-1–PAGE-36;PAGE-35vacuous by construction andPAGE-15's wrapping clause vacuous by a false antecedent, both stated on the row;PAGE-36's adapter half owner-elsewhere, 8a's plan Tasks 13 and 20), plus twelve cross-reference rows and one non-ID row for spec-forced boundary 5 naming 7b's Task 11; the roadmap's legend verbatim; sections: requirement rows, what was built, guards run red (45), audit groups run, deviations from the plan, findings routed, postponed work (thecancellation:widening; the PENDING → GUARDED flip).docs/sdk-documentation/pagination.md(new; every example run on 4.0.6 and 3.2.11 through a doc harness),architecture.md, the core README,README.mdanddocs/README.md— whose built-phases sentences had omitted 6b onmain; corrected while adding 7c.docs/knowledge/notes/pagination.md— four reference entries (the SSE-under-PAGE-14corpus defect is a pair of harvested entries;pagination/b2a85752carriesBODY-11; §12's serde-agnosticism intro is unharvested; R8's$!prescription is wrong inside a callee), keys cited in support, none overriding;ruby scripts/verify_knowledge_structure.rbOK.docs/work/mvp/phase8/phase8b/2026-09-11-phase8b-async-runtime-adapter.md— an eight-line dated correction at Task 9:Dexpace::Page::_Executorlives insig/dexpace/page.rbs, and the executor mode posts the first dispatch too. Routed here because the work falls inside 8b's plan (CLAUDE.md's routing rule); flagged by round 0 as R0-7 and left for the merge to confirm.CLAUDE.md— the built-phases sentence reads… 6c, 7b and 7c are built; the opening paragraph carries 7b's SSE sentence then the pagination layer; every count re-derived from the combined tree by the reconcile pass — 208 lib files besideversion.rb, 208sig/mirrors, nineteenprivate_constanttest-mirror exceptions (page/closing.rbjoins), sixteen checklists, sixteen pages, eighteen gates; 7b's and 7c's "Constraints that will bite" lines in merge order; theSEAM-27line gainsURL.resolvebeside 6b's join; thegates:serde_boundarysentences saypage/**is guarded.PAGE-14/SSE-26bullet is closed here.docs/product-spec/,docs/sdk-design-ruby/,docs/knowledge/harvested/,docs/deviations.md,docs/first-release.mdand every other phase's documents (the one 8b sentence above excepted) are untouched.Reconciliation onto 7b's
mainThis stack was reviewed on
c53638band rebased onto34f52e8(7b) by a reconcile pass that resolved six prose collisions (CLAUDE.md, README.md, docs/README.md, architecture.md, the core README, the roadmap) by keeping both lanes' content in merge order and re-deriving every count from the tree; the roadmap gains a dated "Phase 7c reconciled onto main after 7b" paragraph, 7c's checklist a dated "Reconciled 2026-09-20" note (its own numbers describe its old base and are annotated, not rewritten), 7b's checklist one dated line recording the gate flip, andquality-gates.mdnow sayspage/**is guarded. 7a (#26) and 8a (#30) are not onmainyet and rebase onto this in turn.Verification
Docs tip (rebased):
bundle exec rake(eighteen gates) green on 4.0.6;ruby .claude/skills/housekeeping/probe.rbexit 0 (all eight checks);ruby scripts/verify_knowledge_structure.rbOK; the housekeeping and knowledge tooling suites green.Known follow-ups
R0-6 (nit, deferred) — the implementer's docs commit body's "thirteen cross-reference rows" should read twelve; the checklist and this PR say twelve.