@ultimat3/ai 23.0.0 → 25.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CLAUDE.md +96 -32
  2. package/README.md +249 -75
  3. package/package.json +12 -11
  4. package/src/agent-facts.ts +9 -2
  5. package/src/agent-transcript.ts +1 -1
  6. package/src/agent.ts +43 -25
  7. package/src/budget.ts +178 -51
  8. package/src/content-blocks.ts +227 -0
  9. package/src/content-errors.ts +67 -0
  10. package/src/echo-provider.ts +102 -0
  11. package/src/embeddings.ts +40 -21
  12. package/src/error-body.ts +38 -3
  13. package/src/errors.ts +19 -11
  14. package/src/eval-baseline.ts +5 -1
  15. package/src/eval-errors.ts +3 -1
  16. package/src/evals.ts +1 -1
  17. package/src/failure-disclosure.ts +90 -0
  18. package/src/gateway.ts +73 -37
  19. package/src/hive-pool.ts +24 -16
  20. package/src/hive-result.ts +4 -1
  21. package/src/hive.ts +15 -5
  22. package/src/index.ts +66 -25
  23. package/src/llm-cache.ts +4 -3
  24. package/src/llm.ts +27 -65
  25. package/src/model-resolve.ts +79 -0
  26. package/src/models.ts +125 -111
  27. package/src/openai-body.ts +13 -2
  28. package/src/openai-messages.ts +86 -15
  29. package/src/openai-provider.ts +28 -31
  30. package/src/openai-wire.ts +11 -3
  31. package/src/pg-vector-sql.ts +11 -7
  32. package/src/pg-vector.ts +21 -10
  33. package/src/prompt-fence.ts +128 -0
  34. package/src/prompt.ts +43 -13
  35. package/src/provider.ts +82 -117
  36. package/src/rag.ts +17 -9
  37. package/src/remote-embedder.ts +15 -41
  38. package/src/respond.ts +86 -0
  39. package/src/runtime.ts +12 -14
  40. package/src/scorers.ts +14 -3
  41. package/src/tool-idempotency.ts +79 -0
  42. package/src/tools.ts +52 -39
  43. package/src/vector-scope.ts +46 -4
  44. package/src/vector.ts +117 -41
  45. package/src/wire.ts +7 -2
  46. package/src/fix-line.evals.ts +0 -35
  47. package/src/fix-line.ts +0 -27
  48. package/src/openai-models.ts +0 -84
package/CLAUDE.md CHANGED
@@ -4,7 +4,8 @@ Tier 4. May import tier 0–3: `core schema i18n money time cache seo entity pol
4
4
  query jobs realtime`. **Never** `mcp manifest render pwa ui admin testing cli`.
5
5
 
6
6
  Declared today: `action` (the primitive `llm()` returns), `cache` (semantic cache), `core`,
7
- `db` (pgvector), `jobs` (`agentJob()`), `money`, `policy`, `schema`, `time`.
7
+ `db` (pgvector), `http` (`statusFor`, so a tool result redacts a 5xx cause as a problem document
8
+ does), `jobs` (`agentJob()`), `money`, `policy`, `schema`, `time`.
8
9
 
9
10
  `jobs` is declared for `agentJob()`: the action→job bridge has to live at tier 4+.
10
11
 
@@ -14,14 +15,24 @@ rather than imported. Same contract, two wire formats — and the same *decision
14
15
  local `=== true`. An in-app agent and an external one must be offered exactly the same tools.
15
16
 
16
17
  **And the same NAME**: `toLlmTool` passes `action.name` through untouched (the export name
17
- `@ultimat3/mcp` serves); `llm()` and `agent()` `.tool()` names are verbatim too. Never derive one.
18
+ `@ultimat3/mcp` serves); `llm()` and `agent()` tool names are verbatim too. Never derive one.
19
+
20
+ **And the same TOOL**: `asProjectableAction` and mcp's `primitiveFromAction` are two copies the
21
+ tier table forces; `scripts/ai-mcp-tool-parity.test.ts` holds them equal — name, description,
22
+ exposure, the whole input schema (an idempotent action's reserved `idempotencyKey` argument,
23
+ `tool-idempotency.ts`), the policy verdict and the replay. A field one adds, the other adds.
18
24
 
19
25
  ## Owns
20
26
 
21
27
  | File | Job |
22
28
  |---|---|
23
- | `models.ts` | the model REGISTRY: `registerModel`, limits, prices, the reasoning controls each one accepts |
24
- | `provider.ts` | `Provider` interface, the request half, the money arithmetic, Anthropic + Echo |
29
+ | `models.ts` | the model REGISTRY: `registerModel`, limits, prices, the reasoning controls each one accepts — and NO row of its own |
30
+ | `model-resolve.ts` | `resolveModel` / `modelForCall` — declaration → prompt → gateway `defaultModel`, none is `X_AI_MODEL_UNRESOLVED` naming all three; `describedModel` for a published fact |
31
+ | `model-fixture.ts` | the rows this package's own suites register (`useFixtureModels()`), as an app's `models.ts` would. Not shipped (`!src/**/*-fixture.ts`) |
32
+ | `provider.ts` | `Provider` interface, the request half, the money arithmetic, `anthropicProvider()` |
33
+ | `echo-provider.ts` | `echoProvider()`, the deterministic double — split from `provider.ts` at its line ceiling |
34
+ | `content-blocks.ts` | the `image` / `document` block shapes, the screen every request runs them through, their token estimate |
35
+ | `content-errors.ts` | the `X_AI_CONTENT_UNSUPPORTED` class; its code and title belong in `errors.ts` |
25
36
  | `wire.ts` | the response half: `usage` / `stop_reason` shapes, and the SSE `MessageStream` |
26
37
  | `error-body.ts` | what a failure body SAYS (`detailOf`) and what must never survive into it (`withoutKey`) — one copy, both transports |
27
38
  | `sse.ts` | Server-Sent Events framing — protocol only, knows nothing about Anthropic |
@@ -29,12 +40,12 @@ local `=== true`. An in-app agent and an external one must be offered exactly th
29
40
  | `openai-messages.ts` | the format mapping's request half: `AiMessage` blocks → OpenAI messages, `LlmTool` → functions, `tool_choice` |
30
41
  | `openai-body.ts` | one chat-completions body, and the per-model reasoning mapping |
31
42
  | `openai-wire.ts` | its response half: one completion, and `ChatCompletionStream` (fragmented tool calls, trailing usage, `[DONE]`) |
32
- | `openai-models.ts` | the OpenAI-format price rows, registered through the same `registerModel` |
33
43
  | `gateway.ts` | routing, retries, cache, budget wiring |
34
44
  | `budget.ts` | token ledgers per request/actor/org, ALS carrier |
35
- | `prompt.ts` | `definePrompt`, content hashing, version registry |
36
- | `embeddings.ts` | `Embedder`, `HashEmbedder`, cosine helpers |
37
- | `remote-embedder.ts` | `RemoteEmbedder` — the production `/v1/embeddings` client |
45
+ | `prompt.ts` | `definePrompt`, `promptHash`, version registry |
46
+ | `prompt-fence.ts` | a template's data fences: which tag pairs enclose each slot (`promptFences`), and the closer-breaking `render` applies inside one |
47
+ | `embeddings.ts` | `Embedder`, `hashEmbedder()`, cosine helpers |
48
+ | `remote-embedder.ts` | `remoteEmbedder()` — the production `/v1/embeddings` client |
38
49
  | `evals.ts` | `defineEval`, the run, the baseline gate, prompt coverage |
39
50
  | `eval-baseline.ts` | the recorded scores: path, read/write, what counts as a regression |
40
51
  | `scorers.ts` | what a `Scorer` is, the built-in ones, and `llmJudge` |
@@ -42,10 +53,13 @@ local `=== true`. An in-app agent and an external one must be offered exactly th
42
53
  | `pg-vector.live.test.ts` | the same store against a real pgvector — DDL, fusion, scope, plan |
43
54
  | `vector-scope.ts` | the tenant + policy envelope, and the tighten-only derive rule |
44
55
  | `pg-vector-sql.ts` | every pgvector statement: DDL, upsert, cosine, FTS, RRF fusion |
45
- | `pg-vector.ts` | `PgVectorStore` — the production store |
56
+ | `pg-vector.ts` | `postgresVectorStore()` — the production store |
46
57
  | `rag.ts` | chunker, retriever, reranker, budgeted context assembler |
47
58
  | `tools.ts` | action → LLM tool definition; the `AgentTool` union and `asProjectableAction`; `runLlmToolCall` |
59
+ | `tool-idempotency.ts` | an idempotent action's reserved `idempotencyKey` tool argument — MCP's, restated (same tier), held equal by the parity test |
60
+ | `failure-disclosure.ts` | `discloseFailure` — the ONE rule for what of a throw a remote reader sees (tool result, hive member); the withheld half is logged as code, name and stack frames — never its message or cause |
48
61
  | `llm.ts` | `llm()` — the model call, declared as an `action`; and what a streamed answer must satisfy |
62
+ | `respond.ts` | the `respond` tool for one `output` (a non-object one wrapped in `{ value }`), its reader, and `parseJsonish` |
49
63
  | `llm-stream.ts` | `.stream()`'s plumbing: the sink, the ambient mark, the one-turn drive |
50
64
  | `agent.ts` | `agent()` — the tool loop, declared as an `action` |
51
65
  | `agent-transcript.ts` | what one turn leaves in the transcript: the assistant replay, the tool results, the correction |
@@ -72,12 +86,24 @@ local `=== true`. An in-app agent and an external one must be offered exactly th
72
86
  off. `Gateway.generate`/`stream` screen it at the one seam, `registerModel` screens `maxOutput`, and
73
87
  `llm()`/`agent()` screen theirs at DECLARATION.
74
88
  - **A bound is screened under the key the DECLARATION uses** (`budget.tokensPerRun` in `limitsOf`
75
- before the ledger's `request`; `retrieve()`'s own `k`; `RemoteEmbedder`'s `maxResponseBytes`).
89
+ before the ledger's `request`; `retrieve()`'s own `k`; `remoteEmbedder()`'s `maxResponseBytes`).
76
90
  - A per-call budget `derive`s from the ambient ledger and only TIGHTENS. **A derived ledger reports
77
91
  back up the chain** (every debit on every ancestor; `reserve` checks each `request` scope); the
78
- STORE is written once, by the ledger the call was made on; reservations queue on the ROOT's turnstile.
92
+ STORE is written once, by the ledger the call was made on.
93
+ - **`reserve` debits, then checks — never a read-then-write across an `await`.** The in-memory scopes
94
+ check and debit synchronously; the store's go through `BudgetStore.take(key, tokens, limit)`, which
95
+ is REQUIRED and must be atomic (no default over `spent` + `add` — that pair is the race). A refusal
96
+ at a later scope gives back what the earlier ones took.
97
+ - **The store is touched only for a scope whose ceiling is DECLARED** (`takeScope`, `meteredKeys`):
98
+ every call carries keys, so a write per key grew the default store by one entry per caller.
99
+ `memoryBudgetStore()` has no window and no eviction (eviction is a ceiling bypass); a counter back
100
+ at zero is deleted.
101
+ - **`llm()`, `agent()`, `hive()` root as `currentBudget() ?? gateway.callLedger(budgetKeysFor(ctx.actor))`**
102
+ — the gateway's ceilings, keyed `actor:<kind>:<id>` / `org:<orgId>` (none for an `undefined`/`null`/`''`
103
+ org — the `orgless` test `query`'s cache uses). `callLedger` is required on
104
+ `Gateway`; `hive()` asks `installedGateway()` so a hive of plain actions needs none.
79
105
  - **`BudgetStore` is where `actor` and `org` live; the default is per PROCESS**
80
- (`createGateway({ budgetStore })`). `add` takes a negative `tokens` for a release — a store that
106
+ (`providerGateway({ budgetStore })`). `add` takes a negative `tokens` for a release — a store that
81
107
  clamps at zero leaks the ceiling.
82
108
  - Cost is `Money` (integer minor units), rounded **up**. A budget throws `X_AI_BUDGET_EXCEEDED`
83
109
  **before** the provider call. Never truncate. List price over-reserves (no introductory prices).
@@ -88,6 +114,9 @@ local `=== true`. An in-app agent and an external one must be offered exactly th
88
114
 
89
115
  - **`llm()` returns an `action`**, never a ninth primitive; it adds only the model half: render the
90
116
  prompt, project `output` into the one `respond` tool, reserve the budget, consult the semantic cache.
117
+ - **A non-object `output` is wrapped in `{ value }` and unwrapped** (`respond.ts`) — a tool's input is
118
+ an object; never refused, because `.stream()` serves `output: t.string`. `parseJsonish` is two
119
+ `indexOf` scans, never a backtracking pattern.
91
120
  - `src/index.ts` re-exports `t` from `@ultimat3/schema` **verbatim** (`index.test.ts`).
92
121
  - One repair turn, then `X_LLM_OUTPUT_INVALID`. A refusal (a 200 with no answer) is `X_LLM_REFUSED`
93
122
  before the parse, carrying `stopDetails.category`; a truncated invalid answer is `X_LLM_TRUNCATED`.
@@ -96,7 +125,7 @@ local `=== true`. An in-app agent and an external one must be offered exactly th
96
125
  `ctx.locale` (in the unconditional half). **The default scope is the calling ACTOR**
97
126
  (`JSON.stringify([actor.kind, actor.id, actor.orgId ?? null])`); `scope` receives
98
127
  `{ input, ctx }`; widening is `scope: () => 'global'`. The instance map is bounded
99
- (`MAX_SEMANTIC_CACHE_SCOPES`).
128
+ (core's `MAX_CACHED_FORMATTERS`).
100
129
  - `cache.invalidates` is not on `llm()`: a `SemanticCache` is not a `CacheTier`. Version bump + `ttl`
101
130
  is the invalidation.
102
131
  - The gateway is ambient (`configureAi`); absent at call time is `X_AI_GATEWAY_MISSING`, never a
@@ -111,11 +140,20 @@ local `=== true`. An in-app agent and an external one must be offered exactly th
111
140
  - **Fallback is across PROVIDERS serving one model, never across models**; `GenerateResult.provider`
112
141
  is stamped by the gateway and put on the span as `llm.provider`.
113
142
  - Server-side `fallbacks` (beta) are never sent — the stable `2023-06-01` surface only.
143
+ - **A slot inside a tag pair is DATA** (`prompt-fence.ts`, #689): `render` breaks, in the ASSEMBLED
144
+ prompt, every fence closer a fenced value wrote any character of (never per value: two slots can
145
+ each hold half) — never deletes, never touches a template-only closer. The
146
+ fences are read once at `definePrompt`; no hash moves.
114
147
  - `definePrompt` refuses a re-registered version whose hash moved; the hash is over core's
115
148
  `canonicalJson` (`prompt.test.ts` pins one hash literally); an absent schema hashes as `''`.
116
149
 
117
150
  ## Invariants — providers and the gateway
118
151
 
152
+ - **The response cache key is core's `fingerprint` over the request, each tool WHOLE** (every `llm()`
153
+ tool is `respond`); a throwing `cache.set` is logged (`ai.cache.write_failed`), never a failure.
154
+ - **An unknown stop/finish reason FAILS CLOSED as `max_tokens` on both formats**, streamed or not.
155
+ - **One failure-body reader** (`error-body.ts`'s `detailOf`, bounded at `READ_LIMIT`) and one
156
+ `withoutKey` scrub — the embedder included.
119
157
  - **`attempt` collects TRANSPORT failures only**: an `UltimateError` other than
120
158
  `X_AI_PROVIDER_UNAVAILABLE` is rethrown on the spot (the same misconfiguration everywhere). Read
121
159
  with `stringField(error, 'code')`.
@@ -126,7 +164,7 @@ local `=== true`. An in-app agent and an external one must be offered exactly th
126
164
  stays UNREGISTERED on purpose.
127
165
  - **The retry SCHEDULE is core's (`backoffDelay`, `isRetryableStatus`); the LOOP is this package's** —
128
166
  core's `retry()` retries unclassified throws, and every value here is an app `Provider`'s plain
129
- object, so it would retry a 400. `gateway-backoff.test.ts`. `RetryPolicy`/`DEFAULT_RETRY` keep the
167
+ object, so it would retry a 400. `gateway-backoff.test.ts`. `RetryPolicy`/`DEFAULT_GATEWAY_RETRY` keep the
130
168
  field names an app writes.
131
169
  - Every non-2xx and every in-band `error` frame is `AiTransportError` with a real `status`.
132
170
  - **Both wire formats answer the same question the same way** (`provider-parity.test.ts`): an in-band
@@ -139,15 +177,33 @@ local `=== true`. An in-app agent and an external one must be offered exactly th
139
177
  - A tool call is emitted whole. Thinking chunks are never appended to `text`.
140
178
  - Anthropic body: no `temperature`/`top_p`/`top_k`, no `budget_tokens`, `effort` inside
141
179
  `output_config`. Model IDs are exact aliases, never date-suffixed.
180
+ - **Apps bring their own models and providers (M12, 25.0.0): no vendor choice, no vendor data.** No
181
+ default model, no catalogue row, no id list ships; the registry starts EMPTY and `resetModels()`
182
+ empties it. A model resolves declaration → prompt → `Gateway.defaultModel` through `resolveModel`
183
+ / `modelForCall` only; none is `X_AI_MODEL_UNRESOLVED` naming all three (with no gateway and nothing
184
+ declared, `X_AI_GATEWAY_MISSING` — the third place does not exist yet). `vendor-neutral.test.ts`
185
+ proves it in a fresh process; no fix line names a vendor id.
142
186
  - **`ModelId` is `string`; the catalogue is an open registry** — `modelSpec(id)` refuses an
143
- unregistered id (`X_AI_MODEL_UNKNOWN`). The built-ins register through the same `registerModel`.
144
- **Re-registering REPLACES the spec and keeps its rung** (negotiated rates). **Registration order is
145
- the ladder within a `family`**, read only by `moreCapableThan`; `X_LLM_REFUSED`'s fix names a rung
146
- ABOVE or nothing.
147
- - `AnthropicProvider.models` is its own list; `EchoProvider.models` reads the registry.
187
+ unregistered id (`X_AI_MODEL_UNKNOWN`, fix `registerModel(…)`) at `costOf`, the budget estimate and
188
+ the body builders, before a socket or a reservation. **Re-registering REPLACES the spec and keeps
189
+ its rung** (negotiated rates). **Registration order is the ladder within a `family`**, read only by
190
+ `moreCapableThan`, over the APP's rows; `X_LLM_REFUSED`'s fix names a rung ABOVE or nothing.
191
+ - **Every provider serves the app's list**: `anthropicProvider({ models })` and
192
+ `openAiProvider({ models })` require a non-empty one (`X_AI_REQUEST_INVALID`, a missing config
193
+ too); `echoProvider().models` reads the registry. **No provider has a model of its own**: a direct
194
+ call naming none is `X_AI_MODEL_UNRESOLVED` from all three — the list is what it serves, never a
195
+ fourth place (`vendor-neutral.test.ts`, one parity case).
148
196
  - **The reasoning controls are PER MODEL (`models.ts`)**: an unasked control is omitted; an asked
149
- control the model lacks is `X_AI_REQUEST_INVALID`, never dropped. Adding a model is a row in `MODELS`.
197
+ control the model lacks is `X_AI_REQUEST_INVALID`, never dropped. Adding a model is a row the app
198
+ registers. `disableThinkingUpTo: 'never'` refuses `thinking: 'disabled'` at every effort;
199
+ `disabledThinking: 'between_tools'` is the wire spelling of the same declaration
200
+ (`model-rules.test.ts`, over the fixture rows).
201
+ - **Media blocks are screened once, by `assertMediaContent`, at the top of BOTH body builders** —
202
+ malformed is `X_AI_REQUEST_INVALID`, untakeable (role, the row's `input`, the wire) is
203
+ `X_AI_CONTENT_UNSUPPORTED`; never dropped, never approximated (`provider-parity-content.test.ts`).
150
204
  - `generate()` above `STREAM_ONLY_MAX_TOKENS` runs the streaming transport.
205
+ - **Every provider and embedder is built by its factory** (`anthropicProvider()`, `echoProvider()`,
206
+ `hashEmbedder()`, `remoteEmbedder()`); the classes are type exports only (`bun run factory-names`).
151
207
  - **`openAiProvider()` is a FORMAT, not a vendor** — `baseUrl`, `auth`, `headers` are the
152
208
  differences; never a second class per vendor:
153
209
  - structured output is the `respond` tool, never `response_format`; `tool_choice` names the tool
@@ -157,17 +213,16 @@ local `=== true`. An in-app agent and an external one must be offered exactly th
157
213
  ESTIMATE when usage never arrives; subtract `cached_tokens` from `prompt_tokens`;
158
214
  - tool-call deltas merge by `tool_calls[].index`; `isComplete()` accepts `[DONE]` or a finish reason,
159
215
  except `[DONE]` with pending tool fragments, which is refused;
160
- - only `gpt-5.6-sol` / `-terra` / `-luna` are priced; `registerOpenAiModels()` re-registers after
161
- `resetModels()` (every `openai-*.test.ts` calls it in `beforeEach`);
162
- - no new `X_*` code; `AiTransportError` takes the provider's `envVar`; the key is revealed late,
216
+ - no code of its own (a media refusal is the shared `X_AI_CONTENT_UNSUPPORTED`);
217
+ `AiTransportError` takes the provider's `envVar`; the key is revealed late,
163
218
  never stored, scrubbed from `detail`.
164
219
  - **`embedBatched` enforces the `Embedder` arity per batch** (`X_AI_EMBEDDER_INVALID` with both
165
- counts). One `RemoteEmbedder` for every vendor; vectors L2-normalised; a wrong width is
220
+ counts). One `remoteEmbedder()` for every vendor; vectors L2-normalised; a wrong width is
166
221
  `X_VECTOR_DIM_MISMATCH`.
167
222
  - **A caught value is read with `renderThrowable`** (`remote-embedder.ts`, `wire.ts`,
168
223
  `openai-wire.ts`) — `scripts/error-render.ts` cannot see `catch` bindings.
169
224
  - **A caller's string is never an object KEY**: the wire tables are `Map`s; `prompt.render` and
170
- `EchoProvider`'s `replies` use `Object.hasOwn`.
225
+ `echoProvider()`'s `replies` use `Object.hasOwn`.
171
226
 
172
227
  ## Invariants — `agent()`, `hive()`, `agentJob()`
173
228
 
@@ -188,10 +243,15 @@ local `=== true`. An in-app agent and an external one must be offered exactly th
188
243
  ("superseded").
189
244
  - A tool result is rendered totally (`'null'` for `undefined`; a non-JSON result reported as such,
190
245
  never as a failure); the throw is read with `stringField`.
246
+ - **A 5xx code's cause reaches the model only when core's `hasPublicCause` allows it** (status from
247
+ `@ultimat3/http`'s `statusFor`) — else `CODE: HIDDEN_TOOL_CAUSE`, plus a branded `callerFix`. The
248
+ same verdict a problem document and `@ultimat3/mcp` give, from `failure-disclosure.ts` — the
249
+ rule a hive member's `reason` follows too. Never a second copy.
191
250
  - **`hive()` is a fan-out action** (`PRIMITIVE_FACTORIES`): `HiveResult` is a SCHEMA from the
192
251
  member's `output`; three arms (`ok`/`failed`/`skipped`) and three counters; `members` in SPLIT
193
- order with `index`; the hive never names an actor; no hive budget code (the root turnstile holds
194
- it); a member's throw is recorded via `isThrownError`/`stringField`; `skipped` reasons
252
+ order with `index`; the hive never names an actor; no hive budget code (the reservation holds
253
+ it); a member's throw is recorded through `discloseFailure` — a 5xx cause or an uncoded throw's
254
+ message becomes `HIDDEN_MEMBER_CAUSE`, logged as facts only; `skipped` reasons
195
255
  `SKIPPED_ABORTED` / `SKIPPED_NO_INPUT`; `onMemberError` is required; `concurrency` 4, `minMembers`
196
256
  2 (a below-floor split still runs serially); an empty split is `X_HIVE_EMPTY`; an aborted `ctx`
197
257
  unwinds with `X_ABORTED`.
@@ -206,15 +266,19 @@ local `=== true`. An in-app agent and an external one must be offered exactly th
206
266
  - **No fix line may name `x ai`** (planned). An eval is selected with `x test eval --filter <name>`;
207
267
  `eval-errors.test.ts` asserts every `x test <word>` is one of the six types.
208
268
  - Every eval result carries the prompt hash. An eval gates on the DROP from its recorded baseline,
209
- mean AND per case. Never-recorded is `X_EVAL_BASELINE_MISSING`, corrupt is `X_EVAL_BASELINE_INVALID`.
269
+ mean AND per case, both sides rounded as the baseline is written. Never-recorded is `X_EVAL_BASELINE_MISSING`, corrupt is `X_EVAL_BASELINE_INVALID`.
210
270
  `baseline` is `import.meta.resolve('./…')`. Every prompt is named by an eval (`X_EVAL_MISSING`, by
211
271
  prompt ID). `ULTIMATE_EVAL_RECORD=1` records; a deliberately-worse test calls `run`, never
212
272
  `assert`; `x verify` with the flag is `X_EVAL_RECORDING` and runs no eval. The gate asks for a
213
273
  recorded baseline, not only a declaration.
214
- - Retrieval is hybrid by default; no vector-only path. **`chunk()` hard-wraps**, and the overlap
215
- carry stops at `buffer.length - 1`.
216
- - `PgVectorStore` is the only production vector path (pgvector + FTS in the app's Postgres);
217
- `MemoryVectorStore` enforces the same envelope. **`pg-vector.live.test.ts` refuses to skip** on a
274
+ - Retrieval is hybrid by default; no vector-only path. **`chunk()` hard-wraps**, the overlap
275
+ carry stops at `buffer.length - 1` and yields to a next unit it does not fit beside (no chunk over
276
+ `size`); `metadata.source` is written AFTER the caller's spread. `VectorStore.prune` is required.
277
+ - **A store with no tenant bound, read inside a request whose actor has an `orgId`, is
278
+ `X_VECTOR_UNSCOPED`** (`assertTenantRead`, both stores, every read). `scope: UNSCOPED`
279
+ (`crossTenant: true`) is the named opt-in; an omitted scope is `UNBOUND`.
280
+ - `postgresVectorStore()` is the only production vector path (pgvector + FTS in the app's Postgres);
281
+ `memoryVectorStore()` enforces the same envelope. **`pg-vector.live.test.ts` refuses to skip** on a
218
282
  Postgres without the extension (CI uses `pgvector/pgvector:pg17`).
219
283
  - The distance ordering lives in an ascending raw subquery (the only shape hnsw answers), pinned by a
220
284
  plan assertion. hnsw scopes after the scan — assert rows, never the node; `analyze` after a backfill.