@arnilo/prism 0.9.0 → 0.10.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 (75) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/README.md +13 -12
  3. package/dist/agent-approval.d.ts +7 -1
  4. package/dist/agent-approval.js +15 -6
  5. package/dist/agent-run-lifecycle.js +19 -5
  6. package/dist/agent-run-state.d.ts +26 -5
  7. package/dist/agent-run-state.js +97 -1
  8. package/dist/agent-session/event-subscriber.d.ts +2 -0
  9. package/dist/agent-session/event-subscriber.js +3 -0
  10. package/dist/agent-session/session/assemble.js +156 -9
  11. package/dist/agent-session/session/persist.js +11 -5
  12. package/dist/agent-session/session/provider-round.js +54 -13
  13. package/dist/agent-session/session/tool-round.d.ts +2 -2
  14. package/dist/agent-session/session/tool-round.js +58 -5
  15. package/dist/agent-session/session/types.d.ts +20 -2
  16. package/dist/agent-session/session.d.ts +65 -4
  17. package/dist/agent-session/session.js +156 -16
  18. package/dist/context-budget.d.ts +11 -0
  19. package/dist/context-budget.js +33 -2
  20. package/dist/contracts-core/agent.d.ts +26 -5
  21. package/dist/contracts-core/extensions.d.ts +3 -0
  22. package/dist/contracts-core/guardrail-packs.d.ts +8 -3
  23. package/dist/contracts-core/loop.d.ts +36 -0
  24. package/dist/contracts-core/provider.d.ts +6 -1
  25. package/dist/contracts-core/run-limits.d.ts +10 -1
  26. package/dist/contracts-protocol.d.ts +6 -4
  27. package/dist/contracts-run-state.d.ts +37 -3
  28. package/dist/contributions.d.ts +2 -1
  29. package/dist/contributions.js +1 -0
  30. package/dist/extensions.d.ts +15 -1
  31. package/dist/extensions.js +68 -0
  32. package/dist/guardrail-packs/types.d.ts +10 -0
  33. package/dist/guardrail-packs/validation-respect.js +16 -0
  34. package/dist/guardrails.d.ts +42 -1
  35. package/dist/guardrails.js +124 -15
  36. package/dist/index.d.ts +6 -6
  37. package/dist/index.js +4 -4
  38. package/dist/middleware.d.ts +1 -1
  39. package/dist/run-bundle.d.ts +6 -1
  40. package/dist/run-bundle.js +4 -1
  41. package/dist/run-limits.js +13 -0
  42. package/dist/testing/prefix-stability-conformance.d.ts +29 -0
  43. package/dist/testing/prefix-stability-conformance.js +91 -23
  44. package/dist/tools.js +10 -3
  45. package/docs/agent-events.md +12 -8
  46. package/docs/agent-session-runtime.md +9 -6
  47. package/docs/caveman.md +1 -1
  48. package/docs/compaction-llm.md +2 -0
  49. package/docs/compaction-observational-memory.md +21 -1
  50. package/docs/durable-runs.md +4 -3
  51. package/docs/embeddings.md +5 -1
  52. package/docs/execution-timeline.md +3 -2
  53. package/docs/extensions.md +20 -3
  54. package/docs/guardrails.md +16 -6
  55. package/docs/hooks.md +282 -0
  56. package/docs/index.md +18 -15
  57. package/docs/input-and-prompt-assembly.md +1 -1
  58. package/docs/instruction-injection.md +1 -0
  59. package/docs/live-testing.md +3 -1
  60. package/docs/memory-fabric.md +28 -0
  61. package/docs/middleware-hooks.md +54 -4
  62. package/docs/migration.md +13 -0
  63. package/docs/options-index.md +3 -1
  64. package/docs/policy-and-audit.md +14 -1
  65. package/docs/prefix-stability-conformance.md +57 -7
  66. package/docs/provider-packages.md +20 -20
  67. package/docs/public-contracts.md +1 -0
  68. package/docs/rag.md +93 -6
  69. package/docs/release-and-install.md +42 -39
  70. package/docs/runs-and-usage.md +17 -8
  71. package/docs/scoped-agent-memory.md +17 -9
  72. package/docs/scoped-memory.md +138 -0
  73. package/docs/tools.md +1 -1
  74. package/docs/wiki.md +4 -2
  75. package/package.json +4 -2
@@ -115,12 +115,25 @@ Memory retrieval keeps its own audit events next to policy decisions; hosts forw
115
115
 
116
116
  | Event | Shape | When |
117
117
  | --- | --- | --- |
118
- | `rag.acl_denied` | `{ sourceId, scope: { tenantId, resourceId, threadId }, reason: "no_grant" \| "check_failed", hits, error? }` via `retrieveContext({ onAccessDenied })` | A source was withheld at the retrieval boundary: revoked/absent/version-mismatched grant, or the grant lookup threw (`error` is redacted, capped at 256 chars) |
118
+ | `rag.acl_denied` | `{ sourceId, scope: { tenantId, resourceId, threadId }, reason: "no_grant" \| "check_failed", hits, error? }` via `retrieveContext({ onAccessDenied })` | A source was withheld: revoked/absent/version-mismatched grant, the grant lookup threw (`error` is redacted, capped at 256 chars), or the store's own predicate filtered it before ranking (`hits: 0`, reported only when the host wires `onDeniedSources` through `retrieveContext`, plan 102 Task 6) |
119
119
  | `Repointed` log line + result | `repointSource()` → `{ from, to, movedChunks, rewrittenEdges, layers, batched }` | A source's grant identity moved and derived artifacts followed |
120
+ | `rag.repointed` (rename audit) | `applySourceRenames({ onRenamed })` → `{ from, to, outcome: "moved", movedChunks, rewrittenEdges, layers }` \| `{ from, to, outcome: "failed", error }` | A batch of identity moves ran: one event per rename that settled, successes and failures alike (`error` redacted and capped at 256 chars) — plan 102 Task 7 |
120
121
  | Invalidation rows | `store.invalidate()` rows (`{ id, reason: "corrected" \| "revoked" \| "forgotten" \| "legal_hold", at }`) read back by `listInvalidatedIds()` | A source was revoked/forgotten/held; tombstones stay for explainability |
121
122
 
122
123
  Events are per *source*, not per hit, and are emitted once per query. They never contain document text, grant contents, or credentials; `check_failed` messages pass through the same redactor as retrieved content. Denials are fail-closed: a source is excluded whether the grant is absent, revoked, or the lookup failed, and the query returns the remaining hits. Aborts are not denials and are never recorded as such.
123
124
 
125
+ The re-point rows are the same kind of evidence for identity moves: `repointSource()` returns its counts to the caller, and `applySourceRenames()` (plan 102 Task 7) writes a batch into a host sink through `onRenamed` — one event per rename that settled, carrying `from`/`to`, the outcome, and either the moved counts per layer or the redacted error. A failed rename is audited before the batch stops (fail-fast) or continues, and a rename that never started is not audited.
126
+
127
+ Since plan 102 Task 6 the table also covers what the **store's own predicate** withheld, which plan 089 recorded as unauditable. A store that declares `authorization: "acl"` reports the sources its `query`/`lexicalQuery` predicate filtered out when the caller opts in, and `retrieveContext()` forwards that report into the same `onAccessDenied` path (one event per source per query, `hits: 0` because no hit ever existed — the finer per-source rule stays on the query-level callback):
128
+
129
+ ```ts
130
+ // store-level: what this query's own predicate withheld, and why
131
+ await store.query({ ...scope, embedding, topK, authorization, onDeniedSources: (d) => audit.write(d) });
132
+ // → [{ sourceId: "doc:payroll", reason: "no_grant" }, { sourceId: "doc:hr", reason: "version_mismatch" }]
133
+ ```
134
+
135
+ Without the callback the store issues no extra statement and its SQL is unchanged; with it, PostgreSQL/pgvector adds one grouped anti-join (`GROUP BY source_id` over the rows the predicate refused) that measured **1.2–1.4ms** on the 23-row fixture on an AMD Ryzen 9 PRO 7940HS. Reports carry source ids and reasons only — never rows, text, grant contents, or principal ids — and never widen the predicate: a withheld source stays withheld with or without the callback.
136
+
124
137
  ## Security and performance notes
125
138
 
126
139
  - Approvals require verified `AgentIdentity`; actor fields are refs only.
@@ -14,9 +14,10 @@ Exported from `@arnilo/prism/testing/prefix-stability-conformance`:
14
14
 
15
15
  Use it when a host owns any part of prompt assembly — custom `inputBuilder`, `promptBuilder`, context providers, instruction injectors, input/prompt middleware, or an explicit `inputLayout` — and wants to prove that progressive disclosure still holds the cache prefix. The runner:
16
16
 
17
- - installs a fixture provider (no network) that loads `skills[0]` on the first turn and `skills[1]` on the second, two provider requests per turn;
17
+ - installs a fixture provider (no network) that loads `skills[0]` on the first turn and `skills[1]` on the second, two provider requests per turn — and, when the host runs an attention compiler, carries a deterministic reasoning block per skill-load round so the compiler's thinking stage has real content to fold;
18
18
  - keeps everything else in `host` exactly as production: system prompt, context providers, builders, middleware, disclosure settings;
19
19
  - measures, for each consecutive captured request, the byte-shared prefix as a fraction of the previous request and fails below `minContinuity` (default `0.95`);
20
+ - reports that fraction twice: `minContinuity` (provider-visible prefix, what the prompt cache pays for) and `cacheableContinuity` (the same measurement with the session's tail segments removed), and asserts whichever `assertOn` selects (default `providerPrefix`);
20
21
  - fails when a loaded body never reaches a provider request, so a builder that drops the tail cannot pass vacuously.
21
22
 
22
23
  ## Inputs / request
@@ -31,30 +32,44 @@ const result = await runPrefixStabilityConformance({
31
32
  context: [projectContextProvider],
32
33
  },
33
34
  skills: [skillA, skillB],
35
+ assertOn: "cacheablePrefix",
34
36
  });
35
37
  ```
36
38
 
37
39
  `PrefixStabilityConformanceOptions`:
38
40
  - `host` — the host's `AgentConfig` minus `provider`, `providerSource`, and `skills`; the runner supplies the fixture provider and fixture skill registry
39
41
  - `skills` — exactly two distinct `Skill` values with non-empty `instructions`, loaded in turn order
40
- - `minContinuity?` — minimum shared-prefix fraction between consecutive requests (default `0.95`)
42
+ - `minContinuity?` — minimum shared-prefix fraction between consecutive requests (default `0.95`); always measured against the provider-visible prefix
43
+ - `assertOn?` — `"providerPrefix"` (default) gates the run on the provider-visible prefix; `"cacheablePrefix"` gates it on the tail-aware measurement instead, for an eager or body-heavy host that deliberately re-sends bodies after the stable prefix
44
+ - `allowedResets?` — how many request pairs may break below `minContinuity` (default `0`, today's behavior). Set `1` for an assembly that folds, compacts, or evicts exactly one boundary; more resets than declared fail, and fewer fail too, because a fixture that was supposed to invalidate the prefix and never did cannot pass vacuously
41
45
  - `inputs?` — the two turn inputs (default fixed strings, so runs stay comparable across hosts)
42
46
 
43
47
  ## Outputs / response / events
44
48
 
45
- Returns `Promise<{ requests: number; minContinuity: number }>`: the captured request count (four) and the lowest shared-prefix fraction observed. Throws a plain `Error` naming the offending request pair and the measured percentage on the first violation. No events, no test runner, no network.
49
+ Returns `Promise<{ requests: number; minContinuity: number; cacheableContinuity: number; resets: readonly number[] }>`: the captured request count (four) and the two lowest shared-prefix fractions observed. Throws a plain `Error` naming the offending request pair and the measured percentage on the first violation. No events, no test runner, no network.
50
+
51
+ A gap below `minContinuity` **on the metric `assertOn` selects** is collected as a reset instead of failing inside the loop: `resets` holds the 1-based index of the request that broke (the later request of the pair, ascending). With the default `allowedResets: 0` the first reset fails the run exactly as before, now adding the observed reset list to the message; `allowedResets: 1` lets a single documented boundary (an attention fold, a compaction, a budget eviction) pass while every other pair must stay byte-stable.
52
+
53
+ Both numbers measure the same consecutive request pairs, byte for byte:
54
+
55
+ - **`minContinuity`** — the provider-visible prefix, messages **and** tool schemas as sent on the wire. This is what the prompt cache can keep paying for, and its meaning is frozen: `0.95` default, unchanged by `assertOn`.
56
+ - **`cacheableContinuity`** — the same fraction recomputed after removing this session's tail segments (loaded skill bodies, resources moved to the tail) from **both** requests of each pair. A host whose provider-visible fraction dips only because of tail bodies reads `1` here.
57
+
58
+ Tail segments are read from the session's own `tailSegments` map — the exact `Message` objects assembly appended — matched by object identity first and by serialized-value equality for a `promptBuilder` that clones messages. Nothing is added to the provider payload and no host content is pattern-matched. When no captured request carried a tail segment (a builder that renders bodies elsewhere), `cacheableContinuity` equals `minContinuity`; it is not reported as `1`.
46
59
 
47
60
  ## Request/response example
48
61
 
49
62
  ```ts
50
63
  import { runPrefixStabilityConformance } from "@arnilo/prism/testing/prefix-stability-conformance";
51
64
 
52
- const { minContinuity } = await runPrefixStabilityConformance({
65
+ const { minContinuity, cacheableContinuity, resets } = await runPrefixStabilityConformance({
53
66
  host: myAgentAssembly,
54
67
  skills: [alphaSkill, betaSkill],
55
68
  });
56
- // throws: "request 2 → 3 kept 41.2% of the previous provider prefix (minimum 95.0%)"
69
+ // throws: "request 2 → 3 kept 41.2% of the previous provider prefix (minimum 95.0%), and 1 pair(s)
70
+ // broke below it (resets [3] of 3 request pairs, allowedResets 0)"
57
71
  // when a context block or the skill catalog is recomposed in place.
72
+ // minContinuity: 0.98 (provider-visible) · cacheableContinuity: 1 (tail bodies excluded) · resets: []
58
73
  ```
59
74
 
60
75
  ## Implementation example
@@ -75,9 +90,44 @@ await runPrefixStabilityConformance({
75
90
 
76
91
  ## Extension and configuration notes
77
92
 
78
- - Loaded skill bodies and URI resources are re-sent after new transcript content by design (the tail is append-only, not immutable); a body larger than `1 - minContinuity` of the whole prompt lowers the fraction without indicating a prefix regression. Raise the fixture's stable prefix or lower `minContinuity` for body-heavy hosts.
93
+ - Loaded skill bodies and URI resources are re-sent after new transcript content by design (the tail is append-only, not immutable); a body larger than `1 - minContinuity` of the whole prompt lowers the provider-visible fraction without indicating a prefix regression. For such a host assert `assertOn: "cacheablePrefix"` (the tail-aware number stays `1`), or raise the fixture's stable prefix or lower `minContinuity`.
94
+ - A volatile leading context provider lowers **both** fractions: context is not a tail segment, so `cacheablePrefix` cannot mask a real prefix regression.
79
95
  - Prompt builders that render skill bodies outside the tail are welcome — the check measures the provider-visible prefix, not where the body sits.
80
- - Attention/tool-result folding and context-budget eviction are explicit invalidation boundaries: run this check on an assembly path that neither folds nor evicts, or expect the fold to reset the measured prefix at that turn.
96
+ - Attention/tool-result folding and context-budget eviction are explicit invalidation boundaries: with the default `allowedResets: 0` the run fails at that turn. Declare the boundary instead of loosening `minContinuity` — one fold is one reset, so a second reset, a post-fold rewrite, or a fold that never happened still fails:
97
+
98
+ ```ts
99
+ const result = await runPrefixStabilityConformance({
100
+ host: {
101
+ ...myAssembly,
102
+ // A predicate gate must settle under the stages it triggers: one that still fires after folding
103
+ // fails closed with `AttentionBudgetError` instead of reporting a reset. This one opens on a
104
+ // run's opening round while the carried request is over the floor, and the fold drops it back.
105
+ attentionCompiler: {
106
+ maxInputTokens: 4_000,
107
+ keepLast: 0,
108
+ thinkingKeepTurns: 0,
109
+ trigger: { kind: "predicate", shouldFold: (state) => state.turn === 1 && state.estimatedInputTokens >= 1_350 },
110
+ },
111
+ },
112
+ skills: [alphaSkill, betaSkill],
113
+ allowedResets: 1, // add `assertOn: "cacheablePrefix"` when tail re-sends should not count either
114
+ });
115
+ result.resets; // [3] — the request after the fold; every other pair stayed append-only
116
+ ```
117
+
118
+ - Documented invalidation boundaries — what `resets` is expected to name. Each row is pinned by a fixture in [`src/__tests__/invalidation-inventory.test.ts`](../src/__tests__/invalidation-inventory.test.ts) that asserts the boundary *position* (message index, and tool index for schemas), so a reordering of the cache-aware layout fails that suite instead of silently relocating a boundary:
119
+
120
+ | Segment | Boundary in the default `cache_aware` layout | Owner |
121
+ | --- | --- | --- |
122
+ | Per-turn instruction-injector text (`on_input`) | Message 0: merged into the leading system prompt — never moved behind the transcript | [Input and prompt assembly](input-and-prompt-assembly.md) |
123
+ | Host / injector context blocks | The context slot: after the hoisted leading system messages, before skills | [Context and skills](context-and-skills.md) |
124
+ | Observational-memory blocks (`observational-memory`, `recent-messages`) | The same context slot; re-rendering identical blocks keeps the prefix byte-identical | [Context and skills](context-and-skills.md) |
125
+ | Compaction summaries | Right after the leading system prompt while nothing user-role precedes it; a leading attachment moves the summary behind the context and skill slots | [Input and prompt assembly](input-and-prompt-assembly.md) |
126
+ | Pending tool results / current input | The suffix: a result inserts immediately before the current input, so that input is the round's boundary | [Input and prompt assembly](input-and-prompt-assembly.md) |
127
+ | `contextBudget` eviction | The first evicted group in the documented drop order (tool results → history → summaries → context → skills → attachments) | [Input and prompt assembly](input-and-prompt-assembly.md) |
128
+ | Tool-schema selection | `request.tools` only: gaining or losing a schema leaves every message byte-identical, while a changed description is a boundary at that schema | [Provider caching](provider-caching.md) |
129
+ | Attention-compiler / tool-result fold | In place at the fold frontier: the oldest stripped or stubbed row is the boundary — one reset, declared with `allowedResets` | [Attention compiler](attention-compiler.md) |
130
+ | Skill bodies and URI resources (tail) | After the transcript, append-only: a newly loaded body never invalidates the prefix and `cacheableContinuity` stays `1` | [Input and prompt assembly](input-and-prompt-assembly.md) |
81
131
 
82
132
  ## Security and performance notes
83
133
 
@@ -25,26 +25,26 @@ Do not use provider packages as a package manager, credential store, env loader,
25
25
 
26
26
  | adapter package | version |
27
27
  | --- | --- |
28
- | `@arnilo/prism-providers/ai-sdk` | 0.9.0 |
29
- | `@arnilo/prism-providers/alibaba` | 0.9.0 |
30
- | `@arnilo/prism-providers/anthropic` | 0.9.0 |
31
- | `@arnilo/prism-providers/azure` | 0.9.0 |
32
- | `@arnilo/prism-providers/bedrock` | 0.9.0 |
33
- | `@arnilo/prism-providers/clinepass` | 0.9.0 |
34
- | `@arnilo/prism-providers/commandcode` | 0.9.0 |
35
- | `@arnilo/prism-providers/deepseek` | 0.9.0 |
36
- | `@arnilo/prism-providers/google` | 0.9.0 |
37
- | `@arnilo/prism-providers/hyper` | 0.9.0 |
38
- | `@arnilo/prism-providers/kimi` | 0.9.0 |
39
- | `@arnilo/prism-providers/model-discovery` | 0.9.0 |
40
- | `@arnilo/prism-providers/neuralwatt` | 0.9.0 |
41
- | `@arnilo/prism-providers/ollama` | 0.9.0 |
42
- | `@arnilo/prism-providers/openai` | 0.9.0 |
43
- | `@arnilo/prism-providers/opencode-go` | 0.9.0 |
44
- | `@arnilo/prism-providers/openrouter` | 0.9.0 |
45
- | `@arnilo/prism-providers/vertex` | 0.9.0 |
46
- | `@arnilo/prism-providers/xai` | 0.9.0 |
47
- | `@arnilo/prism-providers/zai` | 0.9.0 |
28
+ | `@arnilo/prism-providers/ai-sdk` | 0.10.0 |
29
+ | `@arnilo/prism-providers/alibaba` | 0.10.0 |
30
+ | `@arnilo/prism-providers/anthropic` | 0.10.0 |
31
+ | `@arnilo/prism-providers/azure` | 0.10.0 |
32
+ | `@arnilo/prism-providers/bedrock` | 0.10.0 |
33
+ | `@arnilo/prism-providers/clinepass` | 0.10.0 |
34
+ | `@arnilo/prism-providers/commandcode` | 0.10.0 |
35
+ | `@arnilo/prism-providers/deepseek` | 0.10.0 |
36
+ | `@arnilo/prism-providers/google` | 0.10.0 |
37
+ | `@arnilo/prism-providers/hyper` | 0.10.0 |
38
+ | `@arnilo/prism-providers/kimi` | 0.10.0 |
39
+ | `@arnilo/prism-providers/model-discovery` | 0.10.0 |
40
+ | `@arnilo/prism-providers/neuralwatt` | 0.10.0 |
41
+ | `@arnilo/prism-providers/ollama` | 0.10.0 |
42
+ | `@arnilo/prism-providers/openai` | 0.10.0 |
43
+ | `@arnilo/prism-providers/opencode-go` | 0.10.0 |
44
+ | `@arnilo/prism-providers/openrouter` | 0.10.0 |
45
+ | `@arnilo/prism-providers/vertex` | 0.10.0 |
46
+ | `@arnilo/prism-providers/xai` | 0.10.0 |
47
+ | `@arnilo/prism-providers/zai` | 0.10.0 |
48
48
  <!-- generated:package-truth:providers end -->
49
49
 
50
50
 
@@ -157,6 +157,7 @@ Important request shapes:
157
157
  | `SessionIndex` / `SessionSearchQuery` / `SessionSearchHit` | Bounded optional session search seam (`search` / `SessionStore.searchSessions?`). Filters: workspace (`metadata.workspaceRoot`), time, provider/model, label/summary, entry-kind (`kind`, e.g. annotation search), optional FTS `query`, ownership. Hits return `sessionId` + optional `leafId` for resume, and on a text match the matched entry pointer (`entryId`, `runId`, 1-based `turn`, store `score`) with a bounded matched-text `snippet`; never credentials. Caps via `resolveSessionSearchQuery` / `DEFAULT_*` / `HARD_MAX_*` session-search constants. SQLite/Postgres index (FTS5 / `tsvector`); memory and JSONL scan linearly. |
158
158
  | `contextBudget` / `getContextBudgetReport` / `ContextBudgetError` | Opt-in assembler budget on `AssembleProviderInputOptions`; deterministic eviction; omission report in `ProviderRequest.metadata` (kinds/ids/sizes only). |
159
159
  | `AgentSession.steer` / `SteerOptions` / pending-steer caps | Mid-run enqueue into active run; optional `softInterrupt`; default 8 msgs / 64 KiB UTF-8. |
160
+ | `AgentSession.close()` | Session teardown: dispatches `session_shutdown` middleware once (idempotent), then closes every subscriber (`acrossRuns` included). `session_start` is its mirror at the first run start; both are per-session, never per-turn. See [Middleware hooks](middleware-hooks.md). |
160
161
  | `SessionSearchUnsupportedError` / `sessionSearchMode` | Memory opt-out (`sessionSearchMode: "unsupported"`); typed throw (not empty success). Memory linear caps are host-overridable via `CreateMemorySessionStoreOptions.search`; the JSONL store searches linearly with the contract default caps. |
161
162
  | `BranchRecord` / `BranchQuery` | Branch handle/leaf pointer and query filters (session, name, parent branch, leaf presence). |
162
163
  | `SessionEntryQuery` | Paginated entry filters: `sessionId`, `runId`, `parentId`, `leafId`, `kind`, timestamp range, ownership. |
package/docs/rag.md CHANGED
@@ -111,9 +111,46 @@ const result = await propagator.propagate("doc:erp-lead");
111
111
 
112
112
  - `propagate(sourceId)` expands the source through `_lineage.sourceIds` (`collectInvalidationIds`, depth 8) into a closed id set, tombstones **all** of it with reason `forgotten` inside one store transaction, then runs every registered handler with `{ sourceId, ids, scope, signal }`. Handlers return how many artifacts they removed (reported per `kind` in `layers`).
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
- - 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.
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.
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.
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
+ - 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
+
119
+ ### One wiring, every layer (host recipe)
120
+
121
+ A host composes the legs itself — no facade ships until a host asks for one, because the propagator already owns handler registration, privilege, and the lineage-closed id set:
122
+
123
+ ```ts
124
+ import { createDeletionPropagator, createMemoryVectorStore, listInvalidatedIds } from "@arnilo/prism-memory";
125
+ import { createRagDeletionHandler } from "@arnilo/prism-memory/rag";
126
+ import { createWikiDeletionHandler } from "@arnilo/prism-memory/wiki";
127
+ import { buildObservationalMemoryContextBlocks, createObservationalMemoryDropHandler } from "@arnilo/prism-memory/compaction/observational-memory";
128
+ import { createFabricRepointHandler } from "@arnilo/prism-memory/fabric";
129
+
130
+ const store = createMemoryVectorStore();
131
+ const propagator = createDeletionPropagator({
132
+ scope,
133
+ vectorStore: store,
134
+ authorization: hostVerifiedPrincipal, // required by the type; must match the scope's tenant
135
+ handlers: [
136
+ createRagDeletionHandler({ store, scope: ragScope }),
137
+ createWikiDeletionHandler({ workspaceRoot }),
138
+ createObservationalMemoryDropHandler({ session, appendEntry }),
139
+ createFabricRepointHandler({ scope, vectorStore: store }),
140
+ ],
141
+ });
142
+ const result = await propagator.propagate("docs/policy.md");
143
+ // { sourceId, ids: ["docs/policy.md", "summary:docs/policy.md"], tombstoned: 2, layers: { rag: 1, wiki: 1, observational: 1, fabric: 1 }, batched: true }
144
+
145
+ // Write path: the drop entry is the ledger's record of what the revocation retired (one append, ids only).
146
+ // Read path: a projection whose ledger was never written passes the same tombstones instead.
147
+ const blocked = await listInvalidatedIds(store, scope);
148
+ const blocks = buildObservationalMemoryContextBlocks(entries, { invalidatedIds: blocked });
149
+ ```
150
+
151
+ - Both paths keep a revoked observation out of memory, in the same rendered order: the drop entry retires every active observation whose id or `sourceEntryIds` intersect the tombstone set (`om.observations.dropped`, ids only — never observation text), and `invalidatedIds` withholds them at build time. So an id is withheld whether or not the physical drop ran, and a projection built from an older snapshot matches the post-drop one.
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
+ - 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.
117
154
 
118
155
  ## Grant recheck and re-pointing
119
156
 
@@ -136,6 +173,7 @@ await store.setSourceAccess(thread, [{ sourceId: "doc:payroll", principalIds: []
136
173
  - Fail closed, never silently: an absent/revoked/version-mismatched grant and a **thrown** store error both withhold the hits, and every withheld source is reported once through `onAccessDenied` as `{ sourceId, scope, reason: "no_grant" | "check_failed", hits, error? }` (`error` is redacted and capped at 256 chars). The query still completes with the remaining hits. Abort still aborts — it is not reclassified as a denial.
137
174
  - There is no per-request off switch: passing `authorization` is what turns the gate on, and the only knob is the audit sink. A store that declares `authorization: "acl"` without `checkSourceAccess` fails closed before ranking.
138
175
  - The store's own query/lexical predicate remains the first line of defense (unauthorized text never leaves the store); the boundary recheck also covers stores whose query leg ignores grants, and revokes that land after the query legs have read.
176
+ - Sources the store filtered *inside* its own predicate no longer go unaudited (plan 102 Task 6). An `authorization: "acl"` store reports what it withheld per query when the caller opts in — `onDeniedSources: (denials) => …` on `query`/`lexicalQuery`, carrying `{ sourceId, reason: "no_grant" | "version_mismatch" | "unknown" }` (ids and reasons only) — and `retrieveContext()` passes that report into the same `onAccessDenied` path, so a store-filtered source produces one event per query with `hits: 0`. The report never widens the predicate, and without the callback the store's SQL is unchanged: PostgreSQL/pgvector then issues no extra statement, or exactly one grouped anti-join with it (1.2–1.4ms on the 23-row protected fixture).
139
177
 
140
178
  When a source's grant identity moves (`doc:a` → `doc:b`, a document re-filed under a new source id), `repointSource()` makes the derived artifacts follow **without re-embedding**:
141
179
 
@@ -151,15 +189,63 @@ const moved = await repointSource({
151
189
  authorization: hostVerifiedPrincipal, // must admit BOTH ids on an ACL store
152
190
  handlers: [createWikiRepointHandler({ workspaceRoot })],
153
191
  });
154
- // { from, to, movedChunks, rewrittenEdges, layers: { wiki: 1 }, batched: true }
192
+ // { from, to, movedChunks, rewrittenEdges, layers: { wiki: 1 }, batched: true } (add `cursor` for the next page)
155
193
  ```
156
194
 
157
- - Chunk rows keep their text, embeddings, offsets, and generation: the row id (`doc:a#0001` → `doc:b#0001`), `_rag.sourceId`, and `_rag.citationId` are rewritten, old ids are deleted, and the whole move lands in one store transaction (`batched: true`) or not at all.
195
+ - Chunk rows keep their text, embeddings, offsets, and generation: the row id (`doc:a#0001` → `doc:b#0001`), `_rag.sourceId`, and `_rag.citationId` are rewritten, old ids are deleted, and the page lands in one store transaction (`batched: true`) or not at all — never a half-written page, and never a re-embed. A durable store keeps that promise: PostgreSQL/pgvector re-keys the chunk rows and rewrites the lineage edges in **one transaction per page**, and the grant check for both ids runs before the first transaction opens. The protected leg measures the small fixture (1 chunk row + 3 lineage edges, 3–8 ms); the 1k-row durable cost above is the propagation number.
158
196
  - Lineage edges (`_lineage.sourceIds`) on derived rows move from `from` to `to` in the same pass, so `createDeletionPropagator()` stays correct afterwards: deleting `doc:b` still tombstones the derived rows, deleting `doc:a` no longer touches them.
159
- - Privileged like deletion propagation: on a store that declares `authorization: "acl"` the caller must pass an `authorization` that admits **both** the source and the destination, and re-point never creates or copies grants — grant the destination first or the move fails closed. `HARD_REPOINT_RECORDS` (4,096) bounds one pass; over the cap nothing moves.
197
+ - Privileged like deletion propagation: on a store that declares `authorization: "acl"` the caller must pass an `authorization` that admits **both** the source and the destination, and re-point never creates or copies grants — grant the destination first or the move fails closed.
198
+ - **One call moves one page.** `HARD_REPOINT_RECORDS` (4,096) is the default `pageSize`, not a ceiling on the scope: a bigger scope returns a `cursor` and the host continues from it. Each page re-keys and rewrites inside its own transaction, so a page is atomic; the ACL check runs once per call (a resumed call re-validates rather than trusting a cached decision), and the whole scope is read per page because no store exposes a ranged read — the bound is on the write, which is where the cost was.
199
+
200
+ ```ts
201
+ let cursor: string | undefined;
202
+ let movedChunks = 0;
203
+ do {
204
+ const page = await repointSource({
205
+ scope,
206
+ vectorStore: store,
207
+ from: "doc:a",
208
+ to: "doc:b",
209
+ authorization: hostVerifiedPrincipal,
210
+ handlers: [createWikiRepointHandler({ workspaceRoot })],
211
+ pageSize: 1_000, // ≤ HARD_REPOINT_RECORDS if you want the documented ceiling
212
+ ...(cursor === undefined ? {} : { cursor }),
213
+ });
214
+ movedChunks += page.movedChunks;
215
+ cursor = page.cursor; // present while records past the page still need moving
216
+ } while (cursor !== undefined);
217
+ ```
218
+
219
+ - The cursor is an opaque token that names the last record **id** the page considered, ascending. Records an earlier page already moved no longer touch `from`, so a **stale cursor is a no-op** (no transaction opens, no handler runs, `movedChunks: 0`), and a cursor from another scope or source pair is rejected with a validation error before the store is read — a partial move is never silently completed as a different one. Re-running a completed move with the final page's cursor reports `movedChunks: 0`.
220
+ - Pages are id-ordered, not store-ordered, so a resume is deterministic even for a store that returns rows in an unstable order. A destination collision fails closed on whichever page it appears in (`MemoryValidationError`, before that page's write).
221
+ - Passing `maxRecords` keeps the plan 089 all-or-nothing posture: over that many records in the scope the call rejects instead of paging. Use it when a partial move is worse than no move; `pageSize` alone is the paged mode.
160
222
  - Row ids that already exist at the destination (other than rows of the moved source) abort the move instead of overwriting (`MemoryValidationError`).
161
223
  - `createWikiRepointHandler()` moves the wiki projection: manifest `rawSources`/`anchors`, the `sourceFileHashes` entry, every page that names the old path, the index pages, and a `Repointed` log line — no recompilation. `pathsFor(sourceId → paths)` maps ids to paths when they differ.
162
224
  - Observational memory: `listInvalidatedIds(vectorStore, scope)` returns the ids a scope currently withholds (`corrected` sources stay) — pass them as `invalidatedIds` to `buildObservationalMemoryProjection()` / recall so already-emitted blocks that rest on a revoked source go stale on the next build instead of being re-injected.
225
+ - Fabric notes (`@arnilo/prism-memory/fabric`): `createFabricRepointHandler({ scope, vectorStore })` is the same handler on **both** seams — registered for a move it rewrites `metadata.fabric.path` on `kind: "file"` notes recorded against `from` (id, text, embedding, `sourceEntryIds`, and every other field reused verbatim, so nothing is re-embedded and no `_lineage` field is invented), and registered on `createDeletionPropagator()` it tombstones the notes of a deleted path through the store's own invalidation path. Notes are store-backed metadata, not derived chunk rows, so this handler is the only path that reaches them; it reads the scope once per leg, selects on `metadata.fabric.path` (never on content), only ever touches its own scope, and reports its count as the `fabric` layer.
226
+
227
+ ### Renaming in batches
228
+
229
+ ```ts
230
+ import { applySourceRenames } from "@arnilo/prism-memory";
231
+
232
+ const { results, failures } = await applySourceRenames({
233
+ scope,
234
+ vectorStore: store,
235
+ authorization: hostVerifiedPrincipal,
236
+ renames: [
237
+ { from: "doc:a", to: "doc:b" },
238
+ { from: "doc:c", to: "doc:d" },
239
+ ],
240
+ handlers: [createWikiRepointHandler({ workspaceRoot })],
241
+ onRenamed: (event) => audit.write({ kind: "rag.repointed", ...event }),
242
+ });
243
+ ```
244
+
245
+ - A thin, audited loop over `repointSource()`: no second re-key path and no new store method. Every pair is handed over as-is, so `repointSource` re-checks its own ACLs (both ids) — nothing is cached or pre-authorized across renames — and each rename walks its own page loop, so a pair above one page still moves completely while every page keeps its one ACL check, one transaction, and one handler pass. The counts folded into the audit event are the rename's totals across pages, and a rename's result carries no `cursor`: a batch is all pages or an error.
246
+ - The batch is validated as a **set before the first store read**: duplicate ids, a chained move (`a→b` then `b→c`), an overlapping move, `from === to`, or an empty id rejects the whole call with `MemoryValidationError` and writes nothing. An empty list is a no-op, not an error.
247
+ - Fail fast by default: the first failing pair writes nothing, later pairs never start, and its original error propagates. `continueOnError: true` records it in `failures` — `{ from, to, error }` — and keeps going.
248
+ - `onRenamed` is the audit sink, called once per rename when it settles: `{ from, to, outcome: "moved", movedChunks, rewrittenEdges, layers }`, or `{ from, to, outcome: "failed", error }` — ids and counts only, never rows or text, and `error` passes through the optional `redact` before it is capped at 256 chars. A pair a fail-fast run never started is not audited; an abort stops the batch and is never recorded as a rename failure.
163
249
 
164
250
  ## Local reranker
165
251
 
@@ -174,7 +260,8 @@ const result = await retrieveContext("How do approvals work?", { embedder, store
174
260
  ```
175
261
 
176
262
  - `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.
177
- - Sizing trade-off: model download is one-time and host-cached, per-query latency is CPU-bound and grows with candidates × tokens. A bge-reranker-base class model (≈1.1 GB fp32 / ≈280 MB int8, `dtype: "q8"`) reranks top-50 in tens to low hundreds of ms on CPU dev hardware — measure it with your own runtime and weight cache, then keep `topK`/`queryCandidates` near what recall actually needs; 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).
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 the phase 102 corpus (24 queries / 96 chunks: one answering chunk + three mention-only chunks per query, k=5, `Xenova/bge-reranker-base` q8 on x86 CPU, deterministic lexical `createHashEmbedder` baseline, vector-only): recall@5 **0.21 → 0.79**, top-50 median **119–289 ms**, and at the package default 20-candidate pool recall@20 was 0.63 before reranking — corpus, misses, pool-bound numbers, latency, and cache state live in [`docs/_evidence/phase102-local-rerank-latency.md`](_evidence/phase102-local-rerank-latency.md), 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 (a semantic embedder starts higher and gains less). 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
+ - 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.
178
265
  - 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.
179
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.
180
267
  - `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.
@@ -5,28 +5,29 @@
5
5
  ## What it does
6
6
 
7
7
 
8
- Prism's current **0.9.0** line has **11 publishable manifests**: the root `@arnilo/prism` core package plus **10 workspace packages** — **19 provider adapters** (19 provider adapter subpaths inside the `@arnilo/prism-providers` family), 4 `prism-*` family packages, and 6 capability packages. (Generated by `node scripts/package-truth.mjs` → `scripts/package-truth.json` — the manifest-derived single source for counts, provider membership, umbrella closures, and profile closures.) The last lockstep cut was 0.3.0; Decision B now publishes changed packages independently inside `^0.3.0` — the plan 039 changed-package cut moved root `@arnilo/prism` and every plan-035+ changed package to **0.3.1**, and the plan 050 changed-package cut moved root plus four changed packages to **0.3.2**; the plan 041-044 changed-package cut moves root to **0.3.3** with `@arnilo/prism-memory@0.3.2` (composite recall scoring), `@arnilo/prism-evals@0.3.1` (trace-to-dataset curation), the three session-store packages at **0.3.1** (run-ledger `promptVersion` provenance), and the initial `@arnilo/prism-prompts@0.0.1` (independent opt-in, outside `prism-all`); plan 054 consolidation then folded `@arnilo/prism-browser` and `@arnilo/prism-obscura` into the `@arnilo/prism-web-tools` family as `/browser` and `/obscura` subpaths, folded `@arnilo/prism-rag`, both compaction strategies, `@arnilo/prism-graft`, and `@arnilo/prism-wiki` into the `@arnilo/prism-memory` family as `/rag`, `/compaction/llm`, `/compaction/observational-memory`, `/graft`, and `/wiki` subpaths (deleting the `@arnilo/prism-compaction` profile), and folded all 17 `@arnilo/prism-provider-*` packages into the `@arnilo/prism-providers` family as `/<adapter>` subpaths (Azure/Bedrock/Vertex stop being special all-only manifests); independent publication continues inside `^0.3.0` ranges (which satisfy 0.3.1, 0.3.2, and 0.3.3). This page describes how they are packed, what each tarball contains, how to install them, the required non-optional **caret** `@arnilo/prism@^0.9.0` peer range, the release workflow, and the offline test budget. The measurable 1.0 readiness gates (command-per-gate) live in [`0.1.0-readiness.md`](history/./0.1.0-readiness.md).
8
+ Prism's current **0.10.0** line has **12 publishable manifests**: the root `@arnilo/prism` core package plus **11 workspace packages** — **20 provider adapters** (20 provider adapter subpaths inside the `@arnilo/prism-providers` family), 4 `prism-*` family packages, and 7 capability packages. (Generated by `node scripts/package-truth.mjs` → `scripts/package-truth.json` — the manifest-derived single source for counts, provider membership, umbrella closures, and profile closures.) The last lockstep cut was 0.3.0; Decision B now publishes changed packages independently inside `^0.3.0` — the plan 039 changed-package cut moved root `@arnilo/prism` and every plan-035+ changed package to **0.3.1**, and the plan 050 changed-package cut moved root plus four changed packages to **0.3.2**; the plan 041-044 changed-package cut moves root to **0.3.3** with `@arnilo/prism-memory@0.3.2` (composite recall scoring), `@arnilo/prism-evals@0.3.1` (trace-to-dataset curation), the three session-store packages at **0.3.1** (run-ledger `promptVersion` provenance), and the initial `@arnilo/prism-prompts@0.0.1` (independent opt-in, outside `prism-all`); plan 054 consolidation then folded `@arnilo/prism-browser` and `@arnilo/prism-obscura` into the `@arnilo/prism-web-tools` family as `/browser` and `/obscura` subpaths, folded `@arnilo/prism-rag`, both compaction strategies, `@arnilo/prism-graft`, and `@arnilo/prism-wiki` into the `@arnilo/prism-memory` family as `/rag`, `/compaction/llm`, `/compaction/observational-memory`, `/graft`, and `/wiki` subpaths (deleting the `@arnilo/prism-compaction` profile), and folded all 17 `@arnilo/prism-provider-*` packages into the `@arnilo/prism-providers` family as `/<adapter>` subpaths (Azure/Bedrock/Vertex stop being special all-only manifests); independent publication continues inside `^0.3.0` ranges (which satisfy 0.3.1, 0.3.2, and 0.3.3). This page describes how they are packed, what each tarball contains, how to install them, the required non-optional **caret** `@arnilo/prism@^0.10.0` peer range, the release workflow, and the offline test budget. The measurable 1.0 readiness gates (command-per-gate) live in [`0.1.0-readiness.md`](history/./0.1.0-readiness.md).
9
9
 
10
10
  Core `@arnilo/prism` ships runtime, CLI, templates, and docs. Every code package has a required `@arnilo/prism` peer inside the Decision B window — the caret current spec is `@arnilo/prism@^0.3.3` and every declared window peer satisfies it: packages republishing in the plan 050 cut carry `^0.3.2`; the plan 039 set keeps `^0.3.1`; unchanged packages keep their `^0.3.0` peer; profiles are pure manifests. The plan 050 republished set declares the required `@arnilo/prism@^0.3.2` peer; the plan 041-044 republished set keeps its existing `^0.3.0` window peer; unchanged packages keep their prior window. Installation activates no provider, listener, database, browser, credential, or tool capability.
11
11
 
12
- The **0.9.0 lockstep cut** moved all **eleven** manifests together: the current declared peer is `@arnilo/prism@^0.9.0` on every package, and `release.mjs` lockstep mode fails closed on any internal range that merely satisfies the cut version instead of matching it. The **0.6.0, 0.7.0, and 0.8.0 lockstep cuts** each moved the then-current manifest set together. The independent-publication history above (0.3.x, 0.4.x, 0.5.x) describes how the line grew when packages moved separately.
12
+ The **0.10.0 lockstep cut** moved all **twelve** manifests together — `@arnilo/prism-hooks` is new in this cut and declares the same range: the current declared peer is `@arnilo/prism@^0.10.0` on every package, and `release.mjs` lockstep mode fails closed on any internal range that merely satisfies the cut version instead of matching it. The **0.6.0–0.9.0 lockstep cuts** each moved their then-current manifest set together. The independent-publication history above (0.3.x, 0.4.x, 0.5.x) describes how the line grew when packages moved separately.
13
13
 
14
14
  <!-- generated:package-truth:inventory begin -->
15
- **11 publishable manifests** — root `@arnilo/prism` plus 10 workspace packages (4 `prism-*` family packages, 6 capability packages). Generated by `node scripts/package-truth.mjs --emit-docs` — do not hand-edit.
15
+ **12 publishable manifests** — root `@arnilo/prism` plus 11 workspace packages (4 `prism-*` family packages, 7 capability packages). Generated by `node scripts/package-truth.mjs --emit-docs` — do not hand-edit.
16
16
 
17
17
  | package | version | notes |
18
18
  | --- | --- | --- |
19
- | `@arnilo/prism` | 0.9.0 | core — runtime, CLI/RPC, templates, docs |
20
- | `@arnilo/prism-channels` | 0.9.0 | family — transport-neutral messaging runtime, durable journal, pairing and one-use approvals; official /telegram (private DMs, opt-in granted groups/topics) and experimental pinned signal-cli /signal |
21
- | `@arnilo/prism-coding-tools` | 0.9.0 | family — /agent, /security, /openapi, /computer-use-linux, /dev, /caveman, /ponytail, /impeccable subpaths |
22
- | `@arnilo/prism-core` | 0.9.0 | family — /runtime, /sessions, /governance, /credentials, /enterprise, /validation subpaths |
23
- | `@arnilo/prism-providers` | 0.9.0 | family — all provider adapters as `/<adapter>` subpaths |
24
- | `@arnilo/prism-acp-agent` | 0.9.0 | capability — ACP adapter |
25
- | `@arnilo/prism-ag-ui` | 0.9.0 | capability — AG-UI/A2A/A2UI adapter |
26
- | `@arnilo/prism-mcp` | 0.9.0 | capability — MCP client/server/OAuth interop |
27
- | `@arnilo/prism-memory` | 0.9.0 | capability — memory plus /rag, /compaction/*, /fabric, /graft, /wiki subpaths |
28
- | `@arnilo/prism-web-tools` | 0.9.0 | capability — Brave/Exa/Firecrawl plus peer-gated /browser and /obscura subpaths |
29
- | `@arnilo/prism-work` | 0.9.0 | capability — /connectors, /documents, /sheets, /diagrams, /document-reader, /sandbox, /skills, /tools subpaths |
19
+ | `@arnilo/prism` | 0.10.0 | core — runtime, CLI/RPC, templates, docs |
20
+ | `@arnilo/prism-channels` | 0.10.0 | family — transport-neutral messaging runtime, durable journal, pairing and one-use approvals; official /telegram (private DMs, opt-in granted groups/topics) and experimental pinned signal-cli /signal |
21
+ | `@arnilo/prism-coding-tools` | 0.10.0 | family — /agent, /security, /openapi, /computer-use-linux, /dev, /caveman, /ponytail, /impeccable subpaths |
22
+ | `@arnilo/prism-core` | 0.10.0 | family — /runtime, /sessions, /governance, /credentials, /enterprise, /validation subpaths |
23
+ | `@arnilo/prism-providers` | 0.10.0 | family — all provider adapters as `/<adapter>` subpaths |
24
+ | `@arnilo/prism-acp-agent` | 0.10.0 | capability — ACP adapter |
25
+ | `@arnilo/prism-ag-ui` | 0.10.0 | capability — AG-UI/A2A/A2UI adapter |
26
+ | `@arnilo/prism-hooks` | 0.10.0 | capability — Claude/Codex-compatible hooks.json adapter compiled onto middleware, guardrail, injector, and stop-hook seams |
27
+ | `@arnilo/prism-mcp` | 0.10.0 | capability — MCP client/server/OAuth interop |
28
+ | `@arnilo/prism-memory` | 0.10.0 | capability — memory plus /rag, /compaction/*, /fabric, /graft, /wiki subpaths |
29
+ | `@arnilo/prism-web-tools` | 0.10.0 | capability — Brave/Exa/Firecrawl plus peer-gated /browser and /obscura subpaths |
30
+ | `@arnilo/prism-work` | 0.10.0 | capability — /connectors, /documents, /sheets, /diagrams, /document-reader, /sandbox, /skills, /tools subpaths |
30
31
  <!-- generated:package-truth:inventory end -->
31
32
 
32
33
 
@@ -35,26 +36,26 @@ The **0.9.0 lockstep cut** moved all **eleven** manifests together: the current
35
36
 
36
37
  | adapter package | version |
37
38
  | --- | --- |
38
- | `@arnilo/prism-providers/ai-sdk` | 0.9.0 |
39
- | `@arnilo/prism-providers/alibaba` | 0.9.0 |
40
- | `@arnilo/prism-providers/anthropic` | 0.9.0 |
41
- | `@arnilo/prism-providers/azure` | 0.9.0 |
42
- | `@arnilo/prism-providers/bedrock` | 0.9.0 |
43
- | `@arnilo/prism-providers/clinepass` | 0.9.0 |
44
- | `@arnilo/prism-providers/commandcode` | 0.9.0 |
45
- | `@arnilo/prism-providers/deepseek` | 0.9.0 |
46
- | `@arnilo/prism-providers/google` | 0.9.0 |
47
- | `@arnilo/prism-providers/hyper` | 0.9.0 |
48
- | `@arnilo/prism-providers/kimi` | 0.9.0 |
49
- | `@arnilo/prism-providers/model-discovery` | 0.9.0 |
50
- | `@arnilo/prism-providers/neuralwatt` | 0.9.0 |
51
- | `@arnilo/prism-providers/ollama` | 0.9.0 |
52
- | `@arnilo/prism-providers/openai` | 0.9.0 |
53
- | `@arnilo/prism-providers/opencode-go` | 0.9.0 |
54
- | `@arnilo/prism-providers/openrouter` | 0.9.0 |
55
- | `@arnilo/prism-providers/vertex` | 0.9.0 |
56
- | `@arnilo/prism-providers/xai` | 0.9.0 |
57
- | `@arnilo/prism-providers/zai` | 0.9.0 |
39
+ | `@arnilo/prism-providers/ai-sdk` | 0.10.0 |
40
+ | `@arnilo/prism-providers/alibaba` | 0.10.0 |
41
+ | `@arnilo/prism-providers/anthropic` | 0.10.0 |
42
+ | `@arnilo/prism-providers/azure` | 0.10.0 |
43
+ | `@arnilo/prism-providers/bedrock` | 0.10.0 |
44
+ | `@arnilo/prism-providers/clinepass` | 0.10.0 |
45
+ | `@arnilo/prism-providers/commandcode` | 0.10.0 |
46
+ | `@arnilo/prism-providers/deepseek` | 0.10.0 |
47
+ | `@arnilo/prism-providers/google` | 0.10.0 |
48
+ | `@arnilo/prism-providers/hyper` | 0.10.0 |
49
+ | `@arnilo/prism-providers/kimi` | 0.10.0 |
50
+ | `@arnilo/prism-providers/model-discovery` | 0.10.0 |
51
+ | `@arnilo/prism-providers/neuralwatt` | 0.10.0 |
52
+ | `@arnilo/prism-providers/ollama` | 0.10.0 |
53
+ | `@arnilo/prism-providers/openai` | 0.10.0 |
54
+ | `@arnilo/prism-providers/opencode-go` | 0.10.0 |
55
+ | `@arnilo/prism-providers/openrouter` | 0.10.0 |
56
+ | `@arnilo/prism-providers/vertex` | 0.10.0 |
57
+ | `@arnilo/prism-providers/xai` | 0.10.0 |
58
+ | `@arnilo/prism-providers/zai` | 0.10.0 |
58
59
  <!-- generated:package-truth:providers end -->
59
60
 
60
61
 
@@ -103,6 +104,8 @@ Consumers install the core package for the runtime and add first-party packages
103
104
  | Local mirror of the release verify gate | `npm run release:dry-run` |
104
105
  | Validate independent versions/ranges and reject registry collisions | `npm run release:check -- --allow-dirty --allow-untagged` |
105
106
  | Preview deterministic changed-package publication | `npm run release:publish -- --dry-run --allow-dirty --allow-untagged` |
107
+ | Verify published artifacts from the registry (post-publish smoke) | `npm run post-publish:smoke` |
108
+ | Run the same smoke against local `npm pack` tarballs (pre-publish parity) | `npm run post-publish:smoke -- --local` |
106
109
  | Resume interrupted package-tag publication | `npm run release:publish -- --resume --report release-artifacts/publish-report.json` |
107
110
  | Protected PostgreSQL enterprise suite | `PRISM_TEST_POSTGRES_URL="$DATABASE_URL" npm run test:postgres` |
108
111
  | Full SDK readiness gate (typecheck + offline tests + pack) | `npm run sdk:ready` |
@@ -150,7 +153,7 @@ A packed tarball contains only public compiled output and release files:
150
153
  - Code packages ship `README.md`, `LICENSE`, and `CHANGELOG.md`; family/profile packages ship `README.md` and `CHANGELOG.md`.
151
154
  - The core tarball additionally ships the full `docs/` directory (the docs hub), `templates/init/`, and the `templates/` gallery (e.g. `deep-research`) used by `prism init`.
152
155
  - `dist/cli.js` and the `bin` link in core.
153
- - **Tarball filenames.** npm strips the `@scope/` prefix, so the core package `@arnilo/prism` produces a tarball named `arnilo-prism-0.9.0.tgz`; family packages produce `arnilo-prism-core-0.9.0.tgz`, `arnilo-prism-coding-tools-0.9.0.tgz`, `arnilo-prism-providers-0.9.0.tgz` (all 19 adapters inside), `arnilo-prism-channels-0.9.0.tgz`, `arnilo-prism-memory-0.9.0.tgz`, and `arnilo-prism-web-tools-0.9.0.tgz`; capability packages like `arnilo-prism-mcp-0.9.0.tgz` and `arnilo-prism-work-0.9.0.tgz` carry their own package version. Independent-package tags carry their own version. The CLI bin name `prism` is unaffected by the package name (`npx prism` still works; npm allows the bin field to differ from the package name).
156
+ - **Tarball filenames.** npm strips the `@scope/` prefix, so the core package `@arnilo/prism` produces a tarball named `arnilo-prism-0.10.0.tgz`; family packages produce `arnilo-prism-core-0.10.0.tgz`, `arnilo-prism-coding-tools-0.10.0.tgz`, `arnilo-prism-providers-0.10.0.tgz` (all 20 adapters inside), `arnilo-prism-channels-0.10.0.tgz`, `arnilo-prism-memory-0.10.0.tgz`, and `arnilo-prism-web-tools-0.10.0.tgz`; capability packages like `arnilo-prism-hooks-0.10.0.tgz`, `arnilo-prism-mcp-0.10.0.tgz`, and `arnilo-prism-work-0.10.0.tgz` carry their own package version. Independent-package tags carry their own version. The CLI bin name `prism` is unaffected by the package name (`npx prism` still works; npm allows the bin field to differ from the package name).
154
157
 
155
158
  Excluded from every tarball by `files` negation:
156
159
 
@@ -257,7 +260,7 @@ Frozen by Phase 12 Task 0 in `scripts/phase12-freeze-manifest.json` (schema gate
257
260
  ## Extension and configuration notes
258
261
 
259
262
 
260
- - **Required `@arnilo/prism` peer.** Every first-party code package declares a non-optional **caret** `@arnilo/prism@^0.9.0` peer (the lockstep 0.9.0 cut rewrote every internal range; the version-literal gate rejects a declared range that only satisfies the cut version) (`peerDependenciesMeta` must not mark `@arnilo/prism` optional; other peers such as `playwright-core` may be optional). **Peer-version policy (plan 030, Decision B — independent packages):** internal ranges stay inside the caret window of the cut they shipped in, so a package may patch independently while consumers remain on a compatible 0.x line. A package outside that window is refused by the release gate until the next coordinated peer bump. Inside the workspace each package also declares `"@arnilo/prism": "file:../.."` in `devDependencies` so `npm install` resolves the peer locally; that devDependency is stripped from consumer installs and is not a runtime dependency.
263
+ - **Required `@arnilo/prism` peer.** Every first-party code package declares a non-optional **caret** `@arnilo/prism@^0.10.0` peer (the lockstep 0.10.0 cut rewrote every internal range; the version-literal gate rejects a declared range that only satisfies the cut version) (`peerDependenciesMeta` must not mark `@arnilo/prism` optional; other peers such as `playwright-core` may be optional). **Peer-version policy (plan 030, Decision B — independent packages):** internal ranges stay inside the caret window of the cut they shipped in, so a package may patch independently while consumers remain on a compatible 0.x line. A package outside that window is refused by the release gate until the next coordinated peer bump. Inside the workspace each package also declares `"@arnilo/prism": "file:../.."` in `devDependencies` so `npm install` resolves the peer locally; that devDependency is stripped from consumer installs and is not a runtime dependency.
261
264
  - **Public access.** All 56 manifests (root + 55 workspace packages: 49 code packages + 6 pure-manifest family/profile packages — the 10 `prism-*` family/profile set is the 6 pure-manifest profiles plus the 4 code packages `prism-caveman`, `prism-impeccable`, `prism-openapi-tools`, `prism-ponytail`) declare `"publishConfig": { "access": "public" }`; the publisher also passes `--access public` explicitly because scoped packages otherwise default to restricted on first publish.
262
265
  - **Shipped vs repository docs.** The npm tarball ships `docs/` pages linked from `docs/index.md` (public API, security, migration, providers, install). It excludes `docs/_evidence/` (per-phase evidence freezes, including `release-0.2.7-evidence.md`), `docs/release-*-evidence.md`, and `docs/api-page-template.md`. Those files remain in git for audit. `dist/__tests__` and `*.map` stay excluded.
263
266
  - **Map retention knob.** Source maps are emitted locally but stripped from tarballs by `!dist/**/*.map`. Removing that `files` negation ships maps in releases (larger tarballs, better consumer stack traces).
@@ -282,7 +285,7 @@ Frozen by Phase 12 Task 0 in `scripts/phase12-freeze-manifest.json` (schema gate
282
285
  ## Security and performance notes
283
286
 
284
287
 
285
- - **Export-count budget.** `scripts/budget-gate.test.mjs` counts each publishable package's public exports (same name classes as `scripts/dead-exports.mjs`) and fails CI when any exceed the `exportCounts` ceilings in `scripts/budgets.json`; the failure names the package and the exact delta. Ceilings are the 0.9.0 pre-release baselines (plan 099 Task 0, measured 2026-09-19): `@arnilo/prism` 1445 and `@arnilo/prism-memory` 892, every other ceiling unchanged since its recorded rebaseline. Each raise carries its measured value and the plans that caused it, and `docs/_evidence/phase54-package-map.md` records the same per-package count in its Budget-Gated Exports column. Growth requires removing exports or rebaselining with a recorded reason.
288
+ - **Export-count budget.** `scripts/budget-gate.test.mjs` counts each publishable package's public exports (same name classes as `scripts/dead-exports.mjs`) and fails CI when any exceed the `exportCounts` ceilings in `scripts/budgets.json`; the failure names the package and the exact delta. The 0.10.0 cut carries `@arnilo/prism` 1456 and `@arnilo/prism-memory` 934 (the 0.9.0 pre-release baselines were 1445 and 892, plan 099 Task 0 measured 2026-09-19; the +11 root raise is logged for plans 103, 104, and 106 and the +42 memory raise for plans 102 and 105, per-entry in `scripts/budgets.json#exportCounts`), and every other ceiling is unchanged since its recorded rebaseline. Each raise carries its measured value and the plans that caused it, and `docs/_evidence/phase54-package-map.md` records the same per-package count in its Budget-Gated Exports column. Growth requires removing exports or rebaselining with a recorded reason.
286
289
  - **Artifact diet.** The same gate re-packs the root tarball and fails if packed bytes, unpacked bytes, or file count exceed `scripts/budgets.json#root` + 5%; the 0.9.0 pre-release baselines are 1414295 packed / 4647338 unpacked / 533 files (measured 2026-09-19, plan 099 Task 0). Tests, fixtures, plans, scripts, `src/`, and `docs/_evidence/**` stay out of the pack (plan 026 rule), and every page linked from shipped `docs/index.md` must be present.
287
290
  - **No secrets or fixtures in tarballs.** Tests, fixtures, `src/`, `plans/`, `.agents/`, `roadmap.md`, and `tsconfig` files are excluded. The `docs avoid real-looking secret examples` docs check and the packaging guard's deny list prevent secret-bearing fixtures from shipping.
288
291
  - **Live tests stay opt-in.** The default `npm test` is network-free by construction and never sets these vars. Provider/compaction live gates stay credential-gated and are not set by default or during `sdk:ready`. The PostgreSQL adapter live matrix is the exception that runs in CI via the dedicated `postgres-integration` job (still skipped in the default suite).
@@ -341,7 +344,7 @@ Workspace coverage rows used to include the symlinked root core `dist/` (workspa
341
344
  | Fact | Value |
342
345
  | --- | --- |
343
346
  | Workspace include filter | `--test-coverage-include=dist/**` per package (package-local denominator) |
344
- | Workspace discovery | any `*.test.js` under `packages/<name>/dist/**`, nested layouts included (`@arnilo/prism-acp-agent` builds to `dist/src/__tests__`, `@arnilo/prism-work` to `dist/<area>/__tests__`) — all 10 workspace packages are measured and artifact keys must match their live manifest names |
347
+ | Workspace discovery | any `*.test.js` under `packages/<name>/dist/**`, nested layouts included (`@arnilo/prism-acp-agent` builds to `dist/src/__tests__`, `@arnilo/prism-work` to `dist/<area>/__tests__`) — all 11 workspace packages are measured and artifact keys must match their live manifest names |
345
348
  | Per-package gate | `lines >= threshold` from `scripts/coverage-thresholds.json` (recaptured 2026-09-11 = min of two back-to-back runs − 3pp; the two runs differed by ≤ 0.02pp); branches/functions recorded, not gated. `phase23-coverage` also fails when a row names a package that is not in the live workspace graph — retired rows must be pruned |
346
349
  | Protected exceptions | `@arnilo/prism-core` only (durable postgres/NATS legs need `PRISM_TEST_POSTGRES_URL` / `PRISM_TEST_NATS_URL`); exempt from the gate and reported separately with the reason. Env- or capability-gated legs elsewhere (memory postgres, coding-tools native sandbox, provider live legs) skip as protected skips but their packages stay gated on the measured run |
347
350
  | Artifact | `scripts/coverage-summary.json` (gitignored, CI-retained): its package-key set must exactly equal live workspace `package.json` names; each row carries `lines`/`branches`/`functions`/`denominatorFiles`/`threshold`/`pass`/`protectedException` + `belowThreshold`; a row whose child failed additionally carries `status`/`exitCode`/`tail` (a redacted tail of the child's output) |
@@ -454,7 +457,7 @@ Every release gate maps to an exact enforcement test or command, so the checklis
454
457
  | NeuralWatt package/docs/examples release gate | `packaging.test.ts` pins `@arnilo/prism-providers/neuralwatt` package exports/type declarations and `@arnilo/prism-providers` family membership; `docs.test.ts` asserts `docs/index.md` links `providers/neuralwatt.md` and `provider-caching.md`, and that `examples/cache-aware-prompt-assembly.ts` plus `examples/neuralwatt-agent-run.ts` exist and are listed. |
455
458
  | Enterprise PostgreSQL package/docs/example gate | Packaging/install/public-contract tests include `@arnilo/prism-core/enterprise/postgres`; `docs.test.ts` pins its API page, four-store migration/ownership/unknown-outcome/async-router guidance, and `examples/enterprise-postgres-state.ts`; `npm run test:postgres` exercises migration, restart, contention, and cleanup with an explicit database URL. |
456
459
  | Version graph and resumable publication | `release.test.ts` covers exact package/lock/range validation, topological order, registry collisions, dry-run, interrupted reports/resume, clean tagged git state, provenance/public/tag arguments, and token-safe errors. `release:check` and `release:publish` derive the workspace graph without a manual package list. |
457
- | Release version claims have one source | `currentVersion()` (`scripts/package-truth.mjs`) reads the root manifest, and `scripts/version-literal-gate.test.mjs` fails when any surface that claims the release version disagrees with it: all 10 manifests, every internal `@arnilo/*` caret range, `package-lock.json`, the `src/index.ts` version constant, the `docs/index.md` current-line banner, the `release.yml` tag list, and the generated `scripts/package-truth.json`. Each stale surface is named in one pre-flight (with a positive control for a half-finished cut), and the release-line suites (`packaging.test.ts`, `docs.test.ts`, `release.test.ts`, `phase24-truth`, `phase34-freeze`) derive their expectations from the manifest instead of pinning the cut. |
460
+ | Release version claims have one source | `currentVersion()` (`scripts/package-truth.mjs`) reads the root manifest, and `scripts/version-literal-gate.test.mjs` fails when any surface that claims the release version disagrees with it: all 12 manifests, every internal `@arnilo/*` caret range, `package-lock.json`, the `src/index.ts` version constant, the `docs/index.md` current-line banner, the `release.yml` tag list, and the generated `scripts/package-truth.json`. Each stale surface is named in one pre-flight (with a positive control for a half-finished cut), and the release-line suites (`packaging.test.ts`, `docs.test.ts`, `release.test.ts`, `phase24-truth`, `phase34-freeze`) derive their expectations from the manifest instead of pinning the cut. |
458
461
  | Pre-publish compatibility gates | `release:gate` (in `sdk:ready`) fails on removed/changed `.d.ts` exports vs `scripts/compat-baseline/` (unless `--allow-break` + migration note), version-range/lockfile drift, and tarball deny-list violations (`plans/`, `code-reviews/`, `docs/review-coverage-*`, `*.map`, `__tests__/`); unit-tested in `scripts/release-gate.test.mjs`. |
459
462
  | Legacy registry markers (plan 054 Task 7) | `scripts/phase54-legacy-registry.mjs --dry-run` verifies every retired name's final published version exists and `latest` is unchanged, and that each deprecation URL anchor exists in `docs/migrate-to-0.4.md`, without mutating the registry; `--apply --confirm` pre-flights all 54 entries and fails closed (zero mutations) on any mismatch, then idempotently adds the `legacy` dist-tag and `<0.4.0` deprecation warning (already-correct entries skipped; per-entry status in `release-artifacts/legacy-registry-plan.json` for safe resume). `packaging.test.ts` asserts the generated plan covers all 54 retired names with uniform messages and valid guide anchors; the offline fixture suite `scripts/phase54-legacy-registry.test.mjs` proves the dry-run/apply/resume behavior without network or tokens. |
460
463
  | Formatting, linting, and coverage thresholds | `npm run lint` and `npm run format:check` run Biome (single root `biome.json`, workspaces inherit) and fail on any lint error or unformatted file; `npm run test:coverage` uses Node's built-in `--experimental-test-coverage` with enforced minimums (lines 60 / functions 70 / branches 75) and no third-party service. All three run inside `sdk:ready`. |