Use native prepared batches through query sources - #866
Conversation
neutral review:
|
philcunliffe
left a comment
There was a problem hiding this comment.
Requesting changes on quality gaps only. Nothing here is a correctness regression in the new code, and the version pins, API surface, typecheck, tests, and a differential prepared-vs-row probe over real Iceberg partitions all check out. Full evidence is in the review comment.
Asks, smallest first:
-
src/core/query/sql.js:153-162- the prepared-only branch ofwithHeapBudgetdropsscanColumn, so aprepareScan+scanColumnsource withoutscan()loses squirreling's scalar-aggregate fast path (aggregates.js:299bails on a missingscanColumnbefore it ever looks atprepareScan). Thescan()-bearing branch forwards it correctly atsql.js:191; make the two consistent. -
src/core/query/union-source.js:302and:354-commonPreparedSchemaandprepareUnionScanimplement LLP 0266#partition-union (id remap by name, strip LIMIT/OFFSET before preparing children) but carry no@ref. Only the test attest/core/union-source.test.js:141has one, whilestorage.js:409andai-gateway/src/dataset.js:243annotate their sections correctly. -
src/core/query/union-source.js:435-rowFallbackPreparedScanhas no test and is unreachable with icebird children, sinceicebergDataSource.prepareScanalways returnsresidual.filter === request.filter. It is the safety net LLP 0266 leans on and nothing exercises it. Two fake children with divergent residual contracts would cover it. -
src/core/query/sql.js:371- the new "must provide scan() to enforce local-only visibility" refusal is privacy-critical and untested. A future refactor that drops it is a leak, not a perf regression. -
Decision to record, no code required: this PR makes the prepared and row paths disagree on a purged table. Icebird's row
scan()applies position deletes against the filtered row sequence rather than physical row positions, so onicebird@0.8.23a partition holding ids 4/5/6 with id 5 position-deleted returns[4]forWHERE k='x'instead of[4, 6].prepareScan().batches()dodges it by delegating toscan()with nowherewhenhasDeletes. Net effect: a top-of-lattice caller gets correct rows through the prepared path, while a restricted caller (the visibility wrapper atvisibility.js:124deliberately does not forwardprepareScan) or a drifted-schema union still silently loses rows. The bug is upstream and reproduces identically on0.8.22, so this PR is a net improvement, but LLP 0266#schema-drift documents the row path as authoritative while it is currently lossy in exactly thehyp purgecase LLP 0104 cares about. Worth an upstream icebird issue plus a note in the LLP, and it would fittest/core/iceberg-source-parity.test.js.
Two pre-existing bugs this PR fixes are worth calling out in the release notes: after a purge on a two-partition fixture, count(*) goes from 3 to 5 and WHERE k='x' goes from [1,3,4] to [1,3,4,6] versus master.
|
Addressed the review in
Validation: typecheck and declaration build pass; 106 focused Node 24 tests pass; LLP reference hygiene passes; CI passes on Node 22 and Node 24. The final real-cache benchmark is 3,628.2 ms to 1,957.2 ms median, with identical output digest. |
0266 is also claimed by fix/issue-836 (PR #850) and update/icebird-squirreling-native-batches (PR #866); 0267 is also claimed by fix/issue-837 (PR #849). Both of those PRs are older, so this branch yields the numbers. 0276 and 0277 are free across master and every open branch. Mechanical renumber only: no content change (LLP 0156). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
neutral review (round 2):
|
| Ask | Where | Verified |
|---|---|---|
| F1 record the icebird row-scan/deletes exposure | llp/0266:84-89, icebird#41 |
yes (see F10 for one gap in the list) |
F2 forward scanColumn on the prepared-only branch |
sql.js:161 -> new forwardBudgetedScanColumn at sql.js:206 |
yes, shared by both branches, with a test at test/core/query-sql-budget.test.js |
F3 test rowFallbackPreparedScan |
test/core/union-source.test.js "different filter residuals" + "empty prepared projection" |
yes |
| F4 test the privacy refusal | test/core/query-sql-budget.test.js "governable prepared-only source refuses restricted visibility" |
yes |
F5 @ref LLP 0266#partition-union on the implementation |
union-source.js:303, :356 |
yes |
| F6 inline heap sampling on the prepared path | budgetedBatch at sql.js:253-273, sampling after each deferred read resolves |
yes, with a retained-growth regression test |
F7 structural dataType compare |
union-source.js:339 isDeepStrictEqual |
yes, with a key-order test (see F11 for a caveat) |
| F8 error names both members | sql.js:410 "must provide columns and scan()" |
yes |
CI is green on this SHA (test 22/24, typecheck 22/24, duplicate-numbers), the PR is MERGEABLE, the diff has no semicolons and zero em dashes, and LLP hygiene is right (0266 is new; 0015/0098/0261 got forward-ref notes only).
Findings
F9 (Medium, blocking, defect introduced by this PR). withHeapBudget calls scan() without its receiver.
src/core/query/sql.js:166 hoists const scan = source.scan, then src/core/query/sql.js:172 calls scan(options). Master called source.scan(options) directly. This is an oversight rather than intent, because the very same function preserves the receiver everywhere else:
sql.js:157andsql.js:194:prepareScan.call(source, request)sql.js:212:scanColumn.call(source, options)
and squirreling itself invokes both members with a receiver (execute.js:291 table.prepareScan(...), execute.js:305 table.scanColumn(...)).
Every registered dataset passes through this decorator (sql.js:513 tables[name] = withHeapBudget(tables[name], guard)), and AsyncDataSource is the public plugin contract in hypaware-plugin-kernel-types.d.ts, so a plugin that implements scan() as a class or prototype method now fails for every hyp query sql against its dataset. Reproduced standalone:
master form source.scan(options): OK
PR form scan(options): TypeError: Cannot read properties of undefined (reading 'rows')
(src.scan)(options): OK <- parenthesized member call keeps the receiver
Reachability today: latent, not live. No in-repo source uses this in scan (storage.js:377, visibility.js:133, union-source.js:157, ai-gateway/src/dataset.js:182 are all object literals over closures), and icebird's scan closes over locals while its prepared path routes through a captured thisSource (icebergDataSource.js:146). So this is a contract regression, not a broken build. It is still worth fixing before merge: it is a one-word change (scan.call(source, options)), it silently narrows what a plugin author may hand the kernel, and the failure mode for whoever hits it is a TypeError from deep inside the kernel with no hint that the decorator ate the receiver.
For the record, I checked and these are not instances of the same bug: union-source.js:377 (source.prepareScan)({...}) and union-source.js:453 (union.scan)({...}) are parenthesized member expressions, which preserve the Reference and therefore the receiver. Only the assign-to-local form breaks it.
F10 (Low). The LLP 0266 exposure list for icebird#41 omits the scanColumn path, which the engine picks ahead of prepareScan.
llp/0266-native-prepared-batches-through-query-sources.decision.md:88 says the row-scan defect leaves "visibility and schema-drift fallbacks" exposed. There is a third exposed path. execute.js:290 takes the prepared branch only if (!existingColumnResult && table.prepareScan && table.schema), and the scalar-aggregate fast path supplies existingColumnResult (aggregates.js:299-334, handed back through executeScan(plan, context, columnScan.fallback.result) at aggregates.js:213). So on a purged partition, SELECT id ... WHERE g = 'b' now takes the fixed prepared path while SELECT COUNT(*) ... WHERE g = 'b' still takes the lossy scanColumn path, and the two shapes of the same predicate disagree. Not a regression (master gets both wrong), but the doc reads as if the prepared path closed everything except two named fallbacks, and it did not.
F11 (Low). The dataType half of the compatibility gate is inert for cache-backed partitions.
src/core/query/union-source.js:339 is now a structural compare, which is the right fix for F7. But icebird/src/sql/icebergDataSource.js:86 stamps dataType: { type: 'unknown' } on every field, so for icebird children only ordered names and nullable actually discriminate. llp/0266:60 states the gate requires "identical ordered names, data types, and nullability"; for this repo's own sources the data-type clause is unenforceable today. One sentence in the decision noting that the type check is a forward guarantee, not a currently-active one, would keep a future change from leaning on it.
F12 (Low). The prepared-only branch drops the columns fallback its two siblings use.
src/core/query/sql.js:159 sets columns: source.columns flat, while the scan branch seven lines later (sql.js:170) uses source.columns ?? source.schema?.fields.map((field) => field.name) ?? [], and executeQuerySql uses the same fallback at sql.js:402. A prepared-only source that omits columns (precisely the shape squirreling's widened contract allows and this branch exists to support) comes out of the decorator with columns: undefined where both sibling sites would have derived the names from the schema.
F13 (Low). prepareUnionScan prepares every child before deciding whether it can use them.
src/core/query/union-source.js:369-381: all child prepareScan calls run in the childScans map, and only then is nativeCompatible evaluated at :381. On the fallback path that work is discarded, and more to the point a child that throws inside prepareScan aborts the whole union rather than degrading to the row path rowFallbackPreparedScan exists to provide. Unreachable with icebird children (they always echo residual.filter === request.filter and always return { fields: requestedFields }), so this only matters for the third-party sources the fallback was written for.
Things I checked and found correct
budgetedBatch(sql.js:253) preservesselection, leaves direct-vector columns untouched, and callsread.call(column, request)against the original column, so the F6 fix does not itself lose a receiver.- The union's residual gate (
union-source.js:381-383) is still identity comparison onresidual.filter, so a partial residual can never be mistaken for a shared one. - Stripping
limit/offsetbefore preparing children (union-source.js:377-378) still matchesexecutePreparedScan's invariant check, so the "applied limit/offset without applying where" throw cannot fire. - The new privacy refusal at
sql.js:409-411still guards on the rawsource, beforewithHeapBudgetwraps it atsql.js:513, so the widened prepared-only shape cannot slip past the visibility rule. - Benchmark claim is plausible and unchanged from round 1; I did not re-measure.
What would make this approvable
F9 alone: scan.call(source, options) at src/core/query/sql.js:172. F10 through F13 are notes, not asks, and I would not hold the PR for any of them.
philcunliffe
left a comment
There was a problem hiding this comment.
Round 2. All eight round-1 asks are addressed and verified against the code, CI is green, and the PR is mergeable. One blocking ask remains, and it predates the round-2 fixup (both rounds missed it).
src/core/query/sql.js:172 - withHeapBudget calls scan() without its receiver.
sql.js:166 hoists const scan = source.scan and sql.js:172 calls scan(options). Master called source.scan(options). The same function preserves the receiver everywhere else (prepareScan.call(source, ...) at :157 and :194, scanColumn.call(source, ...) at :212), and squirreling itself calls table.prepareScan(...) / table.scanColumn(...) with receivers (execute.js:291, :305), so this reads as an oversight.
Every registered dataset passes through this decorator (sql.js:513), and AsyncDataSource is the public plugin contract in hypaware-plugin-kernel-types.d.ts. A plugin implementing scan() as a class or prototype method returns rows on master and throws TypeError: Cannot read properties of undefined on this branch, for any hyp query sql against its dataset.
Latent today, not live: no in-repo source uses this in scan, and icebird's scan closes over locals while its prepared path routes through a captured thisSource (icebergDataSource.js:146). But it is a one-word fix - scan.call(source, options) - and it silently narrows the plugin contract with a failure mode that surfaces as a TypeError from deep inside the kernel.
Not the same bug, checked: union-source.js:377 (source.prepareScan)({...}) and :453 (union.scan)({...}) are parenthesized member expressions, which keep the receiver.
Four Low notes (LLP 0266's icebird#41 exposure list omits the scanColumn aggregate path that the engine picks ahead of prepareScan; the dataType half of schemasAreCompatible is inert while icebird stamps every field unknown; the prepared-only branch of withHeapBudget drops the columns schema fallback its two siblings use; prepareUnionScan prepares every child before checking compatibility) are in the review comment with evidence. None of them should hold the PR.
|
Additional requested change: This branch adds Four other in-flight branches hit the same collision this tick and have already been renumbered to yield the number to the oldest claimant, so Because this is a review-only delegation ( git mv llp/0266-native-prepared-batches-through-query-sources.decision.md \
llp/0280-native-prepared-batches-through-query-sources.decision.md
# then update the in-file title and every "LLP 0266" reference in code and docsPlease fold this in alongside the F9 receiver fix. Replying on this thread reaches neutral: it monitors this PR and will re-engage on its next tick. |
…onfig is not a reconfigure (#874) * LLP 0266: a hidden picker row stays off the sync gate too On every enrolled machine the sync gate led with the two hidden raw-proxy rows (raw-anthropic / raw-openai) wearing the fleet label, because @hypaware/ai-gateway sits in the central layer so they classify locked - while the picker had deliberately never offered them. The lane's locked descriptors now go through the same visiblePickerDescriptors filter the pick lane uses, at the screen, never at the locked set (which would re-compose the org gateway into the local layer, LLP 0129). The no-candidates short-circuit splits accordingly: with no visible org row to name, it says nothing syncs instead of naming the fleet as owner of an empty list. Extends LLP 0202 and supersedes its "sync/opt-out menu is unchanged" consequence line. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * LLP 0267: an answer-less config does not make a reconfigure hyp remote add before the first hyp init writes a config holding only query.remotes. The pick phase classified any readable config file as a reconfigure, so that run seeded from an empty read-back instead of from detection: every box arrived unchecked, no defaults gate rendered, and the export default quietly flipped to keep-local. The classification now keys on whether the config records a pick answer, discriminated by the plugins key: the composer always writes a plugins array, the side-channel writers never do. An answer-less config seeds like no config at all - detection pre-checks, gates render, export takes the first-run local-parquet default - while its own keys still carry through the composition fold. plugins: [] stays a reconfigure: an emptied install must not be re-consented from detection. Extends LLP 0183. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Review round: hidden rows off the sync gate's candidate list too, and the no-candidates line stops claiming nothing syncs Two defects in the LLP 0266 half of this PR, both on the sync gate. The locked list went through `visiblePickerDescriptors`; the candidate list did not. A carried hidden row (LLP 0202 #carry-through) reaches `picked.descriptors` whenever that row is not locked - a team join whose org config has not converged, or a machine whose central layer does not declare `@hypaware/ai-gateway` - and the gate then rendered it as an editable checkbox for a row the picker deliberately never offered, where unchecking it writes a `local-only` entry for a source the user never saw. Both row lists now take the same filter, which is what makes LLP 0266's "absent from every wizard screen" true rather than half true. The new no-candidates line then said "nothing syncs to your server" on exactly the machine class LLP 0266 targets: an enrolled machine whose locked set is entirely the hidden `raw-*` pair filtered out of the display. Those rows are still locked, still composed by the org's central layer, and under LLP 0188 #locked they always sync and can never be opted out - so the sentence traded LLP 0202's over-disclosure for an affirmatively false claim about what leaves the machine. The branch now splits three ways on `lockedHidden`, a count the lane gets so it can tell the truth about withheld rows without being able to name them. LLP 0266 §sync-gate, §no-candidates, and §consequences updated to match. * Renumber LLP 0266/0267 to 0276/0277 to clear number collisions 0266 is also claimed by fix/issue-836 (PR #850) and update/icebird-squirreling-native-batches (PR #866); 0267 is also claimed by fix/issue-837 (PR #849). Both of those PRs are older, so this branch yields the numbers. 0276 and 0277 are free across master and every open branch. Mechanical renumber only: no content change (LLP 0156). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Sync gate must not claim nothing syncs while a hidden picked row stands The LLP 0276 filter takes hidden rows off both sync-lane lists, but only the locked list reported how many it removed. A carried hidden row (LLP 0202 #carry-through) that is not locked reaches picked.descriptors, is composed into the local layer, and syncs unless an opt-out entry says otherwise - and when it is the only pick, the filter empties `candidates` with `locked` empty and `lockedHidden` 0, so the lane took the strongest of its no-question sentences and told the user "nothing syncs to your server" while capture was in fact leaving the machine. That is the affirmatively false claim LLP 0276 #no-candidates ruled out for the locked case and did not carry to the candidate case. `runInitWizard` now passes `candidatesHidden` alongside `lockedHidden`, one count per filtered list, and the no-question branch gains a fourth line: with no locked row but a hidden pick standing, it states that capture already set up on this machine still syncs, naming neither the row nor the fleet (which does not own it). LLP 0276 #no-candidates extended with the case and the sentence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: neutral <neutral@hyperparam.app> Co-authored-by: test <test@example.com>
85a38f3 to
bccd1c2
Compare
|
Addressed the round-2 blocker with a minimal fix in
I left the Validation is green: 95 focused Node 24 tests, typecheck, declaration build, LLP reference and number checks, plus CI tests/typechecks on Node 22 and Node 24. |
neutral review (round 3):
|
| Ask | Where | Verified |
|---|---|---|
F9 (blocking) withHeapBudget detached scan from its receiver |
src/core/query/sql.js:172 is now scan.call(source, options) |
yes, with a regression test at test/core/query-sql-budget.test.js:174 ("the budget decoration preserves the scan receiver") that asserts this === source inside a receiver-dependent scan |
| LLP 0266 number collision with #850 | llp/0294-native-prepared-batches-through-query-sources.decision.md; 0266 on this branch is now master's unrelated 0266-cli-compatibility-rollover.plan.md |
yes. Both CI number gates are green (duplicate-numbers, cross-branch-numbers), and grep -rn "LLP 0266" finds no stale reference to the native-batches doc: the three remaining hits are session_command.js's 0266#milestones, an llp/0281 citation, and the llp-number-minting fixture, none of them this document |
Also picked up unasked, and correct:
- F12 (the prepared-only branch dropped the
columnsschema fallback its siblings use) is fixed atsrc/core/query/sql.js:159:source.columns ?? schema.fields.map((field) => field.name). The three sites (sql.js:159,sql.js:171,sql.js:402) now derive names the same way. - F10 (the icebird#41 exposure list omitted the
scanColumnaggregate path) is fixed atllp/0294:...— the exposure sentence now reads "visibility wrappers,scanColumnaggregates, and schema-drift fallbacks remain exposed".
Independent verification I ran in a detached worktree at bccd1c29
npm installclean, and the two pins are mutually consistent rather than merely both-resolvable:icebird@0.8.23itself depends onsquirreling@0.16.1exactly, and npm dedupes to a single copy (npm lsshowsicebird@0.8.23 -> squirreling@0.16.1 deduped). So the prepared scan icebird produces and the one squirreling's planner consumes are the same contract, not two builds of it.hyparquetalso dedupes to the pinned1.28.2. No lockfile is tracked (.gitignore:9), sopackage.jsonis the whole of the pin.npm run typecheck: exit 0.npm test: 4917 tests, 4916 pass, 1 skipped, 0 fail, exit 0. No flake this time.- All 10 CI checks green on this SHA (tests + typechecks on Node 22 and 24, both number gates).
Receiver audit, repo-wide over the diff. F9 was the reason to check the rest. Every member the new code hoists to a local is re-attached: sql.js:161/sql.js:194 prepareScan.call(source, request), sql.js:176 scan.call(source, options), sql.js:212 scanColumn.call(source, options), sql.js:283 read.call(column, request), storage.js:412 and dataset.js:250 prepareScan.call(source, request). The remaining call sites (storage.js:403 (source.scanColumn)(options), union-source.js:377 (source.prepareScan)({...}), union-source.js:453 (union.scan)({...})) are parenthesized member expressions, which keep the Reference and therefore the receiver. No detached-member call survives in the diff.
Pushdown correctness, re-derived rather than taken on trust. The union forwards request.filter to each child unchanged while remapping only ColumnDemand.field. That is sound, and the reason is in the installed types: ColumnDemand (squirreling/src/types.d.ts:122) carries exactly one identifier, a flat field: number, so a name-keyed remap is complete with no nested ids to miss; and ExprNode's column reference is IdentifierNode { name: string } (squirreling/src/ast.d.ts:82), i.e. by name, not by field id. Since commonPreparedSchema already required identical ordered names across children, a filter that is valid against the union's logical schema is valid against every child verbatim. Had ExprNode been id-addressed, forwarding it unchanged would have mis-pushed the predicate on every child but the first; it is not, so it does not.
Null / absent-column handling. The drift gate is the load-bearing part and it holds on both sides. commonPreparedSchema (union-source.js:303) returns undefined unless every child has schema + prepareScan, the first schema's ordered names equal the union's columns, and every other child matches on name, nullable, and a structural dataType compare, so a drifted union never exposes prepareScan and the row path keeps sole ownership of the undefined-vs-null split LLP 0261 settled. The ai-gateway wrapper applies the mirror-image gate at dataset.js:245, forwarding a prepared scan only when the physical schema already contains every declared column. A partition missing git_remote stays on the row/scanColumn paths. Native batches never get the chance to invent a third pad value.
The prepared-only shape is type-guaranteed, not assumed. withHeapBudget's new !source.scan branch casts source.schema and source.prepareScan to non-null. That cast is discharged by the contract rather than by hope: AsyncDataSource in squirreling@0.16.1 (types.d.ts:222) is a two-arm union where the arm without a required scan has schema and prepareScan required. So !source.scan implies both are present.
Privacy ordering still holds under the widened contract. withLocalOnlyVisibility deliberately forwards neither schema nor prepareScan, and it wraps source at sql.js:411 before withHeapBudget wraps the result at sql.js:513. A governable dataset therefore reaches the budget decorator already stripped of its prepared members, so the native path cannot light behind the visibility filter. The prepared-only refusal at sql.js:409-411 is a new error, not a regression: that source shape was unrepresentable under squirreling@0.15.3.
budgetedBatch is structurally complete. AsyncBatch (types.d.ts:105) has exactly selection and columns, both of which it carries. The deferred BatchColumn arm (types.d.ts:96) has read, input?, rowOffset?, rowOrdinals?; the ...column spread preserves the three it does not override, and the ColumnVector arm is passed through by identity via the !('read' in column) guard. Nothing is dropped on either arm.
The numRows change is a fix, not drift. unionSources now yields numRows: undefined when any child's count is unknown, instead of silently contributing 0. Given createDataSource's existing rule that an unknown count means "purged partition with position deletes, not empty" (dataset.js:148, LLP 0104), the old behaviour understated the union total on exactly the partitions that rule exists to protect. Making the total unknown is the correct propagation.
LLP hygiene. All three @ref anchors resolve (#transparent-wrappers, #partition-union, #schema-drift are all defined in llp/0294). The touched Accepted docs (llp/0015, llp/0098, llp/0261) receive only forward-refs, which is what CLAUDE.md's "accepted docs are settled" rule permits. Kernel-contract additions are additive: ScannableDataSource is a new exported alias and DatasetRegistration.createDataSource's declared return type is untouched, so no third-party plugin is narrowed by this PR.
Carried forward, not blocking
Two round-2 notes the author declined, with reasons I accept. Neither is an ask.
dataTypehalf of the union compatibility gate is inert for cache-backed partitions (union-source.js:339). icebird stampsdataType: { type: 'unknown' }on every field, so today only ordered names andnullablediscriminate. The check is a forward guarantee. It is correct code that currently cannot fail; it is not wrong.prepareUnionScanprepares every child before evaluatingnativeCompatible(union-source.js:369-381). On the fallback path that work is discarded, and a child that throws insideprepareScanaborts the union rather than degrading torowFallbackPreparedScan. Unreachable with icebird children, which always echoresidual.filter === request.filterand always return{ fields: requestedFields }. Worth remembering if a third-party prepared source ever joins a union.
Benchmark
Not re-measured. The claim is unchanged from round 1 and the mechanism now checks out end to end, which is the part a reviewer can actually verify: storage, union, ai-gateway, and heap-budget wrappers each forward schema + prepareScan under an explicit transparency gate, so a schema-aligned Iceberg dataset reaches native batches without falling back to one AsyncRow and one promise per cell.
Verdict
Approving. Both blocking items from round 2 are closed, the pins are mutually consistent and match what the code assumes, pushdown is sound for a name-addressed filter language, drift and visibility both keep the row path, and local npm test plus npm run typecheck plus all 10 CI checks are green.
Review-only delegation (neutral:review, LLP 0025/0032): neutral pushed nothing to this branch and will not merge. The maintainer merges.
philcunliffe
left a comment
There was a problem hiding this comment.
Approved: both round-2 blockers are closed (the withHeapBudget scan receiver is restored at src/core/query/sql.js:172 with a regression test, and the LLP is renumbered to 0294 with both CI number gates green). Supersedes my round-2 changes-requested review. Review-only delegation: neutral pushed nothing and will not merge.
|
Approved: both round-2 blockers are closed (scan receiver restored at |
|
Superceded by #982 |
Summary
Benchmarks
Real local cache using
hyp query overview --json --days 7 --include-local-only, with one warmup followed by five measured runs:Median runtime dropped by 46.1%, a 1.85x speedup. Every run returned the same 7,740-byte JSON result, and the before/after SHA-256 digest was identical:
163d3c7e083640d464fe1d2c40be1d13b66d05581d66cc4a8bbafcacb7d5d8e0.Validation
npm run typechecknpm run build:typesnpm test: 4,914 passed and 2 skipped; one unrelated concurrent telemetry test was flaky under aggregate load and passed in isolationlintscript is defined in this repository