@arnilo/prism 0.11.0 → 0.12.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 (89) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +28 -27
  3. package/dist/cli-dev.js +1 -1
  4. package/dist/cli-init.js +4 -4
  5. package/dist/cli-provider-add.js +2 -2
  6. package/dist/field-evidence.d.ts +53 -0
  7. package/dist/field-evidence.js +143 -0
  8. package/dist/index.d.ts +3 -1
  9. package/dist/index.js +2 -1
  10. package/dist/run-bundle.js +2 -2
  11. package/docs/acp-agent.md +2 -2
  12. package/docs/ag-ui.md +1 -1
  13. package/docs/attention-compiler.md +1 -1
  14. package/docs/cli-rpc.md +2 -2
  15. package/docs/coding-tools.md +3 -3
  16. package/docs/compaction-observational-memory.md +13 -3
  17. package/docs/computer-use-linux.md +2 -2
  18. package/docs/core.md +9 -10
  19. package/docs/dev-inspector.md +1 -1
  20. package/docs/disaster-recovery.md +1 -1
  21. package/docs/document-extraction.md +140 -0
  22. package/docs/evaluations.md +1 -1
  23. package/docs/execution-timeline.md +30 -0
  24. package/docs/guardrails.md +53 -0
  25. package/docs/history/130-cyclic-workflows-primitive-review.md +293 -0
  26. package/docs/history/132-document-ingestion-primitive-review.md +124 -0
  27. package/docs/history/README.md +2 -0
  28. package/docs/history/migrate-to-0.12.0.md +57 -0
  29. package/docs/history/retire-node-runtime.md +59 -0
  30. package/docs/hooks.md +1 -1
  31. package/docs/host-compositions.md +2 -2
  32. package/docs/host-security.md +1 -1
  33. package/docs/index.md +30 -25
  34. package/docs/live-testing.md +1 -1
  35. package/docs/mcp-tools.md +2 -2
  36. package/docs/messaging-channels.md +1 -1
  37. package/docs/migrate-to-0.7.md +1 -1
  38. package/docs/migrate-to-0.8.md +2 -2
  39. package/docs/migrate-to-0.9.md +1 -1
  40. package/docs/migration.md +2 -2
  41. package/docs/model-registry.md +1 -1
  42. package/docs/model-routing.md +21 -0
  43. package/docs/multi-agent-patterns.md +77 -4
  44. package/docs/obscura.md +1 -1
  45. package/docs/openapi-tools.md +1 -1
  46. package/docs/operations.md +2 -2
  47. package/docs/peer-dependencies.md +18 -17
  48. package/docs/performance.md +32 -32
  49. package/docs/prompt-registry.md +1 -1
  50. package/docs/provider-packages.md +23 -21
  51. package/docs/providers/decisions.md +134 -0
  52. package/docs/providers/laya.md +116 -0
  53. package/docs/providers/typesafe.md +148 -0
  54. package/docs/rag.md +17 -5
  55. package/docs/release-and-install.md +119 -113
  56. package/docs/run-bundle.md +4 -0
  57. package/docs/runs-and-usage.md +1 -1
  58. package/docs/signal-channel.md +1 -1
  59. package/docs/sqlite-persistence.md +5 -5
  60. package/docs/synapta-integration-review.md +238 -0
  61. package/docs/telegram-channel.md +1 -1
  62. package/docs/testing.md +33 -14
  63. package/docs/wiki.md +12 -11
  64. package/docs/work-sandbox.md +2 -0
  65. package/docs/work-tools.md +3 -3
  66. package/docs/workflows.md +213 -11
  67. package/package.json +22 -21
  68. package/templates/business-worker/README.md.tmpl +3 -3
  69. package/templates/business-worker/package.json.tmpl +4 -5
  70. package/templates/business-worker/src/tests/agent.test.ts.tmpl +1 -1
  71. package/templates/business-worker/tsconfig.json.tmpl +2 -1
  72. package/templates/deep-research/README.md.tmpl +4 -4
  73. package/templates/deep-research/package.json.tmpl +4 -5
  74. package/templates/deep-research/src/tests/research.test.ts.tmpl +1 -1
  75. package/templates/deep-research/tsconfig.json.tmpl +2 -1
  76. package/templates/init/README.md.tmpl +3 -3
  77. package/templates/init/optional/evals-example.ts.tmpl +1 -1
  78. package/templates/init/optional/workflows-example.ts.tmpl +1 -1
  79. package/templates/init/package.json.tmpl +4 -5
  80. package/templates/init/src/tests/agent.test.ts.tmpl +1 -1
  81. package/templates/init/tsconfig.json.tmpl +2 -1
  82. package/templates/personal-assistant/README.md.tmpl +3 -3
  83. package/templates/personal-assistant/package.json.tmpl +4 -5
  84. package/templates/personal-assistant/src/tests/agent.test.ts.tmpl +1 -1
  85. package/templates/personal-assistant/tsconfig.json.tmpl +2 -1
  86. package/templates/provider/README.md.tmpl +2 -2
  87. package/templates/provider/package.json.tmpl +3 -4
  88. package/templates/provider/src/tests/provider.test.ts.tmpl +1 -1
  89. package/templates/provider/tsconfig.json.tmpl +1 -1
@@ -0,0 +1,148 @@
1
+ # TypeSafe Jev provider package
2
+
3
+ ## What it does
4
+
5
+ `@arnilo/prism-providers/typesafe` registers the TypeSafe Jev decision model. Jev does not generate free text, stream, or call tools. A request is valid only with `options.structuredOutput`: the JSON schema compiles to System One questions, the messages become the state, and one `POST /v1/systemone` round trip returns typed answers rendered as schema-valid JSON.
6
+
7
+ The package registers provider `typesafe`, models `jev-latest` and `jev-preview`, and an `api_key` auth method through `createExtensionKernel().load([...])`. Versioned pins (`jev-1.13.0`) go through `defineTypeSafeModel`.
8
+
9
+ For raw probabilities, confidence, and the actual responding checkpoint instead of rendered JSON, use the shared [`decisions`](decisions.md) subpath (re-exported from this package); the adapter deliberately does not copy those values onto events.
10
+
11
+ ## When to use it
12
+
13
+ Use it when a host wants a hosted yes/no, pick-one, or rubric decision and already has a JSON object schema. Do not use it as a general chat model, a tool-calling agent, or a streaming text model.
14
+
15
+ ## Inputs / request
16
+
17
+ ```ts
18
+ import {
19
+ createTypeSafeProvider,
20
+ createTypeSafeProviderPackage,
21
+ defineTypeSafeModel,
22
+ TYPESAFE_API_KEY_ENV,
23
+ TYPESAFE_DEFAULT_BASE_URL,
24
+ typeSafeModels,
25
+ } from "@arnilo/prism-providers/typesafe";
26
+ ```
27
+
28
+ | Field | Type | Purpose |
29
+ | --- | --- | --- |
30
+ | `apiKey` | `CredentialValueSource` | Bearer token. Hosts wire `TYPESAFE_API_KEY` through the credential seam. The library does not read `process.env`. |
31
+ | `fetch` | `typeof fetch` | Optional fetch for tests and hosts. |
32
+ | `baseUrl` | `string` | Overrides `https://api.typesafe.ai`. Trailing slashes are trimmed. `/v1/systemone` is appended per request. |
33
+ | `id` | `string` | Overrides the provider id (default `typesafe`). |
34
+ | `models` | `readonly ModelConfig[]` | Overrides `typeSafeModels`. |
35
+ | `maxRetries` | `number` | Retries after the first attempt. Default 2. Forwarded to the shared client. |
36
+
37
+ `generate()` accepts a normal `ProviderRequest` with these constraints:
38
+
39
+ | Field | Requirement |
40
+ | --- | --- |
41
+ | `options.structuredOutput` | Required. `{ name, schema }`. Missing schema fails before any fetch. |
42
+ | `tools` | Rejected. Decision model, no tool support. Fails before any fetch. |
43
+ | `options.compat.boolean_threshold` | Optional number in `0..1`. Default `0.5`. Out of range fails before any fetch. A boolean is true only when the noul probability is strictly greater than the threshold, so `0.5` renders `false` at the default. |
44
+ | `messages` | State only. A single user text message is sent as a string; otherwise the client sends `[{ role, text }]` and drops non-text parts. Do not write the question into the state — the model judges that text instead of answering it. |
45
+ | `model.model` | Sent verbatim. The responding version in the API body is not rewritten into the output. |
46
+ | `signal` | A signal already aborted throws before fetch. Abort during the round trip rejects the request. |
47
+
48
+ Schema mapping (unsupported shapes throw before any network call and name the field path):
49
+
50
+ | JSON Schema | Question |
51
+ | --- | --- |
52
+ | `boolean` | `noul`. Instructions come from `description`, then `title`, then the field path. |
53
+ | string `enum` of 2–255 values | `choice`. Criteria are the option labels unless `x-systemone.options` (or the `x-typesafe` alias) supplies descriptions. |
54
+ | integer `enum` equal to `0..N-1` with 2–10 described levels | `score`. Levels come from `x-systemone.levels` (alias `x-typesafe`), aligned to the declared enum order and sent in numeric order. An undescribed whole-number enum stays a `choice`. |
55
+ | nested object | One question per leaf, id `outer.inner`. |
56
+ | string, unbounded number, array, union, empty object | Rejected. |
57
+
58
+ Extension keys live on the property:
59
+
60
+ ```json
61
+ { "type": "boolean", "x-systemone": { "criteria": { "true": "destructive", "false": "safe" } } }
62
+ { "type": "string", "enum": ["run", "reject"], "x-systemone": { "options": { "run": "safe", "reject": "destructive" } } }
63
+ { "type": "integer", "enum": [0, 1, 2], "x-systemone": { "levels": ["opaque", "partial", "actionable"] } }
64
+ ```
65
+
66
+ Limits rejected up front: more than 255 choice options, more than 10 score levels, more than 256 questions. Field names that contain a dot are rejected so dotted nesting stays unambiguous.
67
+
68
+ Curated models declare `capabilities: { input: ["text"], output: ["text"], tools: false, streaming: false, structuredOutput: "json_schema" }`, `limits: { contextWindow: 32000, maxOutputTokens: 0 }`, and `cost: { input: 0.04, output: 0, currency: "USD", unit: "per_million_tokens" }`.
69
+
70
+ ## Outputs / response / events
71
+
72
+ | Event | Behavior |
73
+ | --- | --- |
74
+ | `message_start` | One, before the answer. |
75
+ | `content_delta` | One text block. The text is `JSON.stringify` of the schema-shaped object. |
76
+ | `usage` | `inputTokens` from the API. `outputTokens` is `0`. `totalTokens` equals `inputTokens`. Omitted when the API sends no usage. |
77
+ | `done` | `stopReason: "end_turn"`, same usage when present. |
78
+ | `error` | Terminal event for gate failures, HTTP failures, and render failures. `401` is not retried. `422` is not retried and the message includes the API field detail. `429` and `5xx` (including `529`) retry, honoring `Retry-After`. |
79
+
80
+ Booleans come from noul vs `boolean_threshold`. Enums come from the choice string (whole-number options stay numbers). Rubric integers are `Math.round` of the score, half rounds up, clamped to the rubric. Nested objects are reassembled from dotted ids. Confidence, probabilities, and score distributions are not copied onto events; the text must stay schema-valid. Success bodies are validated before output: malformed answer types, missing/non-finite numeric values, probabilities outside `0..1`, and invalid token counts produce an error rather than a coerced decision. Finite rubric scores still round and clamp as described above.
81
+
82
+ ## Request/response example
83
+
84
+ ```json
85
+ {
86
+ "model": "jev-latest",
87
+ "state": "rm -rf ./build",
88
+ "questions": {
89
+ "verdict": {
90
+ "type": "choice",
91
+ "instructions": "How should this be handled?",
92
+ "criteria": { "run": "run", "reject": "reject", "ask": "ask" }
93
+ },
94
+ "irreversible": { "type": "noul", "instructions": "Would running this destroy data?" }
95
+ }
96
+ }
97
+ ```
98
+
99
+ ```json
100
+ {
101
+ "model": "jev-1.13.0",
102
+ "answers": {
103
+ "verdict": { "type": "choice", "choice": "ask" },
104
+ "irreversible": { "type": "noul", "noul": 0.91 }
105
+ },
106
+ "usage": { "input_tokens": 96, "output_tokens": 0 }
107
+ }
108
+ ```
109
+
110
+ Rendered text: `{"verdict":"ask","irreversible":true}`.
111
+
112
+ ## Implementation example
113
+
114
+ ```ts
115
+ import { createEnvCredentialResolver, createExtensionKernel } from "@arnilo/prism";
116
+ import { createTypeSafeProviderPackage, TYPESAFE_API_KEY_ENV } from "@arnilo/prism-providers/typesafe";
117
+
118
+ const kernel = createExtensionKernel();
119
+ await kernel.load([
120
+ createTypeSafeProviderPackage({
121
+ apiKey: createEnvCredentialResolver(process.env, { typesafe: TYPESAFE_API_KEY_ENV }),
122
+ }),
123
+ ]);
124
+ ```
125
+
126
+ A request must carry `options.structuredOutput`. The answer arrives as one text delta.
127
+
128
+ ## Extension and configuration notes
129
+
130
+ - Replace the catalog with `models` or register extra pins via `defineTypeSafeModel({ model: "jev-1.13.0" })`. Defaults (capabilities, 32k context, zero output tokens, $0.04/1M input) still apply.
131
+ - `compat.boolean_threshold` is the only request knob. There is no temperature, tool choice, or streaming flag.
132
+ - `baseUrl` points the same client at a proxy. It does not change the wire shape.
133
+ - No model listing runs at setup or during `generate()`.
134
+
135
+ ## Security and performance notes
136
+
137
+ - **Egress.** State text and compiled questions leave the process for TypeSafe's hosted API (`https://api.typesafe.ai` unless `baseUrl` overrides it). Do not put secrets in the state.
138
+ - **Credentials.** The bearer token is resolved once per request under the provider id; that same value is used for transport and error redaction. It is never logged.
139
+ - **Cost and latency.** One `POST /v1/systemone` round trip per structured-output request. Questions are free in parallel; state tokens are the cost. Output tokens are not billed (`output: 0`). There is no streaming and no second hop for model discovery.
140
+ - **Fail closed.** Missing schema, tools, an out-of-range threshold, and unsupported fields fail before fetch.
141
+
142
+ ## Related APIs
143
+
144
+ - [System One decisions](decisions.md): raw typed decision calls over the same wire, with probabilities, confidence, actual model, usage, and timing.
145
+ - [Laya](laya.md): same wire and schema mapping against a self-hosted `laya-serve`.
146
+ - [Structured output](../structured-output.md): `options.structuredOutput` contract this adapter requires.
147
+ - [Provider packages](../provider-packages.md): registration and auth-method shape.
148
+ - [Credentials and redaction](../credentials-and-redaction.md): `CredentialValueSource` and `createEnvCredentialResolver`.
package/docs/rag.md CHANGED
@@ -34,7 +34,7 @@ Document lifecycle:
34
34
  | `resolveReranker(config)` | Declarative reranker config (`kind: "local" \| "tei" \| "openai-compatible" \| "voyage" \| "fake" \| "none"`) → `Reranker`. |
35
35
  | `createGoogleDriveConnector({ tokenProvider, resolveAccess })` | Drive `files.list` + `changes.list` connector. Host maps permissions; watch payloads are not authorization. |
36
36
  | `DocumentLoader` / `Parser` | Small host-replaceable seams. `@arnilo/prism-memory/rag/loaders` and `/rag/parsers` export reference adapters. |
37
- | `textParser` / `markdownParser` / `htmlParser` / `pdfParser` | UTF-8 text, Markdown, script/style-stripping HTML, and uncompressed-text PDF parsers. |
37
+ | `textParser` / `markdownParser` / `htmlParser` / `pdfParser` | UTF-8 text, Markdown, script/style-stripping HTML, and uncompressed-text PDF parsers. A host parser from `createDocumentIngest` ([Document extraction](document-extraction.md)) can replace `pdfParser`; it must throw on failed OCR so `replaceDocument` does not index. Do not raise `HARD_MAX_PARSE_MS_CAP` (30 s). |
38
38
 
39
39
  Index/retrieve:
40
40
 
@@ -112,7 +112,7 @@ const result = await propagator.propagate("doc:erp-lead");
112
112
  - `propagate(sourceId)` expands the source through `_lineage.sourceIds` (`collectInvalidationIds`, depth 8) into a closed id set, tombstones **all** of it with the propagator's resolved reason (`forgotten` by default; `legal_hold` stamps `hold: true`) inside one store transaction, then runs every registered handler with `{ sourceId, ids, scope, reason, signal }`. Handlers return how many artifacts they removed (reported per `kind` in `layers`). The context `reason` is the single source of truth: a handler's own `reason` option is only the fallback for a hand-built context, so a `legal_hold` propagation cannot land in a handler's tombstones as `forgotten`.
113
113
  - Tombstones, not deletions, for derived rows: rows stay for explainability (`recall({ explain: true })` reports the invalidation), and lineage links never dangle. Handlers own physical removal (chunk rows, files, ledger entries).
114
114
  - Retrieval is belt-and-suspenders: `retrieveContext()` reads per-scope invalidations before assembly and drops any candidate whose record id, `_lineage.sourceIds`, or `_rag.sourceId` is tombstoned — so a delete that lands after the query legs read rows still returns zero hits. The split matters for direct store users: the store's own SQL predicate filters by record id and `_lineage` edge, while a source's *own* chunk rows are covered by the `_rag.sourceId` rule at the retrieval boundary (or removed physically by the `rag` handler) — a raw `store.query()` is not a recall path.
115
- - `HARD_PROPAGATION_EDGES` (4,096) is the one-pass privileged ceiling; over it the whole delete rejects (fail-closed), never a half-tombstoned document. Each store `invalidate` call carries at most `HARD_INVALIDATION_BATCH` (64) entries. On a durable store that shape holds: PostgreSQL/pgvector tombstones 1,001 rows (1,000 derived chunk rows + the source root) in **one transaction and 22 statements** (16 of them `HARD_INVALIDATION_BATCH`-sized `INSERT`s), measured at **29–155 ms** across runs on an AMD Ryzen 9 PRO 7940HS against `pgvector/pgvector:pg16` (more under parallel load) — the durable counterpart of the in-memory suite's 1k-under-2s check, and evidence rather than a gate. Re-run it with `PRISM_TEST_POSTGRES_URL=… npm run test:postgres` (`packages/memory/src/__tests__/postgres-propagation.integration.test.ts`); the leg also proves the store's own SQL predicate hides the tombstoned rows, not only the in-app guard, and that a denied propagation opens no transaction at all.
115
+ - `HARD_PROPAGATION_EDGES` (4,096) is the one-pass privileged ceiling; over it the whole delete rejects (fail-closed), never a half-tombstoned document. Each store `invalidate` call carries at most `HARD_INVALIDATION_BATCH` (64) entries. On a durable store that shape holds: PostgreSQL/pgvector tombstones 1,001 rows (1,000 derived chunk rows + the source root) in **one transaction and 22 statements** (16 of them `HARD_INVALIDATION_BATCH`-sized `INSERT`s), measured at **29–155 ms** across runs on an AMD Ryzen 9 PRO 7940HS against `pgvector/pgvector:pg16` (more under parallel load) — the durable counterpart of the in-memory suite's 1k-under-2s check, and evidence rather than a gate. Re-run it with `PRISM_TEST_POSTGRES_URL=… bun run test:postgres` (`packages/memory/src/__tests__/postgres-propagation.integration.test.ts`); the leg also proves the store's own SQL predicate hides the tombstoned rows, not only the in-app guard, and that a denied propagation opens no transaction at all.
116
116
  - Deletion is privileged: `authorization` is required, tenant-checked, and enforced through the store's existing `checkSourceAccess` ACL when the store declares `authorization: "acl"` (missing grant → `MemoryScopeError` before anything is written). Retrieval paths never construct a propagator.
117
117
  - Observational memory registers its own leg: `createObservationalMemoryDropHandler({ session, appendEntry })` (from `@arnilo/prism-memory/compaction/observational-memory`) folds the session ledger once per propagation and writes one `om.observations.dropped` entry for the observations that rest on a tombstoned record id; see [observational memory](compaction-observational-memory.md).
118
118
 
@@ -152,6 +152,18 @@ const blocks = buildObservationalMemoryContextBlocks(entries, { invalidatedIds:
152
152
  - One `listInvalidatedIds` read per projection build (one scope read, `corrected` entries stay), and the recipe adds no work beyond the propagator: the same `layers` result already answers per-leg counts, so nothing is re-read to report it.
153
153
  - The fabric leg is the one that cannot be left out: a note names its document by `metadata.path`, so no `_lineage` edge exists to walk and a deleted path would otherwise keep being served. `createFabricRepointHandler()` tombstones the notes recorded against the deleted id in the same pass (plan 102 Task 11), and the same handler follows a `repointSource()` move — see the re-point section below.
154
154
 
155
+ ## Revocation through derived context
156
+
157
+ Grant rechecks and tombstones cover retrieval; derived context (observational memory, wiki pages, fabric notes) needs the propagation pass above. `examples/revocation-propagation.ts` runs the whole chain offline on in-memory stores:
158
+
159
+ 1. **Access loss for one principal** (`setSourceAccess` with the grant narrowed) excludes the source for that principal, still serves it to another, keeps the rows, and writes no tombstone.
160
+ 2. **Deletion** (`createDeletionPropagator` + `createRagDeletionHandler`) tombstones the source, deletes its chunk rows, and excludes it for every principal — including one that still held a grant.
161
+ 3. **The observational-memory leg runs in two observable steps:** `listInvalidatedIds(store, scope)` feeds `invalidatedIds` into `buildObservationalMemoryContextBlocks()` so a revoked observation is withheld before the drop entry lands, and `createObservationalMemoryDropHandler()` later writes the `om.observations.dropped` entry; both paths render the same memory.
162
+ 4. **A revoke landing while a reranker runs** is withheld by the post-rerank gate and reported through `onAccessDenied` (`no_grant`), so a mid-flight grant change cannot leak.
163
+ 5. **A thrown grant check** fails closed as `check_failed` with the store error redacted and capped at 256 chars; the query still completes with the remaining hits.
164
+
165
+ The example also keeps the disclosure boundary honest: a report generated before the revoke did contain the authorized text, and revocation prevents subsequent exposure — it does not claim to erase prior external disclosure.
166
+
155
167
  ## Grant recheck and re-pointing
156
168
 
157
169
  Retrieval never trusts a grant snapshot. `retrieveContext()` re-asks the store for **each distinct source** it is about to inject, on both sides of the reranker:
@@ -260,10 +272,10 @@ const result = await retrieveContext("How do approvals work?", { embedder, store
260
272
  ```
261
273
 
262
274
  - `resolveReranker({ kind: "local" })` is the zero-config path. The model runtime is a host seam exactly like `Embedder`: `createLocalReranker({ model?, runtime?, onLoad?, cacheDir?, dtype?, device?, allowRemoteModels? })`. Pass `runtime: { load(model) → { id, score({ query, documents, signal }) } }` to inject a runtime the host already owns (transformers.js, onnxruntime-node, llama.cpp). With no `runtime`, the built-in loader resolves `@huggingface/transformers` at first use — the package declares no inference dependency (no new dependency name in any manifest) and nothing resolves it at build/install time.
263
- - Sizing trade-off: the download is one-time and host-cached, and per-query latency is CPU-bound and grows with candidates × tokens, so keep `topK`/`queryCandidates` near what recall actually needs — the reranker reorders what retrieval returned, it cannot recover a chunk the candidate pool never returned. Measured on one corpus (24 queries / 96 chunks: one answering chunk + three mention-only chunks per query, k=5, `Xenova/bge-reranker-base` q8 on x86 CPU, vector-only) with two embedders: the deterministic lexical `createHashEmbedder` baseline gives recall@5 **0.21 → 0.79** and recall@20 **0.63** at the package default 20-candidate pool, while the semantic `Xenova/all-MiniLM-L6-v2` q8/cpu (384 dims) gives **0.79 → 0.79** and recall@20 **1.00** — the semantic baseline starts at the lexical reranked number, the reranker's lift is 0.000 on this corpus, and the pool is not the binding constraint. Read the row for your own embedder: with a lexical/deterministic embedder the pool bound is what to raise first; with a semantic one the reranker is ordering quality only and the residual misses need better retrieval. Top-50 median was 95–289 ms across runs. Corpus, misses, pool-bound numbers, latency, cache state, and the semantic side-by-side live in [`docs/_evidence/phase111-reranker-semantic-recall.md`](_evidence/phase111-reranker-semantic-recall.md), with the lexical control in [`docs/_evidence/phase102-local-rerank-latency.md`](_evidence/phase102-local-rerank-latency.md); both are regenerated by `PRISM_TEST_LOCAL_RERANK=1 npm run test:live`. Treat the numbers as one data point on one machine, not a ceiling: dtype, device, and the embedder move them. The non-CPU (fp16/GPU) leg is host-provisioned — re-measure before expecting the CPU numbers to hold. The package guarantees the plumbing (one lazy load, one batched score call per rerank), not the model's speed. The hosted/TEI adapters stay for scale (higher throughput, no local RAM, no download).
275
+ - Sizing trade-off: the download is one-time and host-cached, and per-query latency is CPU-bound and grows with candidates × tokens, so keep `topK`/`queryCandidates` near what recall actually needs — the reranker reorders what retrieval returned, it cannot recover a chunk the candidate pool never returned. Measured on one corpus (24 queries / 96 chunks: one answering chunk + three mention-only chunks per query, k=5, `Xenova/bge-reranker-base` q8 on x86 CPU, vector-only) with two embedders: the deterministic lexical `createHashEmbedder` baseline gives recall@5 **0.21 → 0.79** and recall@20 **0.63** at the package default 20-candidate pool, while the semantic `Xenova/all-MiniLM-L6-v2` q8/cpu (384 dims) gives **0.79 → 0.79** and recall@20 **1.00** — the semantic baseline starts at the lexical reranked number, the reranker's lift is 0.000 on this corpus, and the pool is not the binding constraint. Read the row for your own embedder: with a lexical/deterministic embedder the pool bound is what to raise first; with a semantic one the reranker is ordering quality only and the residual misses need better retrieval. Top-50 median was 95–289 ms across runs. Corpus, misses, pool-bound numbers, latency, cache state, and the semantic side-by-side live in [`docs/_evidence/phase111-reranker-semantic-recall.md`](_evidence/phase111-reranker-semantic-recall.md), with the lexical control in [`docs/_evidence/phase102-local-rerank-latency.md`](_evidence/phase102-local-rerank-latency.md); both are regenerated by `PRISM_TEST_LOCAL_RERANK=1 bun run test:live`. Treat the numbers as one data point on one machine, not a ceiling: dtype, device, and the embedder move them. The non-CPU (fp16/GPU) leg is host-provisioned — re-measure before expecting the CPU numbers to hold. The package guarantees the plumbing (one lazy load, one batched score call per rerank), not the model's speed. The hosted/TEI adapters stay for scale (higher throughput, no local RAM, no download).
264
276
  - Host defaults: `dtype: "q8"` with `device: "cpu"` on x86 — fp32 weights are roughly 4× the download for no measurable ranking gain in this size class, and fp16/GPU is worth opting into only when the host already provisions it. Weights are cached per host: pass one `cacheDir` (e.g. `~/.cache/prism/models`) and the runtime lays out one subdirectory per model id, so a second model or a second process reuses the same files — point local embedders running through the same runtime at that directory too. With `cacheDir` omitted the runtime's own default cache applies (inside the installed package). On a cache miss the model is downloaded once into that directory and later runs stay on disk: add `allowRemoteModels: false` on an offline host to fail instead of reaching the model registry, which is exactly what the live leg's second pass proves.
265
277
  - Cheap by construction: the model loads lazily once per reranker instance, `score` is called once per rerank with every candidate (never one call per document), and `onLoad({ model, loadMs })` is the only opt-in observability — no document text is ever logged. Zero network after load; the built-in loader only touches the model registry at load time, and `allowRemoteModels: false` pins it to local files.
266
- - Failure is loud: a missing runtime, an unreachable model, or a runtime that returns no per-document scores throws a redacted `RagValidationError` naming the model and the install path (`npm i @huggingface/transformers` or pass `{ runtime }`). There is deliberately **no** silent lexical fallback.
278
+ - Failure is loud: a missing runtime, an unreachable model, or a runtime that returns no per-document scores throws a redacted `RagValidationError` naming the model and the install path (`bun add @huggingface/transformers` or pass `{ runtime }`). There is deliberately **no** silent lexical fallback.
267
279
  - `rerankHits` is unchanged and still owns the caps and the trust boundary: local scores reorder the same `RagHit` references (provenance/trust untouched), byte/ms/concurrency limits apply, and abort/timeout/malformed-score cases fail closed.
268
280
 
269
281
  ## Implementation example
@@ -385,7 +397,7 @@ Reranker adapters are probed against operator-deployed endpoints — each leg sk
385
397
 
386
398
  ```bash
387
399
  PRISM_TEST_TEI_RERANKER_URL=http://tei.svc:8080 \
388
- node --test packages/memory/dist/rag/__tests__/live.test.js
400
+ bun test packages/memory/dist/rag/__tests__/live.test.js
389
401
  ```
390
402
 
391
403
  | Env var | Purpose |