postjevsql.git / CLAUDE.md
CLAUDE.mdpreviewCLAUDE.mdsource1605 lines · 91.1 KB · raw
1@README.md
2
3# Design contract
4
5Each rule below names the project it comes from: **pg-jev**, **jevQL** or
6**pg_typesafe**. Rules marked **new** are this repo's own. Evidence and
7the full comparison are in the brain at
8`~/brains/personal/technology/artificial-intelligence/jev/postjevsql.md`
9and the `ecosystem/` pages next to it. When a rule changes, update that
10page too.
11
12## SQL surface
13
14- **Functions** (pg-jev / jevQL names, kept compatible):
15  - `jev(row, q [, threshold])` returns `bool`.
16  - `jev_prob` returns the Noul probability.
17  - `jev_choice` and `jev_score` / `jev_score_norm` return the top label
18    and the level.
19    `jev_score(row, q, levels text[])` built 2026-09-26
20    (`tests/score.rs`): the vendor's probability-weighted level, 0 to
21    n − 1, and `jev_score_norm` that level over n − 1. Levels are sent
22    lowest first in the caller's order, so reordering them is another
23    question and another cache key. Fewer than 2 or more than 10 levels,
24    or a NULL level, raises 22023 before anything is sent. The two share
25    one judgment and one `jev_cache` row, which stores the score,
26    confidence, per-level probabilities and legend.
27    `jev_choice(row, q, options text[])` built 2026-09-26
28    (`tests/choice.rs`): the top label. Options are sent in the caller's
29    order as label keys with no description, so reordering them is
30    another question and another cache key. A single option is returned
31    without a request or a cache row. No options, more than 255, or a
32    NULL, empty or repeated option raises 22023 before anything is sent.
33    The `jev_cache` row stores the choice, confidence and per-option
34    probabilities as ordered pairs, since a jsonb object would sort them.
35  - `jev_confidence(row, q, kind, options text[])` returns the vendor's
36    confidence in a Choice or Score, stored exactly as returned and never
37    recomputed. Built 2026-09-26 (`tests/confidence.rs`) with pg-jev's
38    signature: `kind` is `'choice'` or `'score'`, and it asks the same
39    question as `jev_choice` or `jev_score` on the same options, so they
40    share one judgment, one request and one `jev_cache` row. A single
41    option is 1.0, as `jev_choice_full` reports it.
42  - **A Noul carries no confidence, and none is derived** (settled
43    2026-09-26). `'noul'` (or any other kind) raises 22023 before
44    anything is sent. The evidence:
45    - The vendor says "(Noul answers don't carry one.)"
46      (docs.typesafe.ai/confidence, read 2026-09-26). The wire contract
47      agrees: in `typesafe-sdk` 0.7.0 and 0.7.2, `_schemas/models.py` is
48      generated from `https://api.typesafe.ai/openapi.json`, and
49      `NoulAnswer` has only `type` and `noul`, while `ChoiceAnswer` and
50      `ScoreAnswer` require `confidence` (pypi.org/project/typesafe-sdk/
51      0.7.2). The Noul page's response example has none either
52      (docs.typesafe.ai/primitives/noul). For a Noul the probability is
53      the answer and the certainty in one.
54    - jevQL derives one as `max(p, 1 − p)`, its own addition
55      (https://github.com/kylemclaren/jevql/blob/274532af852e8edfb7715ec6dca1113e589cb191/internal/typesafe/client.go#L59-L68).
56      That is a recomputation, and it disagrees with the vendor's own
57      formula (below) at n = 2, which is `2 · max(p) − 1`: at p = 0.8 the
58      vendor's form gives 0.6 and jevQL's 0.8, and jevQL's never falls
59      below 0.5, even at p = 0.5. A threshold tuned on Choice confidences
60      would mean something else on it. It is not taken.
61    - pg-jev reads the answer's `confidence` key, so its `kind = 'noul'`
62      silently returns NULL
63      (https://github.com/realZachi/pg-jev/blob/afd11fa856d7a2b831a1bfd8ee7f869ce8efcd62/sql/jev--0.2.0.sql#L527-L531).
64      postjevsql raises 22023 instead: refuse loudly, not guess.
65    - A caller who wants jevQL's figure writes `greatest(p, 1 - p)` over
66      `jev_prob` in their own query.
67  - `jev_eval` returns the Noul in full, as a composite (*Typed
68    results*), not raw JSON.
69  - Threshold precedence: argument, then the `jev.threshold` GUC, then
70    0.5.
71    Built 2026-09-26 (`tests/threshold.rs`): `jev` is two overloads,
72    `(anyelement, text)` and `(anyelement, text, float8)`, claimed by the
73    scan like `jev_prob`. True when the probability is `>=` the threshold
74    (pg-jev's comparison). `jev.threshold` is a Userset GUC in [0, 1]
75    whose default is 0.5, so `RESET` falls back to it; an argument
76    outside [0, 1] raises 22023 before anything is sent. The threshold is
77    applied to the judgment, never sent, so `jev` and `jev_prob` on the
78    same question share one request and one cache row. A NULL threshold
79    argument gives NULL, since every function is STRICT (pg-jev's
80    `DEFAULT NULL` falls through to the setting instead).
81- **Operational functions and modes** (from the newer Jev extensions:
82  prasanthj/duckdb-jev, parable-work/jev-datafusion):
83  - `jev_stats()` returns this backend's counters since it started:
84    requests that reached the server, retries, redials (never sent),
85    connections opened, cache hits and misses, input and output tokens,
86    and cost (input tokens at `jev.price_per_mtok` when answered; output
87    is not billed), and `in_flight`, the HTTP/2 streams open now. Per
88    backend, not cluster-wide. Built 2026-09-26
89    (`postjevsql-pg/src/stats.rs`). A stream is counted by a guard held
90    across the transport's send, so a timed-out, cancelled or failed
91    request leaves the count on `Drop`. `dedupe_hits` counts calls that
92    waited on an equal judgment instead of sending.
93  - Every retry, redial, connection and answer is logged at DEBUG1 with
94    its request id, through `jev-client`'s `Observer` port (review D4).
95  - `jev.on_error` = `error` (default) | `unsure`. `unsure` turns a failed
96    judgment into NULL: `jev()` is then false and the probability NULL,
97    so a network fault skips a row and never passes one. A failed
98    judgment is the remote or the path failing on this row: a 408, 429,
99    529, 5xx, context overflow, connect or interrupted exchange, timeout,
100    oversized response, or the failure of the request an equal call
101    waited on. Settings, the key (401/403), a request we built wrong, TLS,
102    a moved model, the budget and `jev.cache_only` still raise, since they
103    would fail every row alike or guard spend. Nothing is cached for a
104    NULL. Built 2026-09-26 (`Failure::is_failed_judgment`,
105    `tests/on_error.rs`); Userset.
106- **`row` is a table alias** (the whole row, pg-jev) **or a column list**
107  `jev((a, b), q)`, which sends only those columns (jevQL). Prefer the
108  column list. It sends less data and costs fewer tokens.
109- **Typed results** (pg_typesafe). `jev_eval` and the `*_full` variants
110  return composites, not bare text or jsonb:
111  - `jev_choice_result(choice text, confidence float8, probabilities jsonb, model text, input_tokens int, output_tokens int)`
112  - The same shape for Noul and Score.
113
114  Built 2026-09-26 (`tests/eval.rs`): `jev_eval(row, q)` returns
115  `jev_noul_result(probability, model, input_tokens, output_tokens)`, the
116  Noul having no confidence; `jev_score_full(row, q, levels)` returns
117  `jev_score_result(score, confidence, probabilities, legend, model,
118  input_tokens, output_tokens)`; `jev_choice_full(row, q, options)`
119  returns `jev_choice_result`. They are claimed by the scan and ask the
120  same question as their scalar function, so they share its judgment,
121  request and `jev_cache` row; a hit reads the row's `answered_model` and
122  tokens. `probabilities` is a JSON array in the order asked ([label, p]
123  pairs for a Choice). The tokens are the whole request's, as stored. A
124  single option has a NULL model and 0 tokens, since nothing was sent.
125- **A Choice sends 2–255 options; a Score sends 2–10 levels.** The API
126  reference caps Choice at "a maximum of 255 options per Choice", and
127  Score "should have at least two levels; the API accepts up to 10"
128  (docs.typesafe.ai/api, read 2026-09-22). The Rust type makes any other
129  count unconstructible. A one-option Choice is answered locally with
130  probability 1.0 and never sent, as the vendor's own hierarchical
131  cookbook does.
132- **Option names are sent to the model, and their order is part of the
133  question.** The docs say "the option names and their descriptions are
134  both sent to the model". Hume's `option-position.json` measured order
135  bias:
136  - Reversing a list moved a support classification from 0.84–0.89 to
137    0.93–0.96.
138  - The correct answer scored 0.34 when listed first and 0.83 when
139    listed last.
140
141  So:
142  - **Options keep the caller's order end to end and are never
143    shuffled.** Randomizing would make the same row give different
144    answers and break the cache key.
145  - **Options travel as an ordered list of `(label, description)`
146    pairs** (SQL `text[]` or a composite array). They never pass through
147    `jsonb`, which does not preserve key order. `serde_json` is built with
148    `preserve_order`, and the request serializer writes `criteria` in
149    that order.
150  - **The label itself is the option key.** Do not use opaque `c0…cN`
151    keys: at 200 options they cost +67% input tokens (4,244 against 2,546)
152    for the same accuracy (Hume, `option-position.json`). Siblings are
153    unique already.
154  - Offer an `other` / `none of the above` option when the list may not
155    cover every input (vendor guidance).
156- **More than 255 options** has two modes. Both are exact about what
157  they return.
158  - **Label tree** (default, when the labels have a meaningful
159    hierarchy). This is the vendor's documented method
160    (docs.typesafe.ai/cookbooks/hierarchical_classification), corrected
161    as follows:
162    - Group siblings by meaning. Never split on the bytes or bits of IDs,
163      because Jev cannot judge an arbitrary bucket. Jev also reads hex and
164      numeric encodings worse than names (jaggedness page).
165    - Keep fanout well below the cap: the vendor reports "a Choice works
166      reliably up to roughly 240 options" (classification_using_confidence
167      cookbook, jev-1.12).
168    - **Each option describes its subtree**: its direct children and a
169      sample of leaves. **The instructions name the parent path.** This is
170      the vendor's own guidance ("each option's value is the child's
171      tree… lets the model see what lives under a branch"); its
172      hierarchical cookbook does neither.
173    - **A node's score is the product of its edge probabilities, kept as
174      `Σ log max(p, 1e-9)`.** It is a real probability that can only fall
175      with depth, so a partial path bounds every leaf below it, and
176      best-first search with pruning is exact. Do **not** use the
177      cookbook's geometric mean `exp(mean(log p))`. It rises with depth
178      and favours deep paths of confident edges (five edges at 0.9 score
179      0.90 against one edge at 0.8, while the real probabilities are 0.59
180      and 0.8). A one-child edge is ×1 and needs no special case.
181    - **Beam with K = 3 by default, all frontier Choices in one request.**
182      K = 3 is the vendor's figure and has not been measured by us.
183    - **Lookahead:** each request also carries the child Choices of each
184      frontier node's top-m likely children, up to the batch token budget.
185      That scores two levels per round trip and roughly halves the round
186      trips (CPC runs to 12 levels). Questions in one request are
187      independent (Hume), and the vendor recommends speculative questions.
188    - **Close calls get an order-debiasing twin.** When a node's
189      separation is near 1×, a reversed-order copy of that Choice is
190      asked, and the log-probabilities are averaged (permutation
191      self-consistency, arXiv 2310.07712). The separation is known only
192      from the answer, so the twin cannot ride in the same request: the
193      node is queued again at its score and the twin takes a K slot in a
194      later round. It costs one copy of that node's option tokens, a
195      round trip only when nothing else is left to ask, and nothing when
196      the node is pruned first.
197    - Return the leaf, its path probability, and the **separation**
198      `exp(score_top − score_second)` (near 1× means ambiguous).
199    - The search is built 2026-09-26 (`postjevsql-core/src/label_tree.rs`),
200      sans-I/O; `jev_choice_tree` (below) drives it from SQL.
201    - Lookahead and the twin are built 2026-09-26 (`Lookahead`,
202      `TwinBelow`, on by default at m = 2, a 60,000-token round and
203      1.5×, all unmeasured and part of the release gate). Lookahead's
204      "likely" is a prior, since a child's Choice rides with its
205      parent's: the children with the most leaves, ties in the caller's
206      order. An answer held before its node is scored is applied without
207      a request once it is, so neither changes the result
208      (`exact_against_exhaustive` runs every combination).
209      K is the Choices asked per round, not a width that drops nodes: a
210      node is dropped only when its bound cannot change the top two
211      leaves (or, under τ, the deepest node at or above τ), so the result
212      and the separation are exact. A one-child node is passed through
213      at ×1 without a request.
214    - Each node's Choice is built by `Tree::choice` (2026-09-26): the
215      instructions are the question and, below the root, `Within: A > B`;
216      an internal child's description is `Contains:` its first 20 direct
217      children (`, and N more`) and `For example:` its first 5 leaves not
218      already listed, depth first; a leaf child has none. The 20 and 5
219      (`Describe`) are unmeasured and belong to the release gate. The scan
220      side is built (2026-09-26, `postjevsql-pg/src/scan/backend.rs`): a
221      judgement awaits `scan::on_backend(closure)`, the scan runs the
222      closure on the backend between waits (so a round's cache lookups
223      are SPI there) and the judgement resumes with the result. A closure
224      whose judgement was dropped is skipped.
225    - `jev_choice_tree(row, q, paths text[] [, tau])` built 2026-09-26
226      (`TreeCall` in `crates/postjevsql/src/lib.rs`; `tests/label_tree.rs`).
227      Each path is a leaf's labels joined by ` > `, the separator
228      `Within:` uses, and the result is the chosen node's path in the same
229      form, so it is unique where a bare label need not be. Under `tau` a
230      stop at the root (nothing at or above τ) is NULL. The tree is built
231      and refused (22023) before anything is sent. Each round's Choices
232      are looked up through `on_backend` and the misses sent in one
233      request with the row as the state; each node's Choice is its own
234      judgment, deduplicated and cached like a `jev_choice`. Every round
235      that sends counts as a row against `jev.max_rows`. A label
236      containing ` > ` cannot be expressed; `jsonb` input is not built.
237    - `jev_choice_tree_full(row, q, paths [, tau])` built 2026-09-26
238      (`tree_out` in `crates/postjevsql/src/lib.rs`; `tests/label_tree.rs`)
239      returns `jev_tree_result(path text, probability float8, separation
240      float8, depth int, leaf bool, fit float8)` from the core `Outcome`, sharing every
241      node's judgment with `jev_choice_tree`. Without τ it is the leaf,
242      its depth, `leaf` true and the separation (NULL when the tree has
243      one leaf). Under τ the separation is NULL, since a stop proves no
244      runner-up, and a stop at the root has a NULL path, probability 1
245      and depth 0.
246    - `Branch::from_paths` (2026-09-26) builds the tree from label paths,
247      top down, siblings in first-appearance order. A path given twice,
248      or one that is both a leaf and the parent of others, is refused:
249      without τ only leaves are returned, so such a label could never be
250      an answer.
251    - **Optional early stop** with `tau => …`: return the deepest node
252      whose path probability is ≥ τ, with its depth and a leaf/internal
253      flag. The default (τ unset) always returns a leaf. The vendor
254      measured the trade: forced leaves were right on 39/60, and reporting
255      the uncertain half one level up made 48/60 useful. Any τ is measured
256      against the pinned model version.
257  - **Chunk and shortlist** (opt-in, for label sets with no meaningful
258    hierarchy and N up to about 1,000). This is the vendor's shape in the
259    skill-suggestion and line-by-line-search cookbooks:
260    - Request 1 sends ⌈N/254⌉ chunk Choices together and keeps the top
261      few from each.
262    - Request 2 is one Choice over the finalists, with full descriptions.
263    - That is two round trips at any N. The cost is tokens: about 8.6
264      tokens per option (Hume: 20 × 200 options came to 34,590 tokens), so
265      it is roughly 10–40× the input of a tree.
266    - The search is built 2026-09-26 (`postjevsql-core/src/shortlist.rs`),
267      sans-I/O. Chunks are contiguous runs of the caller's list, as even
268      as ⌈N/254⌉ allows, sent as labels only; the top `keep` of each (3,
269      ours and unmeasured, part of the release gate) go to the finalist
270      Choice in the caller's order with their descriptions. Its
271      probability is among the finalists, not over all N. No labels, an
272      empty or repeated label, or a `keep` that leaves fewer than 2 or
273      more than 255 finalists is refused (22023). One label is returned
274      without a request.
275    - `jev_choice_shortlist(row, q, options text[] [, descriptions
276      text[]])` built 2026-09-26 (`ShortlistCall` in
277      `crates/postjevsql/src/lib.rs`; `tests/shortlist.rs`), opt-in beside
278      `jev_choice_tree` and driven the same way: each round's Choices are
279      looked up through `on_backend`, the misses sent in one request with
280      the row as the state, and each Choice is its own judgment,
281      deduplicated and cached. Every round that sends counts as a row
282      against `jev.max_rows`. Descriptions are one per option (a NULL one
283      sends none) and ride with the finalists only; a NULL option, or a
284      descriptions array of another length, is refused (22023) with the
285      core's refusals before anything is sent. It returns the label.
286    - `jev_choice_shortlist_full(row, q, options [, descriptions])` built
287      2026-09-26 (`ShortlistCall` in `crates/postjevsql/src/lib.rs`;
288      `tests/shortlist.rs`) returns `jev_shortlist_result`
289      (`jev_choice_result` plus `fit`), sharing every Choice's judgment
290      and `jev_cache` row with `jev_choice_shortlist`.
291      `choice` is the label the search chose (the vendor's may differ on a
292      tie). Confidence, `probabilities` ([label, p] pairs, the finalists
293      in the caller's order, among the finalists and not over all N),
294      model and tokens are the finalist Choice's, as stored; round 1's
295      tokens are in `jev_stats()` and EXPLAIN ANALYZE. A single option is
296      1.0 with a NULL model and 0 tokens, as `jev_choice_full` reports it.
297  - **"Does any label fit at all"** is a separate Noul in the same request
298    (the vendor's `gate` / `exists` pattern). Nouls never rank labels,
299    because Noul and Choice scores are not comparable.
300    Built 2026-09-26 (`postjevsql-core/src/gate.rs`, `Tree::gate`,
301    `Shortlist::gate`; `tests/label_tree.rs`, `tests/shortlist.rs`): the
302    `_full` functions ask it in their first round's request and return its
303    raw probability as `fit`, the last column of `jev_tree_result` and of
304    `jev_shortlist_result` (`jev_choice_result` plus `fit`). No threshold
305    is applied, since thresholds do not transfer. The scalar functions do
306    not ask it, as they could not return it. Its instructions are the
307    question and `Does any of these labels fit?` followed by the tree as
308    its root is described (`Contains:` / `For example:`), or every
309    shortlist label in the caller's order; it is its own judgment,
310    deduplicated and cached. `fit` is NULL when the search sends nothing
311    (one leaf or one option).
312  - The cookbook's "beam 4/4 against greedy 2/4" is one hand-picked
313    example per hierarchy, so it is anecdote, not accuracy evidence. The
314    literature picks no single winner: Yoshimura & Kashima (arXiv
315    2508.04219) find top-down and direct prompting each win on different
316    datasets, and LATTICE (arXiv 2510.13217) finds shortlist reranking
317    wins at low token budgets and tree search at high ones. This is why
318    both modes exist and the label-tree harness is a release gate
319    (*Testing*).
320  - This is hierarchical classification. It is not a radix tree.
321- **Thresholds do not transfer between question types or model
322  versions.** The jaggedness page shows a Noul and a yes/no Choice on the
323  same question disagreeing (0.22 against 0.01). `jev.threshold` applies
324  to Noul only, and any tuned threshold is recorded against the pinned
325  model version.
326- **Refuse loudly instead of guessing** (jevQL's discipline). jevQL's
327  refusal list is the set of cases this extension must eventually
328  support, not the set it may skip:
329  - `jev_*` in `OR`
330  - `HAVING`
331  - window functions
332  - DML (handled, with RETURNING still refused; below)
333  - CTEs and subqueries
334  - `DISTINCT`
335  - comparisons like `jev_prob(...) > 0.7`
336  - a `jev((a.x, b.y), q)` over two relations. That is a join condition,
337    which `set_rel_pathlist_hook` never sees; it needs a join or
338    upper-path hook.
339
340  Comparisons, `OR` and `AND` batch in the relation scan: it claims a
341  call anywhere in a restriction clause, and judges only where Postgres
342  reaches it (*Execution*).
343
344  Aggregates, `HAVING`, `DISTINCT` and window functions are handled
345  (2026-09-27, `postjevsql-pg/src/scan/lift.rs`; `tests/batching.rs`): a
346  `planner_hook` post-pass, after `standard_planner`, puts a jev scan
347  between an Agg, WindowAgg, Group or Result and its child wherever that
348  node evaluates a call reading only the child's columns (OUTER_VAR, no
349  Param, SubPlan or aggregate), laid out as `plan_custom_path` lays it
350  out, and rewrites the call to an OUTER_VAR column of the new scan. The
351  scan's `lefttree` is also the child, for EXPLAIN's deparser only. The
352  relation scan no longer claims calls under an Aggref or WindowFunc.
353
354  CTEs and subqueries are handled (2026-09-27, `tests/batching.rs`, on
355  17 and 18 against a per-row reference) with no code of their own: each
356  is planned by its own `PlannerInfo`, so the relation scan wraps its
357  relations there, and the post-pass and the `ExecutorStart` check walk
358  `PlannedStmt->subplans` and every plan state's `initPlan`/`subPlan`.
359  Covered: a `MATERIALIZED` CTE and one read twice (judged once), a scalar
360  InitPlan, `IN` (pulled up to a semi join), a hashed `NOT IN` SubPlan,
361  and a correlated scalar SubPlan. `EXISTS` with a jev qual stays a
362  correlated SubPlan, because `convert_EXISTS_sublink_to_join` refuses a
363  volatile WHERE. A correlated SubPlan runs once per outer row, so each
364  run's scan judges that run's rows and repeats are cache hits; it is
365  batched per run, never across outer rows. A call reading an outer column
366  (`jev_prob((t.body, o.id), q)` in a correlated subquery) gets it as a
367  Param: the scan evaluates it on every run, each run sends its own
368  value, and a repeated one is a cache hit, whether or not the child
369  depends on the Param. Under `WITH RECURSIVE`, a condition over the
370  worktable (`jev(r, q)`) and one over a table the recursive term joins
371  are judged per iteration, the recursion stopping where the condition
372  fails, and no row twice (`tests/batching.rs`, 2026-09-27).
373
374  Partitioned and inherited tables are handled (2026-09-27,
375  `wrap_paths` in `postjevsql-pg/src/scan/plan.rs`; `tests/batching.rs`,
376  on 17 and 18): the appendrel parent is not wrapped, each member is,
377  with its translated quals and the parent's select list translated by
378  `adjust_appendrel_attrs_multilevel`. An Append pulls members in turn,
379  so a member's rows are in flight together; the members share the
380  statement's dedupe and budget, and an equal judgment in two partitions
381  is one request.
382
383  DML and row locks are handled (2026-09-27, `tests/dml.rs`, on 17 and
384  18 against the mock's per-row answers; `tests/sidecar.rs` for UPDATE
385  through a foreign table): `wrap_paths`, `wrap_projections` and the
386  `lift.rs` post-pass no longer skip non-SELECT statements or rowMarks,
387  so UPDATE's SET and WHERE, DELETE's WHERE, INSERT … SELECT and
388  SELECT … FOR UPDATE are batched, and a call in both SET and WHERE is
389  one judgment. **An EvalPlanQual recheck never sends** (settled
390  2026-09-27). The scan's `scanrelid` is 0, so ExecScan's recheck asks it
391  to fill the slot: it pulls the child, whose scan substitutes the row's
392  current version, and judges it as any row. The recheck's executor has
393  its own `EState`; `Statement::new` follows `es_epq_active` to the
394  parent's, so the recheck shares the statement's dedupe registry and
395  budget, and `Statement::rechecking` gives it no client. An unchanged
396  version has the same key and is answered from the statement's
397  judgments or the cache. A version whose judged columns a concurrent
398  update changed is a question the statement never asked, so it is
399  refused with 40001 (serialization failure, retry the statement)
400  rather than sent or answered with the old version's answer. Under
401  `jev.on_error = unsure`, a failed judgment in `SET` writes NULL.
402
403  Still refused: a call over two relations (a join qual or a join's
404  select list); a call in RETURNING, ON CONFLICT or a MERGE action,
405  which ModifyTable evaluates row by row above the scan (the
406  `ExecutorStart` check reads those lists); and a whole-row argument
407  of a foreign table being updated, which setrefs matches to
408  postgres_fdw's own whole-row identity column (both varattno 0) and
409  which fails with 42804. Use a column list there.
410
411  Until one of the rest is handled, it raises a specific error. The
412  error comes before anything is sent: an `ExecutorStart` check
413  (`postjevsql-pg/src/scan/guard.rs`) refuses any plan in which a call
414  is evaluated outside a jev scan, so a scan elsewhere in the same plan
415  never judges a window first.
416- **Volatility.** `jev*` functions are `VOLATILE` (they call a remote
417  model whose answers vary between calls) and `PARALLEL RESTRICTED`, so
418  the relation gets no partial paths. Otherwise a parallel seq scan could
419  evaluate them one row at a time in the workers, because
420  `set_rel_pathlist_hook` runs before Gather paths are generated
421  (`allpaths.c:542-561`).
422
423## Execution
424
425- **Batching belongs to the plan, not to a function** (new). A CustomScan
426  node, added via `set_rel_pathlist_hook`, consumes child tuples, builds
427  requests and emits results. Do not use pg-jev's side-channel read-ahead
428  inside a scalar function, which falls back to one request per row for
429  subqueries and CTEs (pg-jev `AGENTS.md:70-80`). Do not make the caller
430  build a `text[]` (pg_typesafe's `*_many`). See *Implementation* §1 for
431  how the node is written over pgrx.
432  - **The node claims every `jev*` expression, not just the WHERE
433    quals.** `set_rel_pathlist_hook` sees only the relation's restriction
434    clauses. A `jev_prob(...) AS p` in the target list or ORDER BY would
435    otherwise run once per row in the projection, outside any batch. The
436    node reads `root->processed_tlist`, computes `jev*` there through
437    `custom_scan_tlist`, and sets `CUSTOMPATH_SUPPORT_PROJECTION`.
438    ParadeDB does the same for its score functions
439    (`pg_search/src/postgres/customscan/basescan/mod.rs:1109-1170`, AGPL,
440    design reference only).
441  - **It wraps every path in `rel->pathlist`, parameterized ones
442    included**, so index nested loops survive.
443  - **LIMIT is honoured by demand, not by a bound.** `ExecSetTupleBound`
444    never reaches a CustomScan (`execProcnode.c`), so the node sends
445    requests only as rows are pulled, keeping at most the in-flight
446    window ahead.
447  - **Settings are read in `begin`, not at plan time.** Generic plans
448    for prepared statements outlive a `SET`.
449  - **Rows the SQL filters out are never judged** by construction: the
450    child plan runs all non-`jev` quals before the node sees a row.
451  - **A call Postgres would not reach is never judged** (built
452    2026-09-27, `postjevsql-pg/src/scan/reach.rs`; `tests/batching.rs`).
453    The plan post-pass derives each call's reach from the scan's final
454    quals and projection: earlier `OR` arms `IS NOT TRUE`, earlier `AND`
455    arms `IS NOT FALSE`, the `CASE` tests before it `IS NOT TRUE` and its
456    own `IS TRUE`, earlier quals `IS TRUE`, and every qual for the
457    projection; OR over a call's occurrences. It is the last per-call
458    entry of `custom_exprs`. The scan evaluates it on each child row and
459    leaves an unreached call NULL, sending nothing. A condition that reads
460    another call's answer, a volatile function, a SubPlan or a
461    `CASE x WHEN` placeholder is dropped, which only widens the reach
462    (`jev(a) OR jev(b)` still judges both).
463- **A planner support function sets the cost** (`SupportRequestCost`),
464  so row estimates and join order account for the expense. It does
465  **not** order quals by selectivity. `order_qual_clauses` sorts by
466  security level and per-tuple cost only, and deliberately ignores
467  selectivity (`createplan.c:5416-5535`, REL_18_STABLE).
468  `SupportRequestSelectivity` applies only to a bare boolean
469  `jev(...)`; `jev_prob(...) > 0.7` is an operator expression and gets
470  the default inequality estimate (~⅓).
471- **Request layout: each row is judged alone** (new; this replaces "20
472  rows per shared state"). pg-jev's layout packs rows into one `state` as
473  `{"condition", "rows": [...]}`. Its "100% at 1–20 rows" measured
474  thresholded labels on three easy questions, not probabilities. On the
475  probabilities that layout is measurably wrong:
476  - jev-orderby-bench (commit 5239795, 2026-09-20, pre-registered, same
477    text at 40 rows per state against 1) found rank correlation with the
478    single-row baseline fell to 0.579, and 77 of 360 decisions flipped at
479    0.5. It is a position effect: rows in slots 0–7 moved 0.049, slots
480    16–23 moved 0.31, slots 24–39 about 0.42. A 20-row batch reaches into
481    the bad band.
482  - colliber/duckdb-jev issue #4 (2026-09-18) measured `score` answers
483    shifting by −0.29 levels on a 0–3 scale depending on which rows
484    shared the state, while `choice` labels agreed 269/270.
485  - The vendor's own re-ranking cookbook sends "one request per
486    candidate · no request sees another". MotherDuck's `prompt_jev()`
487    packs 32 rows per request by default; this contract rejects that on
488    the evidence above.
489
490  So a request is built one of two ways, and no row ever shares context
491  with another:
492  - **Row as state** when a row has two or more distinct questions in
493    the query. The state is the row, and each question is a branch. This
494    is the measured single-row baseline, and the state is billed once for
495    all of that row's questions.
496  - **Row in instructions** when a row has exactly one question. The
497    `state` holds only context shared by every row (often empty). Each
498    (row, question) becomes its own question whose structured
499    `instructions` carry the row. The vendor documents this form ("loop
500    over the potential records and build one of these questions per
501    field, all sent in a single call", primitives/advanced). Hume measured
502    that questions are isolated: a secret placed in a sibling question
503    scored 0.00. So many rows share a request without seeing each other.
504    Throughput is then bounded by tokens, not requests: at ~175 tokens
505    per row, 250k tokens/s is ~1,400 rows/s against 20 requests/s.
506
507  **Release gate:** row-in-instructions has no published accuracy
508  measurement against row-as-state, and Hume found facts placed in
509  *options* scored worse than in the state (0.49–0.89 against 1.00).
510  Before it is enabled, the jev-orderby-bench harness must compare
511  row-as-state against row-in-instructions on mean |Δp|, Spearman and
512  position effect (*Testing*). If it fails, the fallback is row as state
513  for every row, which is measured correct and caps throughput at the
514  account's request limit (20 rows/s).
515- **Deduplicate before sending** (new). Within a statement, identical
516  cache keys share one request: the first sends, and the rest wait on
517  its answer. The column-list form makes duplicates common. JevSQL and
518  LOTUS both deduplicate before calling.
519  Built 2026-09-26 (`crates/postjevsql/src/lib.rs`, `Shared`;
520  `tests/cache.rs`): the first call of a key registers it, and an equal
521  call waits on it without a cache lookup or a budget charge. The
522  registry is one per statement, held in the same `Statement` state as
523  the spend guards (`postjevsql-pg/src/scan/statement.rs`), so the scans
524  of a self-join share requests too. A request that fails fails its
525  waiters. One that is dropped unanswered (a rescan, a cancel)
526  unregisters the key, and each waiter then asks again itself, so
527  another scan's rescan never fails a call.
528- **Network I/O stays in the backend, non-blocking, and waits on the
529  latch** (pg_typesafe's principle; the mechanism is in *Implementation*
530  §2):
531  - Ctrl-C and `statement_timeout` must work while a request is in
532    flight.
533  - **Never call a Postgres API from a spawned thread.**
534- **Concurrency is bounded** by `jev.concurrency`, which counts HTTP/2
535  streams on the backend's one connection, not connections. It is
536  clamped to the server's `MAX_CONCURRENT_STREAMS` (100, measured;
537  *Implementation* §2).
538- **Retries follow the vendor SDKs**, checked in source: Python
539  `typesafe-sdk` 0.7.1 (`_core/retry.py`) and JS `@typesafe-ai/sdk` 0.6.0
540  (`dist/index.mjs:73-119`), fetched 2026-09-22.
541  - Retry 408, 429 and 500–599 (529 is inside that range), plus
542    connection errors and timeouts.
543  - At most 2 retries after the first attempt.
544  - Backoff starts at 0.5 s, doubles to a 5 s cap, and subtracts up to 25%
545    jitter.
546  - A server delay is read from `retry-after-ms` first, then
547    `Retry-After`, and capped at 60 s (the JS SDK's cap).
548  - Each attempt times out after 10 s (both SDKs' default). The whole
549    retry budget is at most 30 s (Python) and never beyond
550    `statement_timeout`.
551  - Retries send `X-TypeSafe-Retry-Count: n`, as both SDKs do.
552  - **Any other 4xx fails immediately.** A missing key returns **403**
553    (measured 2026-09-22); an invalid key returns **401**
554    `authentication_error` (measured 2026-09-23). Error bodies are
555    `{"detail": {"error_type", "message"}}`.
556  - **Context overflow is a 400 with `detail.error_type =
557    "max_tokens_exceeded"`**, not the 422 the vendor table implies (Hume's
558    `context-limit.json`, 20 trials; jev-axi `src/client.ts:311`). That
559    400 halves the request and retries. A single row that still overflows
560    fails with a clear error.
561  - Every error message includes the `x-typesafe-request-id` response
562    header, which is present even on auth errors.
563  - Responses are capped at 8 MB (pg_typesafe). An oversized answer
564    was already produced and billed, so it is never requested again.
565  - **Never sent is not a retry.** When hyper hands the request back
566    unsent, or h2 reports REFUSED_STREAM or a GOAWAY on it, the request
567    is resent at once on a fresh connection. There is no backoff and no
568    retry-count, at most twice.
569  - A TLS refusal (certificate, no h2) is never retried.
570- **Errors carry a SQLSTATE a caller can branch on**, with the request
571  id and attempt count in DETAIL (`crates/postjevsql/src/failure.rs`).
572  The classes follow postgres_fdw's use: 08 when the remote cannot be
573  reached, 38 when the remote failed.
574
575  | Failure | SQLSTATE |
576  |---|---|
577  | settings, invalid request | 22023 |
578  | no or two API keys, HTTP 401/403 | 28000 |
579  | DNS, connect, TLS | 08001 |
580  | interrupted, timed out | 08006 |
581  | 429/529 | 53000 |
582  | a miss under `jev.cache_only` | 55000 |
583| a changed row version in an EvalPlanQual recheck | 40001 |
584  | context overflow, response over 8 MB | 54000 |
585  | other API errors, wrong answer | 38000 |
586- **The account's rate limit is shared cluster-wide**, and the extension
587  enforces it rather than discovering it through 429s (new). jev-1.13.0
588  allows 250,000 tokens/s and 1,200 requests/min per account, and the
589  vendor warns the limits "can change without notice". No
590  `x-ratelimit-*` headers are documented, and none appear on error
591  responses.
592  - **GCRA, one `PgAtomic<AtomicU64>` per limit**, each holding a
593    theoretical arrival time. Admission is one compare-and-swap loop per
594    limit, and a request is admitted only if both limits pass. The token
595    cost is estimated, then corrected from `usage.input_tokens` after the
596    response. A token bucket would need two words and a lock; GCRA needs
597    one atomic.
598  - **The rate adapts to 429s:** a shared effective rate in shmem halves
599    on each 429 and rises gradually after successes. The GUCs
600    `jev.max_tokens_per_second` and `jev.max_requests_per_minute` are hard
601    ceilings.
602  - The wait for admission happens inside the `WaitEventSet`, so it
603    stays cancellable.
604  - Built 2026-09-26 (`postjevsql-core/src/gcra.rs`,
605    `postjevsql-pg/src/ratelimit.rs`; `tests/ratelimit.rs`). The three
606    words (two TATs and the adaptive scale) live in a PG17+ named DSM
607    segment (`GetNamedDSMSegment`), attached in `begin`, so
608    `shared_preload_libraries` is not needed; they are std `AtomicU64`s
609    timed on `CLOCK_MONOTONIC`, which every process shares. The
610    ceilings are `Sighup` (one account, one limit), default 250,000 and
611    1,200, and 0 is no limit. `Transport::admit` waits before every
612    attempt, retries included, on an executor timer, outside the attempt
613    timeout and the 30 s retry budget, so a queue is never read as a slow
614    server. The estimate is the body at the learned characters per token
615    (below; read once in `begin`), corrected from `usage.input_tokens`; a never-sent request returns its tokens. A 429
616    halves the rate and re-prices the throttled request's slot at the
617    halved rate, so its retry already waits the new interval.
618- **A request fits the model's context, and tokens are the only
619  per-request bound.** jev-1.13.0 takes 64k tokens per request, and 32k
620  for the state plus any one question. Hume's measurements:
621  - 6,893 questions in one request were accepted (65,402 tokens); 8,000
622    were refused, for tokens only.
623  - The largest accepted request was 65,756 tokens; the largest single
624    branch 33,002.
625  - A request's fixed overhead is ~267 tokens, plus ~8 per extra minimal
626    question.
627
628  The tokenizer is unpublished and matches none of 192 public ones, and
629  deriving it is barred by the terms (MCA §2.3(c)). So the planner
630  estimates with a ratio learned from cached `usage.input_tokens` per
631  model, falling back to `chars / 2.6` (jev-axi's conservative figure).
632  It validates option and level counts before sending. Overflow is
633  handled by the 400 rule above.
634  Built 2026-09-26 (`postjevsql-core/src/token_ratio.rs`,
635  `learned_ratio` in `crates/postjevsql/src/lib.rs`; `tests/budget.rs`):
636  per request in `jev_cache` for `jev.model` (rows sharing a request id,
637  the state once and each question), characters over the input tokens
638  above the 267-token overhead. Statement pricing, the spend guards and
639  EXPLAIN's `Estimated Input Tokens` use it, read once per statement,
640  and so do the rate limiter's admission estimate and the label tree's
641  lookahead budget, which take the statement's ratio in `begin`
642  (`tests/ratelimit.rs`, `tests/label_tree.rs`).
643- **The connection is reused across queries** (new), not rebuilt per
644  query. A reconnect happens only after more than 300 s idle, because
645  Cloudflare closes idle connections at 400 s (*Implementation* §2).
646  Measured here: a cold call costs 44.5 ms of our own (library load,
647  TLS setup, TCP/TLS/h2 handshake), and a warm one about 0.17 ms
648  (`tests/latency.rs`, 2026-09-23).
649
650## Cache
651
652- **Durable table, not per session** (new; pg-jev caches per session,
653  jevQL in a local SQLite file, and pg_typesafe not at all). TypeSafe
654  keeps nothing for you: "TypeSafe will be under no obligation to store
655  or retain Customer Data and may delete Customer Data at any time" (MCA
656  §10.3, "Last updated Sep 19, 2026"). Storing outputs is allowed:
657  "TypeSafe … assigns to Customer all of its right, title, and interest"
658  in Output (MCA §4.2).
659- **The key is the hash of what was actually sent**, so an incomplete
660  key cannot be written:
661
662  `sha256(pinned model ID ‖ prompt version ‖ namespace ‖ layout ‖ the exact
663  serialized bytes of the state and of this judgment's question object,
664  with the question id removed)`
665
666  - The question id is left out because it is "not sent to the
667    underlying model" (api.md). The vendor's cookbooks do the same:
668    "What goes on the wire and into the cache key: no id, no
669    bookkeeping."
670  - **Layout** (row as state or row in instructions) is in the key.
671    Answers are not assumed equal across layouts.
672  - **Namespace** is a GUC (`jev.cache_namespace`, from JevSQL). Changing
673    it on purpose re-judges every input.
674  - **Lookup uses `jev.model`**, because the answering model is unknown
675    before the call. The check that the response's `model` equals the pin
676    is what keeps that honest.
677- **`jev.model` accepts only versioned IDs.** A GUC check hook rejects
678  the aliases `jev-latest` and `jev-preview`. The vendor says an alias
679  "moves when a new release ships, so the answers behind it can change
680  without a change on your side", and advises pinning once thresholds are
681  tuned. Versioned IDs are accepted "whether or not they appear" in
682  `GET /v1/models`. If the response's `model` differs from the pin, the
683  statement fails, so a moved model is never cached under the wrong
684  version. No deprecation policy is published, and MCA §2.5 promises only
685  "commercially reasonable efforts" of notice. So **`jev.cache_only`**
686  serves hits and fails on misses; the cache stays usable after a pinned
687  version is withdrawn.
688- **Serialization is defined, not "sorted keys":**
689  - **Rows** are canonicalized with RFC 8785 (JCS): sorted keys, no
690    whitespace, RFC 3339 UTC timestamps, NULL columns written as `null`.
691    `numeric` values beyond f64 precision are written as JSON strings, so
692    no digits are lost. Built 2026-09-23:
693    - `jev-protocol/src/jcs.rs` keeps each number's lexeme. It emits the
694      ECMAScript form only when that form has exactly the original
695      value. Otherwise it emits a string of the exact value in the same
696      ECMAScript layout, so equal values give equal bytes whatever their
697      scale (`…890.10000` and `…890.1`). Generic canonicalizers round
698      through f64: serde_json_canonicalizer sent 9007199254740993 as …992.
699    - **Dates and times are written by the scan, not `to_json`**
700      (`postjevsql-pg/src/row.rs`, `RowJson`; `tests/canonical.rs`).
701      `to_json` writes BC and five-digit years outside RFC 3339
702      (`"0044-03-15T12:00:00+00:00 BC"`, `"10000-01-01T00:00:00"`) and
703      keeps a `timetz`'s own offset (`"12:00:00-05"`), measured on PG 18,
704      2026-09-26. So `timestamptz`, `timestamp`, `date` and `timetz` use
705      ECMAScript's `toISOString` years: astronomical (1 BC is `0000`),
706      four digits for 0000–9999, otherwise a sign and at least six
707      (`-000043`, `+010000`, `+5874897`). `timestamptz` ends `+00:00`
708      and `timetz` is shifted to UTC; the infinities stay
709      `"infinity"`/`"-infinity"`. `RowJson` walks records, arrays and
710      domains itself so nested values get the same form, and writes
711      every other leaf with `to_json`.
712    - The scan evaluates those leaves under a GUC nest level with
713      TimeZone=UTC, IntervalStyle=iso_8601, extra_float_digits=1,
714      bytea_output=hex and lc_monetary=C (`postjevsql-pg/src/row.rs`).
715      This is the mechanism of a function's SET clause, undone on return
716      and on abort.
717  - **Choice options** keep the caller's order (*SQL surface*) and are
718    never passed through JCS.
719  - The request body and the cache key both come from this one
720    serializer.
721- **A cached answer is one recorded draw, not a fixed point.** Repeats
722  spread by up to 0.06 on identical input (Hume, 30 calls × 24 questions)
723  and by up to 0.10 when only irrelevant state bytes change (the vendor's
724  Noul self-consistency cookbook: `covered` 0.43–0.53). The vendor says
725  plainly: "The model is no more deterministic for it." So a threshold's
726  unsure band is **±0.10** until it is measured on this estate's own
727  questions. The cache is still right to have, because it makes reruns
728  reproducible, but it does not rest on a determinism argument.
729- **`confidence` is stored exactly as the vendor returned it**, never
730  recomputed, and only a Choice or Score has one (*SQL surface*,
731  `jev_confidence`). The docs call it "a statistic computed from the
732  probability distribution" without giving a formula in the prose
733  (docs.typesafe.ai/confidence, read 2026-09-26). The page's interactive
734  widget, visible only in the raw page source, computes
735  `(n · max(p) − 1) / (n − 1)` clamped to [0, 1], where n is the number of
736  options or levels. The API reference's Choice example (0.88/0.12/0)
737  reports 0.81 where that formula gives 0.82 (docs.typesafe.ai/api), so
738  the formula is a close description, not a contract, and recomputing it
739  would not reproduce the vendor's number. The full distribution is
740  stored too.
741- **Built 2026-09-26** (`crates/postjevsql/src/lib.rs`, `tests/cache.rs`):
742  the `jev_cache` table, one row per (state, question) judgment. The scan
743  looks each call up (SPI) before building requests, sends only the misses,
744  and stores answers in `Judge::settle`, which runs on the backend between
745  waits and at scan end, since a judgement future may not call Postgres.
746  `jev_stats()` counts hits and misses per judgment, and EXPLAIN ANALYZE
747  per scan (*Cost and safety*). `jev.cache_only` (Userset) serves hits
748  and fails a miss with 55000 before anything is sent; it needs no API
749  key and is exempt from the spend guards, since it spends nothing
750  (`tests/cache.rs`, 2026-09-26). The
751  table is `REVOKE`d from PUBLIC, since a writer could forge answers; the
752  scan reaches it as its owner (`postjevsql-pg/src/owner.rs`), as a
753  SECURITY DEFINER function would.
754- **Each cache row is an audit receipt:**
755  - the key and the request bytes
756  - the requested and the answered model
757  - the `x-typesafe-request-id` (the only handle for a billing dispute)
758  - `usage`
759  - the full answer
760  - the time and the namespace
761## Cost and safety
762
763- **EXPLAIN shows the price before it is paid** (jevQL's `--explain`,
764  moved into `ExplainCustomScan`):
765  - candidate rows after SQL filters
766  - cache hits (cost $0)
767  - requests
768  - estimated input tokens (*Execution*'s estimator)
769  - dollars
770
771  **Cache hits are shown only under ANALYZE** (decided 2026-09-26).
772  Plain EXPLAIN runs no rows, and the hits depend on the rows' canonical
773  JSON, so counting them would mean running the child scan and hashing
774  every row: a full read of the data from a command that promises to
775  read none. Plain EXPLAIN therefore prices every candidate as a miss,
776  which is the bound `jev.max_cost` refuses on, and labels it
777  `Worst-Case Cost`. Built 2026-09-26 (`Judge::explain`,
778  `postjevsql-pg/src/scan/exec.rs` `explain`; `tests/cache.rs`):
779  - without `COSTS OFF`: `Candidate Rows`, `Estimated Requests`,
780    `Estimated Input Tokens` (one attempt) and `Worst-Case Cost` (three
781    attempts, `Budget::price`), from the child's row estimate;
782    that estimate is taken before the jev conditions (2026-09-27,
783    `rows_before` in `postjevsql-pg/src/scan/plan.rs`), since the child
784    no longer runs them and every row it emits is judged: a table's is
785    its tuples times the selectivity of the other clauses, as the planner
786    computes it, and any other child's is divided by the jev conditions'
787    selectivity (`tests/batching.rs`, `tests/budget.rs`);
788  - under ANALYZE, per scan: `Cache Hits`, `Cache Misses`, `Shared
789    Judgments` (deduplicated), `Requests` sent, `Input Tokens` reported
790    by the answers, and `Cost` at `jev.price_per_mtok`, counted as
791    `jev_stats()` counts them.
792
793  The price is a per-model GUC (`jev.price_per_mtok`, default 0.042 for
794  jev-1.13.0), not a constant: MCA §8.2 says rates "may vary based on …
795  the model". There is no batch endpoint, async discount, cached-input
796  discount or idempotency key (searched docs and both SDKs, 2026-09-22).
797- **Spend guards** (jevQL): `jev.max_rows` and `jev.max_cost`.
798  - Both are checked **before the first request**, against the
799    estimate.
800  - `jev.max_cost` budgets **3 attempts per request**. With no
801    idempotency key, a retry after a timeout can be billed twice.
802  - Actual `usage` is checked while the statement runs, and the
803    statement aborts once spent plus in flight would exceed the budget.
804  - Built 2026-09-26: both are `Userset`, with -1 meaning no limit (as
805    `temp_file_limit`), so 0 means "send nothing". `ExecutorStart`
806    (`postjevsql-pg/src/scan/guard.rs`) sums the jev scans' child row
807    estimates and refuses with 54000 before the first request. While
808    running, one budget per statement (`postjevsql-pg/src/scan/
809    statement.rs`, keyed by the `EState` and dropped when its query
810    context resets) is shared by every jev scan of the plan. Before a
811    row is sent, its worst case (3 attempts, at the learned ratio) is
812    held in flight, and the row that would take the rows sent, or the
813    dollars spent plus in flight, over a guard is refused. When the
814    answers arrive, the worst case is replaced by their
815    `usage.input_tokens` times each one's attempts, since a timed-out
816    attempt may have been billed. A row dropped unanswered (an error, a
817    cancel, a LIMIT) keeps its worst case as spent. Built 2026-09-26
818    (`tests/budget.rs`).
819- **Terms that bind the design** (MCA, Sep 19 2026):
820  - §2.3(b): output may not be used to "train a model to imitate the
821    output of the Services". **Cached distributions must never train a
822    stand-in for Jev.** Using them as features downstream is allowed; the
823    vendor suggests it.
824  - §2.3(a): no offering the Services "as a standalone service". One API
825    key must never serve third parties through this extension.
826  - §2.3(c): no deriving the model's "algorithms, structure". So no
827    tokenizer fingerprinting.
828  - §16.7: terms change on 60 days' notice.
829- **Privileges** (pg_typesafe): `REVOKE ALL … FROM PUBLIC` on every
830  function at install. Spending quota needs an explicit `GRANT`.
831- **The API key never appears in SQL or logs** (pg_typesafe):
832  - Read it from `TYPESAFE_API_KEY` in the server environment, or from
833    the superuser-only `jev.api_key_file`. **Exactly one source:** both
834    set is an error rather than a precedence rule, which would silently
835    bill the wrong account. Errors name the sources, never the key.
836  - A session `SET` is superuser-only and documented as appearing in
837    logs.
838- The endpoint must be `https://`. `http://` is allowed only to localhost
839  for tests.
840- **`jev.mock_response` is superuser-only** (pg_typesafe), so a granted
841  role cannot forge answers.
842
843## Implementation: Rust's four weak spots and how they are closed
844
845Checked 2026-09-22. Evidence is in the brain page.
846
847### 0. Workspace: unsafe lives in one edge crate
848
849`jev-protocol`, `jev-client` and `jev-mock` moved to ~/jevcrates on
8502026-10-01, shared with jevsnes and jevhooks, and come in as the git
851submodule `third-party/jevcrates` (pinned by its gitlink, buckified by
852reindeer as path dependencies). Change them there, then move the pin.
853
854- **`jev-protocol`**: the System One protocol, with no I/O, no
855  runtime and no Postgres, so any Jev client can use it. It holds:
856  - question types (Choice 2–255 unique labels in caller order, Score
857    2–10 levels, enforced by construction)
858  - `ModelId`, which accepts only versioned ids
859  - `Json`, with RFC 8785 canonical form for row state
860  - the exact request bytes
861  - response verification against the questions asked, read through
862    typed keys
863  - structured API errors, including the `max_tokens_exceeded` kind
864  - the retry policy
865
866  It is our own because no published crate fits (surveyed 2026-09-23).
867  kunobi-jev requires reqwest and tokio. typesafe-client's types-only
868  mode sends state re-serialized through `serde_json::Value`, so its
869  bytes would differ from the canonical bytes a cache key hashes. Its
870  design points are credited in `lib.rs`.
871- **`jev-client`**: the policy, with no runtime or HTTP stack of
872  its own. It owns:
873  - retries and their budget, and per-attempt timeouts
874  - "never sent" redials, which are not retries
875  - request ids on every error, and error classification
876
877  It reaches the world through two ports:
878  - `Transport`: send one request, and classify the failure as
879    NotSent, Connect, Interrupted, Tls, TooLarge or Config
880  - `Runtime`: time, sleep, timeout and jitter
881
882  `postjevsql-pg` implements both. The tests implement them with a
883  script and a virtual clock, so every policy case is exact and instant
884  (review D1–D3, 2026-09-23).
885- **`crates/postjevsql-core`** (added with the cache): no pgrx and no
886  I/O. It holds:
887  - the batch planner
888  - the cache key
889  - the rate limiter's GCRA
890  - the label-tree search
891  - chunk-and-shortlist
892
893  The batch planner (`src/batch.rs`, built 2026-09-26) chooses each
894  row's layout from its distinct questions and builds its state, and the
895  cache key hashes the `Placement` it returns, so a key's layout is the
896  one sent. Row in instructions is unreachable until the layout-parity
897  gate passes: its evidence type, `LayoutParity`, has no values, so every
898  row is row as state. A row is placed only from a `Planned` the planner
899  returned, never from a bare `Layout`, and row in instructions carries
900  the evidence, so no caller can ask for it. `crates/postjevsql/src/lib.rs` calls it for every
901  request. It has `#![forbid(unsafe_code)]` and is tested with plain `rust_test`.
902- **`jev-mock`**: a mock System One endpoint for tests.
903- **`crates/postjevsql-pg`**: the **only** crate that may contain
904  `unsafe`. It holds:
905  - the CustomScan wrapper (§1)
906  - the WaitEventSet executor (§2)
907  - the memory-context reset glue
908  - **every hook registration**: `set_rel_pathlist_hook`, and the GUC
909    check hooks, which pgrx 0.19.2 made `unsafe` (#2348)
910
911  It exports safe types only. It may depend on the pure crates
912  (`jev-protocol`), never the reverse.
913- **`crates/postjevsql`**: the pgrx extension. It glues
914  `jev-protocol` to `postjevsql-pg` through their safe APIs, under
915  `#![forbid(unsafe_code)]`. That is compatible with pgrx's macros:
916  - pgrx emits its `unsafe` code with call-site or
917    `mixed_site().located_at(..)` spans (`pgrx-sql-entity-graph/src/
918    pg_extern/mod.rs:49-56`, `pgrx-macros/src/rewriter.rs:92`).
919  - A rustc 1.98.1 proc-macro experiment confirmed that those spans do
920    not trip the lint; only user-hygiene spans do (2026-09-22).
921
922  No published project was found doing this, so CI builds the crate
923  with the lint on to keep the claim true.
924
925### 1. pgrx has no CustomScan wrapper, so write one, once
926
927pgrx v0.19.2 exposes CustomScan and the planner hooks only as raw
928`pg_sys` bindings. All of that `unsafe` lives in `postjevsql-pg` behind a
929safe trait. The Jev scan then implements that trait and never touches
930`pg_sys`:
931
932- `create_custom_path`
933- `plan_custom_path`
934- `begin`
935- `exec` (returns the next slot)
936- `explain`
937- `end`
938
939No safe Rust CustomScan crate is published: crates.io has none, and
940pgrx issue #1405 ("Add `ExtensionNode` support") has been open since
9412024. **Model the trait on lagodb-core's `LagodbCustomScanProvider`**
942(github.com/lagodb/lagodb, `lagodb-core/src/customscan`, Apache-2.0,
943@db35f59). It has `begin`, `next_slot`, `rescan`, `end`, DSM, re-checks
944for rows locked after a change, and a `custom_private` envelope that
945survives `copyObject`. It may be copied with attribution. xataio/deltax
946(Apache-2.0) and darthunix/pg_fusion (BSD-2) are further permissive
947references. ParadeDB's `pg_search` (`trait CustomScan`) proves the
948shape, but **ParadeDB is AGPL-3.0**: read it for the design, and do not
949copy its code into this MIT-intended repo. Every `unsafe` block carries a
950`// SAFETY:` comment naming the Postgres invariant it relies on.
951
952The wrapper is sound only if it enforces these. Each one is a trap:
953
954- **Every callback in `CustomScanMethods` / `CustomExecMethods` is
955  `#[pg_guard] extern "C-unwind"`.** A Postgres `ERROR` is a `longjmp`.
956  Jumping across Rust frames that own `Drop` values is undefined
957  behaviour. `pg_guard` turns an `ERROR` into a Rust panic and turns a
958  panic at the boundary back into an `ERROR`. One unguarded callback
959  voids the whole wrapper.
960- **Never put a Rust pointer in `custom_private`.** Plans are copied
961  (`copyObject`), cached in prepared statements, and serialized to
962  parallel workers. Plan-time state must be a List of Nodes. Runtime
963  state belongs in the scan state, built in `begin`.
964- **`EndCustomScan` is not called when the query errors.** Anything
965  that owns an OS resource (the connection, sockets) must also be freed by
966  a `MemoryContextRegisterResetCallback` on the executor's context.
967  Otherwise every cancelled query leaks sockets.
968- **The scan state lives in palloc'd memory, which never runs `Drop`.**
969  Embed `CustomScanState` as the first field of a `#[repr(C)]` struct.
970  Drop the Rust part in place, exactly once: in `end`, or in the reset
971  callback, whichever runs first.
972- **References into Postgres memory carry the context's lifetime.**
973  Use pgrx's `MemCx<'mcx>`. Nothing borrowed from a per-tuple context
974  may outlive the next `exec`.
975- **The safe types are `!Send` and `!Sync`**, so safe code cannot move
976  them to another thread.
977- **`rescan` is required, not optional.** `ReScanCustomScan` is listed
978  under "Required executor methods", and any scan on the inner side of a
979  nested loop is rescanned. It resets the batch state; repeats are
980  answered from the cache.
981- **Path flags cover only BACKWARD_SCAN, MARK_RESTORE and PROJECTION**
982  (PG18 `extensible.h`). Don't set the first two. Set PROJECTION
983  (*Execution*). Parallelism is controlled through `parallel_safe` and
984  partial paths, and the `jev*` functions are `PARALLEL RESTRICTED`.
985
986### 2. Async inside the backend, on Postgres's own wait
987
988Async runs in the backend on a single-threaded executor whose reactor
989**is** a Postgres `WaitEventSet`. There is one design and no fallback.
990
991- **Executor** (in `postjevsql-pg`): single-threaded, with no `Send`
992  bound on futures. The ready queue lives on the backend thread. Wakers
993  push to that queue. There are no other threads, so nothing wakes from
994  elsewhere; debug builds assert the calling thread.
995- **Reactor**: a `WaitEventSet` **built for each wait** from the
996  currently registered fds, holding:
997  - `WL_LATCH_SET` on `MyLatch`
998  - `WL_EXIT_ON_PM_DEATH`
999  - `WL_SOCKET_READABLE` / `WL_SOCKET_WRITEABLE` for every registered
1000    socket
1001
1002  It is freed by a `Drop` guard. It is not kept long-lived, because
1003  there is no `RemoveWaitEvent` in any supported major or master. A long-lived
1004  set could never drop a closed socket after a reconnect or a DNS UDP
1005  exchange, and its size is fixed at creation. `CreateWaitEventSet`
1006  takes a `ResourceOwner` in every supported major (17 and later), so
1007  there is no version shim. With the usual
1008  single h2 socket this is `WaitLatchOrSocket`, and the extra epoll
1009  syscalls cost nothing against 70–500 ms requests.
1010
1011  The wait timeout is the executor's earliest timer. After each wake:
1012  `ResetLatch`, `CHECK_FOR_INTERRUPTS()` (guarded), then poll the woken
1013  tasks. There is one blocking point, with no polling slices and no
1014  threads.
1015- **Task scopes.** The connection task is scoped to the backend and held
1016  in a thread-local. Each scan's tasks belong to a **scan guard**, and
1017  the guard's `Drop` removes them from the executor. The guard is
1018  dropped by `end` or by the memory-context reset callback. Otherwise a
1019  cancel either kills the shared connection (if the executor is
1020  dropped) or leaves the cancelled query's futures in the ready queue (if
1021  it is not).
1022- **Cancellation is `Drop`.** Cancel or `statement_timeout` raises an
1023  `ERROR` from `CHECK_FOR_INTERRUPTS()`. `pg_guard` turns it into a
1024  panic, which unwinds out of the executor and drops the in-flight
1025  futures. That closes their streams, which is async's native cancel
1026  semantics.
1027- **Stack** (runtime-agnostic crates only):
1028  - `hyper` 1.x client, with our socket type implementing
1029    `hyper::rt::Read`/`Write` over a non-blocking `std::net::TcpStream`
1030    registered in the reactor, and our executor as hyper's spawner.
1031  - `rustls`, which needs no runtime, with the system trust store loaded
1032    once.
1033  - `hickory-resolver` on the same executor, verified 2026-09-22
1034    against 0.26.3:
1035    - It accepts custom runtimes through `hickory_net::runtime::
1036      RuntimeProvider` (`crates/net/src/runtime.rs:245` at tag
1037      `v0.26.3`), plugged in with
1038      `Resolver::builder_with_config(config, provider)`.
1039    - Its I/O traits are `futures-io`, not tokio.
1040    - Built (2026-09-23): `postjevsql-pg/src/dns.rs`. Its sockets are
1041      `Send + Sync` bare fds, and debug builds assert the backend
1042      thread. `jev.dns_servers` (superuser; `ip[:port], …`) overrides
1043      `/etc/resolv.conf`. That is what lets the tests use a mock DNS
1044      server on a high port. Resolved addresses are tried in the
1045      resolver's order (IPv4 first).
1046    - hickory's cache is moka's `sync::Cache`, whose `thread::spawn`
1047      calls are all in tests or doc comments (checked in moka 0.12.16),
1048      so it adds no threads.
1049    - It is kept over `getaddrinfo` because DNS is the one step where
1050      plain libc would block without seeing a cancel (glibc waits 5 s
1051      per attempt, per nameserver), and reconnects now happen after
1052      every 300 s idle gap. The resolved address is cached for its TTL
1053      across reconnects.
1054    - Stated cost: hickory reads `/etc/resolv.conf` and `/etc/hosts`
1055      (`crates/resolver/src/hosts.rs:226`) but not nsswitch, so nscd,
1056      nss-resolve, LDAP and mDNS names do not resolve.
1057- **HTTP/2, one connection per backend.** `api.typesafe.ai` negotiates
1058  `h2` over ALPN (checked 2026-09-22 with `openssl s_client -alpn h2`).
1059  Every batch of every query is a stream on that one connection, so
1060  there is no per-request connection setup and no pool.
1061  - **Server limits** (raw h2 handshake, 2026-09-22; the path is a
1062    Cloudflare edge in front of an Envoy origin):
1063    - `SETTINGS_MAX_CONCURRENT_STREAMS` = 100
1064    - `INITIAL_WINDOW_SIZE` = 65536
1065    - `MAX_FRAME_SIZE` = 16777215
1066
1067    `jev.concurrency` is clamped to the peer's
1068    `MAX_CONCURRENT_STREAMS`. Above that limit, hyper's `poll_ready`
1069    queues without saying so, and the setting would not mean what it
1070    says. hyper keeps h2's view of the peer's SETTINGS private, so
1071    `net.rs`'s `PeerSettings` reads the value off the server's frames as
1072    hyper reads them, and the scan asks for its window before each row
1073    it pulls. Until a backend's first connection has sent SETTINGS, the
1074    limit is taken as 100 (RFC 9113 §6.5.2's recommended minimum); a
1075    reconnect starts from the last value seen. Request bodies near the 64k-token limit (~256 KB) exceed the
1076    server's 64 KiB stream window and pay window-update round trips; that
1077    is a known cost, not a bug.
1078  - **Our receive windows are adaptive** (hyper's `adaptive_window`,
1079    set in `https.rs`). hyper's defaults (5 MB per connection,
1080    2 MB per stream, `src/proto/h2/client.rs:48-49`) are below
1081    `8 MB × concurrency`.
1082  - **The connection is reused across queries, not held for the
1083    backend's lifetime.** Cloudflare closes idle client HTTP/2
1084    connections after **400 s, not configurable** (developers.cloudflare
1085    .com/fundamentals/reference/connection-limits, updated 2026-07-23).
1086    An idle backend runs no executor, so it cannot ping between queries.
1087    At the start of each scan, if the connection has been idle for more
1088    than 300 s, drop it and reconnect before sending. Pings run only
1089    while a scan is active: hyper's keep-alive on the executor's timers,
1090    every 20 s without a frame, closing the connection when one goes
1091    unanswered for 10 s. Both are superuser GUCs
1092    (`jev.keepalive_interval`, `jev.keepalive_timeout`) and part of the
1093    `Endpoint`, so changing one opens a new connection. `tests/keepalive.rs`
1094    shows against jev-mock that pings go out while a scan waits and not
1095    between statements, and that an unanswered one closes the connection
1096    and the request is retried on a new one (2026-09-26).
1097  - **On `GOAWAY`**, retry only streams above `last-stream-id` or those
1098    refused with `REFUSED_STREAM`. A stream cut mid-flight goes through
1099    the normal retry policy.
1100  - **No HTTP proxy support comes for free.** hyper does not read
1101    `HTTPS_PROXY`. If a deployment needs an egress proxy, CONNECT
1102    tunnelling is built explicitly.
1103
1104**Hickory constraints (verified in source):**
1105
1106- **`default-features = false, features = ["system-config"]`.** The
1107  default features include `tokio`, and `hickory-net`'s `tokio` feature
1108  enables `tokio/rt-multi-thread`. Every DNS-over-TLS, DNS-over-HTTPS and
1109  QUIC feature forces `tokio` through `__tls`. So the backend resolves
1110  with plain DNS from `/etc/resolv.conf` only.
1111- **Its traits demand `Send + Sync`:**
1112  - `RuntimeProvider: Clone + Send + Sync + Unpin`
1113  - `Spawn::spawn_bg` takes `Future + Send`
1114  - `DnsUdpSocket`, `DnsTcpStream` and `Time` are `Send + Sync`
1115
1116  So the executor must accept `Send` futures as well as local ones. The
1117  socket and handle types must be `Send + Sync` by construction: they
1118  hold a raw fd and a token, and reach the reactor through a
1119  backend-thread-local. They are never a pointer into Postgres memory.
1120  Debug builds assert that they are used on the backend thread.
1121
1122**Process hazards:**
1123
1124- **Create nothing in `_PG_init`.** Under `shared_preload_libraries` it
1125  runs in the postmaster, before the fork. The rustls `ClientConfig`,
1126  RNG, TLS session state and connection are all built lazily in the
1127  backend on first use. Do not rely on aws-lc's fork detection.
1128- **No Rust dependency may install a signal handler.** Postgres's own
1129  SIGINT/SIGALRM handlers set `MyLatch`, which is how cancel reaches the
1130  wait.
1131- **`Drop` of futures, streams and connections must never panic.**
1132  Cancellation unwinds (`panic = "unwind"`, from the cargo-pgrx
1133  template), and a panic during unwind aborts the backend.
1134- **Incompatible with extensions that call Postgres from their own
1135  threads.** pgrx #2228: pg_duckdb does this, so another extension's
1136  thread can run our planner and executor hooks, where pgrx's thread
1137  check aborts. Loading both is unsupported; the README says so.
1138
1139**Traps (each looks like a simplification and is a regression):**
1140
1141- **tokio in the backend.** Its multi-thread runtime runs futures on
1142  worker threads, where Postgres APIs corrupt the backend. Even its
1143  current-thread runtime sleeps in its own `epoll`, which retries after
1144  `EINTR` and never sees the latch, so a cancel waits for the request to
1145  finish. reqwest, and hyper-util's default resolver, bring
1146  `spawn_blocking` threads.
1147- **Running tokio in 10 ms slices with interrupt checks in between.**
1148  That is polling: it adds latency and burns CPU.
1149
1150  This is a project rule, not a pgrx rule. pgrx's own
1151  `docs/src/design-decisions.md` allows threads that never touch
1152  Postgres. We use none, so no Postgres call can ever happen off-thread.
1153  pgrx #1067 (async design, still open) concedes that tokio
1154  `current_thread` does not handle interrupts.
1155- **A proxy daemon for the HTTP.** It adds a process, an IPC hop and a
1156  serialization step to every request, and buys nothing that one
1157  multiplexed connection per backend does not already give.
1158- **Threads in a bgworker.** Never.
1159
1160**Considered and not chosen:**
1161
1162- **libcurl's `multi_socket` interface.** It is a correct alternative.
1163  It multiplexes over HTTP/2 by default (`CURLMOPT_PIPELINING` defaults
1164  to `CURLPIPE_MULTIPLEX`). `CURLMOPT_SOCKETFUNCTION` and
1165  `TIMERFUNCTION` let it register sockets in the WaitEventSet, and
1166  curl-rust exposes them (`src/multi.rs:157`, `256`, `515`, `378`). It
1167  also honours `HTTPS_PROXY` and resolves through nsswitch. Supabase
1168  pg_net drives exactly this, from a bgworker with its own epoll. hyper
1169  is chosen so that the edge crate's types stay in Rust. The price is
1170  building proxy support explicitly and accepting hickory's DNS limits.
1171
1172### 3. Packaging per major is nix's job
1173
1174**Supported majors are PostgreSQL 17 and later** (17 and 18 today;
1175`PG_MAJORS` in `build/defs.bzl`). The floor is 17 because the rate
1176limiter's only path is `GetNamedDSMSegment` (PG17+) and
1177`CreateWaitEventSet` takes a `ResourceOwner` from 17 on, so no code
1178path carries a version shim. Supporting 16 and earlier is a deliberate
1179later pass, not an omission: it would add a second rate-limiter path
1180(`shared_preload_libraries` shmem) and the WaitEventSet shim back.
1181Both packages build (2026-09-26). The test harness serves either
1182major: `tests/support/postgres.rs` uses `extension_control_path` on 18,
1183and on 17, which lacks it, runs the server from a symlink prefix whose
1184extension directory links the built tree (nixpkgs' relative-to-symlinks
1185patch makes the server read that prefix's sharedir; measured
11862026-09-26). The devshell writes `postgres_bin_NN` and `pg_config_NN`
1187for every major into `.buckconfig.local`.
1188
1189**One buck graph builds every major** (built 2026-09-26). The major is
1190a configuration: `//platforms:pgNN` is the host platform plus the
1191`pg_major` constraint, and everything that differs by major branches on
1192it through `pg_major_select` in `build/defs.bzl` (unconstrained
1193configurations get the newest):
1194
1195- pgrx's and pgrx-pg-sys's `pgNN` features, and pgrx-pg-sys's bindgen
1196  run's `PGRX_PG_CONFIG_PATH` (from `pg_config_NN`), are set by the
1197  wrappers in `third-party/pg_major.bzl`, which `reindeer.toml`'s
1198  `buckfile_imports` loads over the prelude's `cargo` and
1199  `buildscript_run`, so a `reindeer buckify` keeps them.
1200- First-party crates' `pgNN` cfg, and the tests' server
1201  (`POSTJEVSQL_POSTGRES_BIN`) and `POSTJEVSQL_PG_MAJOR`, select the same
1202  way (`tests/BUCK`, `tools/record-gates/BUCK`).
1203- `first_party_test` also emits `<test>-pgNN` for every older major,
1204  with `default_target_platform = //platforms:pgNN`, so `buck2 test
1205  //...` runs the whole suite on 17 and 18.
1206
1207**Built 2026-09-26 through buck, not cargo-pgrx** (`nix/package.nix`,
1208flake `legacyPackages.<system>.postgresqlNNPackages.postjevsql`). The
1209first-party crates exist only as BUCK targets, so `buildPgrxExtension`
1210would need a second, hand-kept Cargo graph beside them. The package
1211builds `//crates/postjevsql:ext` in the sandbox, the tree the tests
1212install, and puts it in nixpkgs' layout (`lib/`,
1213`share/postgresql/extension/`), so `withPackages` and
1214`services.postgresql.extensions` take it. The traps it closes:
1215
1216- Every `http_archive` in `third-party/BUCK` is fetched by nix from the
1217  url and sha256 read out of that file at eval, and
1218  `nix/offline_archive.bzl` is loaded over the builtin in the sandbox:
1219  buck2's downloader refuses `file://` (measured 2026-09-26).
1220- buck2 loads a trust store at startup, so `SSL_CERT_FILE` is set
1221  though nothing is fetched.
1222- The prelude's wrapper scripts start `#!/usr/bin/env bash`, which the
1223  sandbox lacks, so buck runs under bwrap with that one path added.
1224- Release builds turn `-Cdebug-assertions` off in `toolchains/BUCK`.
1225- The majors built are `PG_MAJORS` in `build/defs.bzl`, read by
1226  `nix/majors.nix` for the flake and the package, which builds with
1227  `--target-platforms //platforms:pgNN` and writes that major's
1228  `postgres_bin_NN` and `pg_config_NN`. Any major not listed fails at
1229  eval with the supported list, rather than compiling against the wrong
1230  headers.
1231
1232The cargo-pgrx bullets below are the reasoning for the pin, which buck
1233carries in `third-party/Cargo.toml`; no `cargo-pgrx` is built.
1234
1235- **Pin `pgrx = "=0.19.2"`, and build `cargo-pgrx` 0.19.2 in this repo's
1236  flake.** Build it with `rustPlatform.buildRustPackage`, the same shape
1237  as nixpkgs' `generic` helper; it becomes a one-liner if NixOS/nixpkgs#526051
1238  merges. Pass it explicitly to `buildPgrxExtension`.
1239- **Never use nixpkgs' default `cargo-pgrx`.** nixpkgs' own
1240  `cargo-pgrx/default.nix` says the default is "Not to be used with
1241  buildPgrxExtension, where it should be pinned". Every in-tree pgrx
1242  extension takes a pinned attribute.
1243- **Why not 0.18.x** (this reverses an earlier pin to 0.18.1):
1244  - There is no `cargo-pgrx_0_18_1` attribute in nixpkgs, so the pin
1245    breaks the day the default moves.
1246  - pgrx 0.18.0 and 0.18.1 wipe the DETAIL line of every ERROR that pgrx
1247    catches and rethrows (#2262, fixed by #2361 in 0.19.2). Every
1248    `pg_guard` boundary in this design would lose error detail.
1249  - 0.18.1 supports pg13 to pg18; 0.19.2 adds pg19. nixpkgs already
1250    ships `postgresql_19` (beta), and GA is expected around the end of
1251    October 2026.
1252  - 0.19.2 adds safe `PgNode` casting (#2347) and extra executor
1253    headers (#2353), both useful for the CustomScan wrapper.
1254  - No nixpkgs PR bumps cargo-pgrx to 0.19 (searched 2026-09-22), so
1255    carrying our own is the only route.
1256- **Per-major builds are ours to wire.** Only packages inside nixpkgs'
1257  `ext/` directory land in `postgresqlNNPackages` automatically, through
1258  `packagesFromDirectoryRecursive`. The flake calls
1259  `postgresql_NN.pkgs.callPackage ./nix/package.nix {}` for each
1260  supported major in `PG_MAJORS`, as the nixpkgs PostgreSQL manual
1261  documents. The sidecar module uses
1262  `services.postgresql.extensions = ps: [ (ps.callPackage ./nix/package.nix {}) ]`.
1263- **The test suite is a separate flake check.** The package sets
1264  `doCheck = false`, as every in-tree pgrx package does, because "pgrx
1265  tests try to install the extension into the postgresql nix store".
1266  nixpkgs' `postgresqlTestExtension` provides the smoke test.
1267- Outside nix, a CI matrix runs `cargo pgrx package` once per major.
1268- **The Cargo workspace is generated from buck** (built 2026-09-26,
1269  `tools/cargo-gen`). Each `first_party_*` macro records its kind, crate
1270  root and deps; the tool writes the root `Cargo.toml` (members,
1271  `[workspace.dependencies]` from `third-party/Cargo.toml`, pgrx's
1272  `=0.19.2` pin with its major feature stripped, pgrx's `panic =
1273  "unwind"` profiles) and one manifest per package, with a `pgNN`
1274  feature for each of `PG_MAJORS` in `build/defs.bzl` (the last is the
1275  default) on every crate that reaches pgrx. Every manifest is `MIT OR
1276  Apache-2.0`, `authors = ["The postjevsql Authors"]`, no `repository`.
1277  `//tools/cargo-gen:drift` fails when the committed manifests differ.
1278  Integration tests (`tests/*.rs`) stay buck-only: they need buck's
1279  location of the built extension.
1280- **Licences are checked, not assumed.** `deny.toml` (built 2026-09-26)
1281  is pgrx 0.19.2's allowlist plus `BSD-2-Clause` and
1282  `CDLA-Permissive-2.0`; cargo-deny rejects anything unlisted, so AGPL
1283  is denied (verified by clarifying a crate as AGPL-3.0-only). The repo
1284  is `MIT OR Apache-2.0` (`LICENSE-MIT`, `LICENSE-APACHE`, "The
1285  postjevsql Authors").
1286- **`THIRD-PARTY` carries the linked crates' notices** (built 2026-09-26,
1287  `tools/third-party-notices`): every crates.io crate linked into the
1288  `.so` (the target deps of `//crates/postjevsql:postjevsql`, so build
1289  scripts, proc-macros and test crates are left out), its SPDX expression
1290  and every licence text, identical texts once. Among them ring is
1291  Apache-2.0 **and** ISC and webpki-roots is CDLA-Permissive-2.0, both
1292  compatible with MIT distribution. aws-lc is not linked: rustls is built
1293  on ring. The nix package installs it in `share/doc/postjevsql`.
1294- **`THIRD-PARTY-sidecar` does the same for the sidecar CLI** (built
1295  2026-09-27): the target deps of `//crates/postjevsql-sidecar:cli`
1296  (tokio, tokio-postgres, toml, …), which links no pgrx. Each binary ships
1297  only its own set: `nix/sidecar-cli.nix` installs it as
1298  `share/doc/postjevsql-sidecar/THIRD-PARTY`. Both files come from one
1299  run of the tool (`:notices[extension]`, `:notices[sidecar]`), because
1300  one `texts/` serves both and its staleness check is against the union.
1301  - The archives are read through `$(query_outputs …)`, which reaches the
1302    private `http_archive` targets in `third-party/BUCK` where a dep
1303    cannot (a `third-party/PACKAGE` visibility is overridden by the
1304    targets' own `visibility = []`, and reindeer hard-codes it). So no
1305    second list of urls and hashes exists.
1306  - A crate with no licence expression or no text fails the build. pgrx,
1307    pgrx-pg-sys, pgrx-sql-entity-graph and seahash ship no text in their
1308    archives, so theirs is in `tools/third-party-notices/texts/`, fetched
1309    from upstream at the version and named in `SOURCE`; an entry there
1310    for a crate linked into neither binary, or one that ships its own,
1311    fails too.
1312  - `//tools/third-party-notices:drift` fails when either committed file
1313    is stale; `buck2 run //tools/third-party-notices:update` rewrites both.
1314
1315### 4. Everything outside Rust is generated or thin
1316
1317- **Install SQL**: generated by pgrx from `#[pg_extern]`,
1318  `PostgresType` and `extension_sql!`. Under buck, `tools/pgrx-schema`
1319  reads the `.pgrxsc` section of the built `.so` (what `cargo pgrx
1320  schema` does). Do not hand-write `postjevsql--x.y.sql`. Grants and
1321  revokes go in `extension_sql!` blocks.
1322- **The version is written once**, in `build/defs.bzl`. The control
1323  file carries pgrx's `@CARGO_VERSION@` placeholder, filled at build.
1324- **No Perl TAP.** Rust integration tests (*Testing*) start a
1325  throwaway instance and connect with `tokio-postgres`. They cover
1326  what pg_typesafe's TAP tests cover:
1327  - retries on 429/529
1328  - `pg_cancel_backend` from a second connection mid-request
1329  - the 8 MB cap
1330  - https enforcement
1331
1332  The details are in *Testing*.
1333- What remains in SQL is the user interface itself and the expected
1334  output, which is correct: SQL is the product's surface.
1335
1336## Deployment: in-database or sidecar
1337
1338The same extension ships two ways. Choose per target and never fork the
1339code.
1340
1341- **In-database.** Install the extension into the Postgres that owns the
1342  data. This is the default whenever you control that server.
1343- **Sidecar** (new). A local Postgres runs the extension and exposes the
1344  target's tables as `postgres_fdw` foreign tables. The target installs
1345  nothing. Clients connect to the sidecar with the ordinary protocol:
1346  psql, JDBC and GUI tools all work.
1347
1348**Managed hosts cannot run it in-database** (vendor docs, 2026-09-22):
1349
1350- **No custom compiled extensions at all:** RDS, Aurora, Cloud SQL,
1351  AlloyDB, Azure, Supabase and PlanetScale. pg_tle takes "JavaScript,
1352  Perl, Tcl, PL/pgSQL, and SQL", no C. Trusted PL/Rust cannot open
1353  sockets.
1354- **Possible on request:** Neon, Xata and Crunchy may host a custom
1355  extension if you ask their support. Xata also offers
1356  bring-your-own-cloud.
1357- **Their built-in HTTP calls are per row:** `http`, `pg_net` (async;
1358  results land after the query ends), `aws_lambda`,
1359  `google_ml.predict_row` and `azure_ml`. They lose the batch node, the
1360  cache and the cost guard.
1361
1362So the sidecar is the supported mode for managed hosts. Proxies were
1363checked and rejected as hosts: PgDog's plugin API has only
1364`init`/`route`/`fini` and cannot rewrite queries or see results, and
1365PgCat is unmaintained.
1366
1367### How sidecar mode splits a query
1368
1369postgres_fdw ships a function or operator to the remote only if it is
1370built in, or belongs to an extension listed in the server's `extensions`
1371option, **and** is IMMUTABLE (`deparse.c:285`,
1372`contain_mutable_functions`). `jev*` is VOLATILE, so it never ships.
1373
1374A `jev*` qual that stays on the sidecar is a **local condition**, and
1375postgres_fdw then refuses to push down (PG18 `postgres_fdw.c`):
1376
1377- joins involving that table (L5837-5842)
1378- aggregates over it (L6527-6532)
1379- LIMIT/OFFSET (L7196-7200)
1380
1381On a foreign table that carries a `jev*` qual, only the other WHERE
1382clauses and ORDER BY run on the target. Everything else runs on the
1383sidecar. So `count(*) … WHERE jev(…)` pulls every row that passes the
1384target's filters. A qual referencing two tables is a join qual, so that
1385join can still push down. LIMIT still stops early, because postgres_fdw
1386reads through a cursor `fetch_size` rows at a time.
1387
1388Everything else in this contract works unchanged, including queries
1389jevQL refuses: CTEs, subqueries, and `UPDATE … WHERE jev(…)` through the
1390foreign table.
1391
1392The CustomScan wraps whatever scan the planner chose, `ForeignScan`
1393included. The child plan is built with the `jev*` quals removed, and the
1394node applies them. This holds in both modes: a child that kept them would
1395judge rows one at a time, outside any request.
1396
1397### Sidecar invariants
1398
1399- **Never list this extension in a foreign server's `extensions`
1400  option.** Volatility already stops `jev*` from shipping, so this is a
1401  guard for the day someone marks a helper IMMUTABLE. At plan time the
1402  CustomScan raises an `ERROR` if the foreign server of any `ForeignScan`
1403  it wraps lists this extension.
1404  Built 2026-09-26 (`refuse_shipping_servers` in
1405  `postjevsql-pg/src/scan/plan.rs`; `tests/sidecar.rs`, a postgres_fdw
1406  loopback): `plan_custom_path` walks the child plan, so a `ForeignScan`
1407  under a Sort (ORDER BY) is found too, and splits the option as
1408  postgres_fdw does. The error is 22023, raised while planning, so
1409  EXPLAIN is refused as well and nothing is sent.
1410- **`fetch_size` is at least the node's in-flight window** (default 100
1411  rows). Otherwise each window costs several network round trips.
1412  `batch_size` does the same for INSERT. `async_capable`,
1413  `parallel_commit` and `parallel_abort` help only with several foreign
1414  scans or servers, so they stay off for a single target.
1415- **The target's credentials never live in the repo.**
1416  - On PG18, prefer `use_scram_passthrough`. No password is stored, but
1417    both servers need the same SCRAM secret, clients must log into the
1418    sidecar with SCRAM, and the target must require `scram-sha-256`.
1419  - Otherwise, the user mapping's password is written from a secret when
1420    the module activates (the estate's opnix / systemd-creds path).
1421  - The connection to the target uses `sslmode=verify-full`.
1422- **The planner gets real remote statistics.** Set
1423  `use_remote_estimate 'true'` on the server, or `ANALYZE` the foreign
1424  tables on a timer. Without either, the high cost of `jev*` is weighed
1425  against guessed row counts, and the batch plan is wrong.
1426- **The cache lives in the sidecar** (a local table). The target is
1427  never written to for bookkeeping.
1428
1429### Costs to state, not hide
1430
1431- Every row that passes the target-side filters crosses the network to
1432  the sidecar, as with jevQL.
1433- postgres_fdw is not two-phase commit ("it is currently not supported
1434  by postgres_fdw to prepare the remote transaction for two-phase
1435  commit"). The remote COMMIT runs before the local one
1436  (`connection.c:1072-1089`). So a failure between the two leaves the
1437  target committed and loses only the sidecar's cache rows, which is
1438  harmless. `PREPARE TRANSACTION` fails on the sidecar once a transaction
1439  has touched a foreign table.
1440- Pushdown is narrower than in-database (see above): joins, aggregates
1441  and LIMIT involving a `jev`-filtered foreign table run on the sidecar.
1442- There is one extra network hop for every query, `jev` or not, that
1443  goes through the sidecar.
1444
1445### Launching it
1446
1447**The Rust CLI `postjevsql-sidecar` is the single engine**
1448(`crates/postjevsql-sidecar`). It reads one TOML config and:
1449
1450- `converge`: creates `postgres_fdw` and `postjevsql`, declares the
1451  foreign server (target host, `sslmode`
1452  defaulting to verify-full, `use_remote_estimate`) with exactly the
1453  declared options, and the user mappings (password from a secret, or
1454  `use_scram_passthrough` on PG18 when none is given)
1455- `sync`: compares the target's catalog with the foreign tables and
1456  plans CREATE, ALTER FOREIGN TABLE (added, altered, renamed or dropped
1457  columns) and DROP, never drop-and-reimport. It prints the plan, and
1458  applies it in one transaction only with `--apply`. A change that would
1459  break a sidecar view, grant or function (pg_depend) is refused, naming
1460  each dependent, and nothing is changed.
1461
1462Each secret (the target password, the Jev API key) comes from a file or
1463an env var, exactly one; both or neither is refused, naming the sources,
1464never the value.
1465
1466Everything that launches a sidecar calls it: the NixOS module
1467(`nix/sidecar.nix`, whose oneshot runs `converge` then `sync --apply`,
1468with opnix or systemd-creds writing the secret files), the container
1469image, and a Postgres the user already runs. The module keeps only what
1470nix owns: `services.postgresql`, this extension from
1471`postgresqlNNPackages.postjevsql`, `postgres_fdw` (contrib) and the unit.
1472
1473**This reverses an earlier rejection** (owner, planning interview,
14742026-09-26). The contract used to say a Rust launcher would be a second
1475configuration engine beside nix. That held only while nix was the one
1476route. The audience is now any Postgres user, including managed-host
1477users with no nix, served by a container image and by setup for their
1478own Postgres, and the owner ruled that setup tooling is a Rust CLI,
1479never Bash. With three launchers, the configuration engine has to live
1480below all of them, so the CLI is the one engine and nix is one caller;
1481declaring the same state in both would be the second engine.
1482
1483**Status (2026-09-27):** the binary and its pg_depend refusal are built
1484(`cli/main.rs`, postjevsql `cac7966`; `tests/sidecar_cli.rs`), and so is
1485the module around it. So is the container image (2026-09-27,
1486`nix/sidecar-image.nix`, flake `packages.<system>.postjevsql-sidecar-image`,
1487a `docker load`-able tarball): PostgreSQL 18 with the per-major package
1488and postgres_fdw, dash as `/bin/sh` only (initdb runs `postgres -V`
1489through popen), uid 999, the cluster in the volume
1490`/var/lib/postgresql`. Its entrypoint is the CLI's `serve`, not a
1491script: resolve every secret (so file-and-env is refused before
1492anything starts), `initdb` if `$PGDATA` is empty, start postgres, wait
1493until it accepts a connection, `converge` and `sync --apply`, then stop
1494it with `pg_ctl stop` and exec `postgres` in its place, so the server is
1495PID 1 and the runtime's signals reach it; a refusal stops the server and
1496exits 1 with the CLI's message. Postgres options go after `--`. Its VM
1497test (`nix/sidecar-image-test.nix`, flake check `sidecar-image`) runs it
1498in docker against `nix/test-target.nix`'s TLS- and SCRAM-only target.
1499
1500The module (`nix/sidecar.nix`, flake `nixosModules.sidecar`) takes the
1501extension as `extension = ps: …` (default `nix/package.nix` for the
1502server's major; the flake's module passes it the pinned buck2) and the
1503CLI as `package` (default `nix/sidecar-cli.nix`, the same derivation
1504building `//crates/postjevsql-sidecar:cli`; also flake
1505`packages.<system>.postjevsql-sidecar`). A oneshot after postgresql
1506writes one TOML per declared server with `pkgs.formats.toml` and runs
1507`converge` then `sync --apply` on it. `converge` also creates
1508`postgres_fdw` and `postjevsql`. The config holds paths only: each
1509mapping's password file is loaded with `LoadCredential` and named by
1510its `/run/credentials/postjevsql-sidecar.service/…` path, and
1511`apiKeyFile` (required, since the unit has no other source) is the
1512server's `jev.api_key_file`. Schemas import into same-named local
1513schemas, as the CLI does. The nixosTest (`nix/sidecar-test.nix`, flake
1514check `sidecar`) boots it against a target cluster on loopback port
15155433, TLS and SCRAM only, and checks `CREATE EXTENSION postjevsql`, the
1516imported tables read over verify-full with the mapping's password, a
1517jev call planned as the scan over a `ForeignScan`, that a restart of the
1518unit picks up a new target column and drops a hand-added `extensions`
1519option, and that neither the unit script nor the config holds the
1520password.
1521
1522## Testing
1523
1524- **Build and test through buck2** from the nix devshell
1525  (`nix develop -c buck2 test //...`). nix provisions toolchains and
1526  Postgres; buck owns the graph. Third-party crates come from
1527  `third-party/Cargo.toml` via reindeer (`vendor = false`), with
1528  `cargo_env = true` so build scripts see the full Cargo environment.
1529  Toolchain paths reach buck through `.buckconfig.local`, which the
1530  devshell writes, because the prelude's cc shim execs without a PATH
1531  search.
1532- **`tests/run-check.sh` is the repo's one check entry and only
1533  delegates** to `nix develop -c buck2 test //...`. mefi-studio runs it
1534  to verify a finished task (its `baseCheckForProject` finds
1535  `test/` or `tests/run-check.sh` off Windows, mefi-studio `94350fc`).
1536  Without a check, every finished run was recorded as "unverified" and
1537  re-dispatched, so each cache plan ran four times (2026-09-26). Do not
1538  add a `package.json`: Studio prefers its scripts over this file, so it
1539  would become a second entry point.
1540- **One buck daemon per checkout, shared by every session in it.** Never
1541  `buck2 kill` it and never run concurrent agents in one checkout. Each
1542  agent works in its own git worktree, which gets its own daemon and
1543  `buck-out`. On 2026-09-23 two agents in the main checkout killed each
1544  other's daemon ("Forkserver is unavailable"), both stalled, and the
1545  cache-key work sat uncommitted for three days.
1546- **Warnings fail tests.** buck prints no compiler output for actions
1547  that succeed, so a warning is invisible. Every first-party target is
1548  declared through `build/defs.bzl`'s `first_party_*` macros, which add
1549  a `<name>-lint` test. That test fails with the report when clippy
1550  (which includes every rustc warning) says anything. Dev and test
1551  builds keep debug assertions on.
1552- **Unit tests**: `jev-protocol` (in ~/jevcrates, `cargo test`) has plain tests, plus proptests
1553  asserting that every parser of network bytes rejects rather than
1554  panics.
1555- **Integration tests run a throwaway Postgres (17 and 18, `PG_MAJORS`) from the devshell**
1556  (`tests/support/postgres.rs`): `initdb` into a temp dir, `postgres` as
1557  a child process, the built extension served in place through
1558  `extension_control_path` / `dynamic_library_path`. The API is
1559  testcontainers-shaped (builder, `start`, teardown on `Drop`). There is
1560  no docker (ruled 2026-09-23, replacing a testcontainers setup that
1561  needed a nix-built image, host networking and a container reaper).
1562  - **Teardown cannot leak.** The postmaster runs with
1563    `PR_SET_PDEATHSIG` = SIGQUIT, so the kernel shuts it down when the
1564    spawning test thread dies, SIGKILL included (verified).
1565  - Every test sets `statement_timeout`, so a hang fails the test
1566    instead of reaching buck's 10-minute kill.
1567  - `tests/latency.rs` measures the extension's own cost per call
1568    against the instant mock (2026-09-23: warm p50 310 µs against a
1569    143 µs floor for the bare query; cold 44.5 ms).
1570- **One mock endpoint, our own** (`jev-mock`): hyper h2
1571  + tokio-rustls + an rcgen CA, trusted through `jev.ca_file` and
1572  answering from a closure. It serves every HTTP case (429/529 with
1573  `Retry-After`, the 400 `max_tokens_exceeded` split, the 403 on a
1574  missing key, the 8 MB cap, https enforcement). It can also observe
1575  `RST_STREAM` through a handler `Drop` guard, which httpmock cannot,
1576  so httpmock is not added as a second mock.
1577- **Cancellation proves the reset**, not merely that the query returned:
1578  - Cancel through both `pg_cancel_backend` and tokio-postgres'
1579    `CancelToken`, from a second connection to the same instance.
1580  - Assert that no socket leaks after 100 cancels.
1581- **Offline regression** runs against `jev.mock_response`
1582  (pg_typesafe's pattern).
1583- **Release gates** (ground-truth runs, not just a passing suite; each is
1584  recorded against the pinned model version):
1585  - **Layout parity:** the jev-orderby-bench harness compares row as
1586    state against row in instructions on mean |Δp|, Spearman and
1587    position effect. Row in instructions stays disabled until it passes.
1588  - **Label tree:** beam width K, lookahead, the order twin and τ are
1589    measured on a labelled hierarchy (the vendor's CPC, Shopify and MeSH
1590    sets are pinned and public), against chunk-and-shortlist.
1591  - **Unsure band:** repeat-spread on this estate's own questions
1592    replaces the provisional ±0.10.
1593  - **Ranking:** `ORDER BY jev_prob … LIMIT` passes a pairwise-inversion
1594    gate (jev-orderby-bench's ≤ 0.15). Probabilities come back at two
1595    decimals, and ties are common (53 of 360 rows at 0.99 in that
1596    bench). The README tells users to add their own tie-break column.
1597  - **Recording is approved; do not ask again.** The owner decided (planning
1598    interview, 2026-09-26) that this pass records each gate ONCE on the
1599    live key with `tools/record-gates --send`, capped by `--max-cost`, and
1600    commits the result as replay fixtures; tests replay them from then on
1601    with no further spend. Running that recording needs no further owner
1602    step. Recorded 2026-09-26 against jev-1.13.0 (`tests/fixtures/`) and
1603    replayed by `tests/gate_*.rs`. What stays owed to a later pass: the
1604    measuring benchmarks themselves, and a replay that stores N
1605    responses per request.