Skip to content

Latest commit

 

History

History
21 lines (18 loc) · 11.1 KB

File metadata and controls

21 lines (18 loc) · 11.1 KB

pagination — notes

Hand-written. ../harvested/pagination.md is what the documents say; this file is what the implementation found, and it wins. Each entry names the harvested entry it answers by that entry's stable key.

Superseded

  • The Enumerator/ensure asymmetry holds on the whole supported range, not only on 3.4.10, and the hard rule it forces binds phase 3 before it binds phase 7. Supersedes pagination/d626cf17, whose "On Ruby 3.4.10, an Enumerator built with Enumerator.new { |y| ... ensure ... } runs its ensure block when a consumer calls #each with a block and breaks out" was verified on 3.4.10 alone, and widens pagination/731d9f17's companion claim, which carries no version qualifier at all. Re-verified on 2026-09-08 against three installed interpreters, one enumerator per case: on 3.2.11, 3.4.10 and 4.0.6 alike, e.each { |v| break if v == 2 } runs the block's ensure; driving the same generator externally with two #next calls and then dropping the reference leaves the ensure unrun after two GC.start calls; and #rewind after an external #next does not run it either. Why the widening matters rather than being a tidier citation: docs/sdk-design-ruby/07-pagination-sse-and-serialization.md §7.1 turns the pair into a repository-wide hard rule — resource acquisition and release never live inside an Enumerator block; the engine owns the resource in its own scope with its own ensure and exposes #close — and the internal-iteration half is the half the rule leans on, because Dexpace::Page.each { }'s block form is what makes PAGE-11/PAGE-12 free. A version-scoped verification of exactly that half leaves open the possibility that the block form behaves differently on required_ruby_version's floor, which would have meant a different shape on 3.2 from the one on the developer's Ruby — the same failure mode as the Data#with finding in ../notes/data-modeling.md and design §3.5's URI::DEFAULT_PARSER pin, both of which pass where you look and fail where you do not. It does not; the shape is uniform across the range. The rule reaches phase 3 first. docs/work/mvp/phase3/2026-09-08-phase3-segmentation-design.md records it as a phase-3 cross-cutting constraint rather than a pagination one: Dexpace::IO::BufferedSource.over(body) pulls chunks from a body's #each on demand (design §3.1, §10.2), several BODY variants are Enumerator-shaped, and IO-41/IO-42/BODY-15/BODY-27 all put a real transport resource behind a close that must actually run. So the owning object with its own ensure and its own #close is a phase-3 obligation, discharged through phase 2's Dexpace::Closeable latch, and phase 7 inherits a rule already paid for rather than discovering it. review · docs/work/mvp/phase3/2026-09-08-phase3-segmentation-design.md · high · sha:manual-phase3-enumerator-range
  • The rule reaches an ordinary #each method, not only an Enumerator.new block, and no guard inside the method can catch it. Supersedes pagination/f57c50f6 ("Resource acquisition and release must never live inside an Enumerator block in the Ruby SDK, because Ruby's cleanup guarantee on break only holds for internal iteration and not for external iteration via #next"), which states the right rule over one class of object too few. Verified on 2026-09-08 against 3.2.11, 3.4.10 and 4.0.6. The fact: a plain def each; @log << :open; begin; yield "a"; yield "b"; yield "c"; ensure; @log << :close; end; end — no Enumerator.new anywhere — behaves exactly like the generator block does. Driven with obj.to_enum(:each), two #next calls and then dropping the reference, the ensure is unrun after two GC.start calls; #rewind does not run it either; obj.each { |c| break if c == "b" } does run it; and a full external drive to StopIteration does run it, which is why Dexpace::IO::BufferedSource.over(body) draining a body to exhaustion is safe and only early abandonment is not. Uniform across the range. The half that closes the obvious escape hatch: block_given? is true inside #each when the method is reached through to_enum(:each) and #next, because the enumerator supplies the yielder as the block — so a raise ArgumentError unless block_given? guard, which looks like it would forbid external iteration, does nothing at all. There is no in-method defence; the only defence is where the resource lives. Why the widening matters rather than being a tidier citation: docs/sdk-design-ruby/07-pagination-sse-and-serialization.md §7.1's rule is read by every phase as "do not write Enumerator.new { ... ensure ... }", and phase 3b's bodies do not write one — Dexpace::Body#each is an ordinary method derived from #write_to. Under the narrow reading a Dexpace::FileBody that opens its handle inside #each looks compliant and leaks a file descriptor on abandonment. What the SDK does, per docs/work/mvp/phase3/phase3b/2026-09-08-phase3b-body-lifecycle-design.md: bodies that hold a resource hold it on the object with #close — Dexpace::ResponseBody and an ownership-transferred Dexpace::StreamBody — so abandoning their enumerator leaks nothing #close would not still release; and Dexpace::FileBody, which BODY-11 obliges to open a fresh handle per write so the handle cannot live on the object, states the residue in its YARD instead of pretending to close it, on the same principle as docs/sdk-design-ruby/10-deliberate-deviations-from-the-reference-contract.md item 10 and BufferedSource.over's own documented residue. resource-management/1676974d is why a finalizer is not the answer. review · docs/work/mvp/phase3/phase3b/2026-09-08-phase3b-body-lifecycle-design.md · high · sha:manual-phase3b-each-method-abandonment

Reference

  • The SSE facade's single-use rule is filed under PAGE-14 and under no SSE ID, and the defect is a PAIR, not a singleton. sse-streaming/5f4803a0 (Rules: "An SSE Enumerable view must not be taken twice over the same source … the facade enforces this by latching a @viewed flag") and its Conclusions companion sse-streaming/b94ce49e ("The single-use guard on the SSE Enumerable view is a SHOULD requirement that the port implements outright") both carry only PAGE-14, so --prefix PAGE --section rules hands a pagination author the SSE facade's latch and --prefix SSE returns neither. Both rules are right about SSE; only the attribution is wrong, and a correction naming 5f4803a0 alone leaves half of it standing. Phase 7c's own PAGE-14 latch is Dexpace::Page::Pages' @viewed flag raising Dexpace::Page::PageStateError — the same shape as SSE-26's, on an unrelated object (the phase-7 segmentation design's Convergence points), and the same error family (the roadmap's phase-10 inbound bullet of 2026-09-13 on PAGE-14 / SSE-26; 7c's ledger row P7-103). Found by 7c's design on 2026-09-10 while running the pagination audit group, confirmed at implementation on 2026-09-20; a harvest attribution, routed here because harvested/ is never hand-edited. review · docs/work/mvp/phase7/phase7c/2026-09-10-phase7c-pagination-design.md · high · sha:manual-phase7c-sse-under-page14
  • The #each-method entry above (the note keyed pagination/b2a85752) carries BODY-11 and not "no requirement ID at all". The phase-7 segmentation design's corpus-attribution finding characterises it as an entry with no ID; measured, --req BODY-11 --origin note and --prefix BODY return it while --prefix PAGE and --prefix PAGE --origin note do not, although its text is the whole of PAGE-11/PAGE-12's lifetime rule. The consequence the charter names is exactly right — a pagination author's audit group misses it — and the characterisation is not, which matters because a reader chasing "an entry with no IDs" will not find it and may conclude the defect was fixed. Recorded beside the entry it describes so the next harvest, or a re-keying of the note, is checked against it. review · docs/work/mvp/phase7/phase7c/2026-09-10-phase7c-pagination-design.md · high · sha:manual-phase7c-b2a85752-carries-body11
  • §12's transport-agnosticism and serde-agnosticism are harvested nowhere. pagination/cb5f1b9e harvests docs/product-spec/12-pagination.md:3 as its second sentence only — "A port MUST preserve the two-view model, the page-lazy fetch discipline, deterministic response-lifecycle management, and the strategy contract" — and the same line's first sentence, "It is transport-agnostic and serde-agnostic: a single stateless strategy parses each response into the page's items plus the fully-formed request for the next page (or an end-of-stream signal)", appears in no entry: --grep 'serde-agnostic|transport-agnostic' returns exactly one hit, http-domain-model/f4bd2330, which is XCUT-18's validation layer and unrelated. The property carries no requirement ID, is the reason the phase-7 segmentation design's spec-forced boundary 5 exists, and is what phase 7c builds to: the built-in strategies take a caller-supplied #call(response) extractor and never a serializer (7c's P7-6), nothing under lib/dexpace/page/ names one, gems/dexpace-core/test/dexpace/page_test.rb scans the fifteen files for the two bare tokens, and phase 7b's tools/serde_boundary.rb (its Task 11) is the gate that carries the directory's row. A harvest-COVERAGE gap rather than an attribution one — a species neither the CLI's [cited by …]/[overridden by …] conflation nor the --prefix rules gap covers — recorded so the next harvest can be checked against it. review · docs/product-spec/12-pagination.md:3 · high · sha:manual-phase7c-serde-agnosticism-unharvested
  • R8's "read $! at the top of the ensure" is unsafe for the reason the pipeline note keyed pipeline/7ce4431d (notes/pipeline.md:8) already measured, and the pagination layer is built without it. Adds to that note, whose finding — $! is dynamically scoped to the thread and non-nil inside anything called from a CALLER's rescue — reaches the close discipline as well as the re-raise: an ensure that read $! to decide between surfacing a close failure (PAGE-15) and attaching it (PAGE-13, PAGE-32) would, inside a consumer's own rescue block, attach the walk's close failure to an unrelated in-flight error and swallow what the requirement says to surface. Measured on 3.2.11, 3.3.12, 3.4.10 and 4.0.6 on 2026-09-20 (gems/dexpace-core/test/dexpace/page/matrix_facts_test.rb, fact 2) and driven as a test (lifetime_test.rb, the two CALLER'S RESCUE cases): the frame that owns the walk records the primary in a rescue ::Exception => error arm that re-raises unchanged and hands the local to Dexpace::Page::Closing.close_walk; $! appears nowhere under lib/dexpace/page/ (7c's ledger row P7-105). The measurement under R8 — a bare ensure whose close raises REPLACES the in-flight error as the primary — stands and is what the shape exists to answer. review · docs/work/mvp/phase7/phase7c/2026-09-10-phase7c-pagination-design.md · high · sha:manual-phase7c-r8-without-dollar-bang