@plurnk/plurnk-schemes 1.3.12 → 1.5.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 (71) hide show
  1. package/README.md +43 -17
  2. package/SPEC.md +368 -86
  3. package/dist/Manifest.d.ts +6 -0
  4. package/dist/Manifest.d.ts.map +1 -0
  5. package/dist/Manifest.js +115 -0
  6. package/dist/Manifest.js.map +1 -0
  7. package/dist/Matcher.d.ts +5 -6
  8. package/dist/Matcher.d.ts.map +1 -1
  9. package/dist/Matcher.js +51 -59
  10. package/dist/Matcher.js.map +1 -1
  11. package/dist/MimetypeClassifier.d.ts +1 -1
  12. package/dist/MimetypeClassifier.d.ts.map +1 -1
  13. package/dist/MimetypeClassifier.js +11 -15
  14. package/dist/MimetypeClassifier.js.map +1 -1
  15. package/dist/NetworkAddress.d.ts +13 -0
  16. package/dist/NetworkAddress.d.ts.map +1 -0
  17. package/dist/NetworkAddress.js +43 -0
  18. package/dist/NetworkAddress.js.map +1 -0
  19. package/dist/OutputScheme.d.ts.map +1 -1
  20. package/dist/OutputScheme.js +5 -6
  21. package/dist/OutputScheme.js.map +1 -1
  22. package/dist/PacketSections.d.ts +6 -0
  23. package/dist/PacketSections.d.ts.map +1 -0
  24. package/dist/PacketSections.js +42 -0
  25. package/dist/PacketSections.js.map +1 -0
  26. package/dist/PathMimetype.js +9 -9
  27. package/dist/PathMimetype.js.map +1 -1
  28. package/dist/Results.d.ts +42 -8
  29. package/dist/Results.d.ts.map +1 -1
  30. package/dist/Results.js +132 -44
  31. package/dist/Results.js.map +1 -1
  32. package/dist/SchemeDiscovery.d.ts +2 -0
  33. package/dist/SchemeDiscovery.d.ts.map +1 -1
  34. package/dist/SchemeDiscovery.js +40 -42
  35. package/dist/SchemeDiscovery.js.map +1 -1
  36. package/dist/SchemeResolver.d.ts.map +1 -1
  37. package/dist/SchemeResolver.js +4 -5
  38. package/dist/SchemeResolver.js.map +1 -1
  39. package/dist/Slicer.d.ts +27 -14
  40. package/dist/Slicer.d.ts.map +1 -1
  41. package/dist/Slicer.js +382 -374
  42. package/dist/Slicer.js.map +1 -1
  43. package/dist/Summarize.js +3 -3
  44. package/dist/Summarize.js.map +1 -1
  45. package/dist/ctx.d.ts +70 -67
  46. package/dist/ctx.d.ts.map +1 -1
  47. package/dist/edit-receipt.d.ts +39 -0
  48. package/dist/edit-receipt.d.ts.map +1 -0
  49. package/dist/edit-receipt.js +2 -0
  50. package/dist/edit-receipt.js.map +1 -0
  51. package/dist/handler.d.ts +13 -10
  52. package/dist/handler.d.ts.map +1 -1
  53. package/dist/handler.js +7 -5
  54. package/dist/handler.js.map +1 -1
  55. package/dist/index.d.ts +14 -8
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +6 -1
  58. package/dist/index.js.map +1 -1
  59. package/dist/packet.d.ts +2 -3
  60. package/dist/packet.d.ts.map +1 -1
  61. package/dist/packet.js +2 -20
  62. package/dist/packet.js.map +1 -1
  63. package/dist/types.d.ts +3 -11
  64. package/dist/types.d.ts.map +1 -1
  65. package/dist/types.js +1 -7
  66. package/dist/types.js.map +1 -1
  67. package/package.json +10 -8
  68. package/dist/DefaultRead.d.ts +0 -11
  69. package/dist/DefaultRead.d.ts.map +0 -1
  70. package/dist/DefaultRead.js +0 -27
  71. package/dist/DefaultRead.js.map +0 -1
package/SPEC.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Contract for `@plurnk/plurnk-schemes-*` sibling packages. Audience: implementer of a URI scheme handler. Consumer: [plurnk-service](https://github.com/plurnk/plurnk-service) (SPEC.md §3).
4
4
 
5
- ## §1 Manifest {§manifest}
5
+ ## §manifest §1 Manifest
6
6
 
7
7
  ```json
8
8
  {
@@ -16,13 +16,12 @@ Class-level manifest (static field on the default export):
16
16
  ```ts
17
17
  import type { SchemeManifest } from "@plurnk/plurnk-schemes";
18
18
 
19
- class Known {
19
+ class Notes {
20
20
  static manifest: SchemeManifest = {
21
- name: "known",
21
+ name: "notes",
22
22
  channels: { body: "text/markdown", preview: "text/markdown" },
23
23
  defaultChannel: "body",
24
24
  category: "data",
25
- scope: "workspace",
26
25
  writableBy: ["model", "client"],
27
26
  volatile: false,
28
27
  modelVisible: true,
@@ -35,47 +34,67 @@ class Known {
35
34
  |---|---|
36
35
  | `name` | Matches `package.json#plurnk.name`. Addressing/routing identity (the URI prefix). |
37
36
  | `channels` | `Record<channelName, mimetype>`. Channel names lowercase. Empty = dynamic per-call. |
38
- | `defaultChannel` | Channel name targeted when path has no `#fragment`. Empty when channels is empty. |
39
- | `category` | `"data"` (entry-bearing) \| `"logging"` (`log://` rows) \| `"control"` (addresses sister processes/runs, owns no entries — e.g. `worker://`). |
40
- | `scope` | `"workspace"` \| `"worker"` (grammar 0.67 `default_scope`; `worker` = per-worker scratch backing `worker://`). |
41
- | `writableBy` | Subset of `["model", "client", "system", "plugin"]`. Consumer returns 403 for outside-set writes. |
37
+ | `defaultChannel` | Channel targeted when path has no `#fragment`. Dynamic-channel schemes may name it without fixing a mimetype; empty means no default. |
38
+ | `category` | `"data"` (entry-bearing) \| `"logging"` (`log://` rows) \| `"control"` (addresses sister workers, owns no entries — e.g. `worker://`). |
39
+ | `writableBy` | Subset of `["model", "client", "plurnk", "plugin"]`; empty declares an immutable scheme. Consumer returns 403 for outside-set writes. |
42
40
  | `volatile` | Boolean. |
43
41
  | `modelVisible` | Boolean. |
44
- | `folderScopes?` | `true` declares that a trailing slash on READ is a collection scope. Absent/false means `/` is ordinary resource syntax. Matcher bodies and explicit globs remain queries independently. |
45
- | `flags?` | Optional `SchemeFlagAffinity`. |
42
+ | `folderScopes?` | `true` declares that a trailing slash on FIND is a collection scope. Absent/false means `/` is ordinary resource syntax. |
43
+ | `flags?` | Optional exact `SchemeFlagAffinity`; see {§manifest-flag-affinity}. |
46
44
  | `example?` | The scheme's terse **hot-path** one-liner (e.g. `"READ(foo://thing/42)"`) — renders in the live catalogue every turn, so keep it to one canonical usage line. Omit → not advertised. Depth goes in `documentation`. |
47
- | `documentation?` | The **deep doc** (semantics / channels / edge cases). Consumer materializes it as a pull-able `worker://plurnk/docs/<name>.md` entry READ on demand; never hits the hot path. Mirrors `ExecInfo.documentation` (schemes#25). |
48
- | `glyph?` | Display icon (emoji / nerdfont). Omit → consumer renders the `name` (`glyph ?? name`). |
49
- | `foldedByDefault?` | Entries land FOLDED, off the ranked manifest surface (READable via address, not poured into the ranked view). For executor-output streams (`<tag>://`) — containment one level up (schemes#20/service#240). Absent/false → ranked/first-class. |
50
- | `storedScheme?` | Value persisted to `entries.scheme`, which may differ from the addressing `name`. Resolution: `storedScheme === undefined ? name : storedScheme`. Absent → defaults to `name` (additive; existing manifests unchanged). Explicit `null` → persists BARE (e.g. File: bare paths, `entries.scheme` NULL, routing name `"file"`). |
45
+ | `documentation?` | The **deep doc** (semantics / channels / edge cases). Consumer materializes it as a pull-able `worker://plurnk/docs/<name>.md` entry READ on demand; never hits the hot path. Mirrors `ExecInfo.documentation`. |
46
+ | §manifest-client-display `glyph?` | Non-empty opaque client presentation glyph. It is projected through {§client-display-capabilities}; omission delegates identity fallback to the client. It never enters model teaching. |
47
+ | `foldedByDefault?` | Entries land FOLDED, off the ranked manifest surface (READable via address, not poured into the ranked view). For executor-output streams (`<tag>://`) — containment one level up. Absent/false → ranked/first-class. |
48
+ | `storedScheme?` | Value persisted to `entries.scheme`, which may differ from the addressing `name`. Absent defaults to `name`. It must be a non-null string because every persisted identity component is non-null. |
51
49
 
52
- **Self-doc split (terse pushes, depth pulls).** {§manifest-self-doc} `example` + `glyph` are the hot-path listing rendered every turn — keep them terse. `documentation` is the deep prose (every op, channel, status code, gotcha); the consumer materializes it as a pull-able **`worker://plurnk/docs/<name>.md`** entry the model READs on demand, off the hot path. Both live on the manifest; the consumer decides what's pushed vs pulled.
50
+ The manifest is closed: unknown top-level fields fail admission. Entry
51
+ visibility is not manifest metadata; the consumer resolves it through the
52
+ entry owner's identity.
53
+
54
+ §manifest-flag-affinity `flags` is a closed environmental-authority declaration.
55
+ Unknown flag fields fail manifest admission; absent fields are false. Only registered
56
+ schemes enter affinity evaluation; an unknown name remains the consumer's
57
+ registration failure.
58
+
59
+ | Field | Scheme is inactive when | Declared requirement |
60
+ | ---------------------- | -------------------------------- | --------------------------------------- |
61
+ | `excludedInAsk` | `mode === "ask"` | Act-mode operation authority |
62
+ | `requiresWeb` | `noWeb === true` | Web access |
63
+ | `requiresInteraction` | `noInteraction === true` | Interactive access |
64
+
65
+ Proposal behavior is not scheme affinity. A handler proposes by returning 202;
66
+ the consumer's proposal lifecycle decides whether a client, loop auto,
67
+ `noProposals`, or a timeout resolves it.
68
+
69
+ §manifest-self-doc **Self-doc split (terse pushes, depth pulls).** `example` is the hot-path listing rendered every turn — keep it terse. `documentation` is the deep prose (every op, channel, status code, gotcha); the consumer materializes it as a pull-able **`worker://plurnk/docs/<name>.md`** entry the model READs on demand, off the hot path. Both live on the manifest; the consumer decides what's pushed vs pulled. `glyph` is client display metadata under {§manifest-client-display}, not self-documentation.
53
70
 
54
71
  **Authoring convention — `docs/<name>.md`.** The contract field stays a plain `string`, but a sibling SHOULD keep the deep doc in a **`docs/<name>.md`** file at the package root rather than inline, and load it into the manifest at module init — e.g. `documentation: await readFile(new URL("../docs/<name>.md", import.meta.url), "utf-8")` (top-level await; `../` resolves identically from `src/` in test and `dist/` once built). Ship it by adding `docs/**/*` to `files`. This keeps prose out of the handler source and gives editors real Markdown; the contract and the consumer's `worker://plurnk/docs/<name>.md` materialization are unchanged. A missing file fails-hard at import (no silent empty doc).
55
72
 
56
73
  ## §2 Interface
57
74
 
58
75
  Sister scheme handlers implement op methods consumed by plurnk-service via
59
- dispatch. An absent method returns **501**, except FIND on an entry-bearing
60
- `category: "data"` scheme: the consumer supplies its standard stored-entry
61
- FIND automatically. The op-dispatch surface is the exported
62
- **`SchemeHandler`** interface — every method optional, each
63
- `(statement, ctx) => Promise<SchemeResult>`, the per-op statement type from
64
- grammar:
76
+ dispatch. The consumer owns READ for every `category: "data"` scheme and owns
77
+ exact-target FIND over its canonical representation; no handler method can
78
+ replace either projection. Other absent operation methods return **501**. The
79
+ exported **`SchemeHandler`** interface gives operation methods their grammar
80
+ statement and `ctx`, while representation preparation receives a deliberately
81
+ narrower request. `editBatch` returns the typed `EditBatchResult`
82
+ specialization:
65
83
 
66
84
  ```ts
67
- import type { SchemeHandler } from "@plurnk/plurnk-schemes";
85
+ import type { EditBatchResult, SchemeHandler } from "@plurnk/plurnk-schemes";
68
86
 
69
87
  export interface SchemeHandler {
88
+ ready?(): Promise<void>;
70
89
  close?(): Promise<void>;
90
+ resolveEntryAddress?(target: ParsedPath, ctx: SchemeCtx): Promise<EntryAddress | SchemeResult | null>;
91
+ prepareRepresentation?(
92
+ request: RepresentationPreparationRequest,
93
+ ctx: SchemeCtx,
94
+ ): Promise<RepresentationPreparationResult>;
71
95
  prepareFind?(statement: FindStatement, ctx: SchemeCtx): Promise<SchemeResult>;
72
- read?(statement: ReadStatement, ctx: SchemeCtx): Promise<SchemeResult>;
73
96
  find?(statement: FindStatement, ctx: SchemeCtx): Promise<SchemeResult>;
74
- open?(statement: OpenStatement, ctx: SchemeCtx): Promise<SchemeResult>;
75
- fold?(statement: FoldStatement, ctx: SchemeCtx): Promise<SchemeResult>;
76
- editBatch?(statements: readonly EditStatement[], ctx: SchemeCtx): Promise<SchemeResult>;
77
- copy?(statement: CopyStatement, ctx: SchemeCtx): Promise<SchemeResult>;
78
- move?(statement: MoveStatement, ctx: SchemeCtx): Promise<SchemeResult>;
97
+ editBatch?(statements: readonly EditStatement[], ctx: SchemeCtx): Promise<EditBatchResult>;
79
98
  send?(statement: SendStatement, ctx: SchemeCtx): Promise<SchemeResult>;
80
99
  exec?(statement: ExecStatement, ctx: SchemeCtx): Promise<SchemeResult>;
81
100
  work?(statement: WorkStatement, ctx: SchemeCtx): Promise<SchemeResult>;
@@ -86,56 +105,270 @@ export interface SchemeHandler {
86
105
  }
87
106
  ```
88
107
 
89
- `prepareFind?` is not a second query implementation. It is the optional
90
- discovery/materialization seam invoked before the consumer's standard FIND
91
- when a data scheme has no custom `find()`. Stored schemes omit it. Acquisition
92
- schemes use it to make an exact requested resource into an ordinary entry,
93
- then receive the same catalog, matcher, span, weight, pagination, and status
94
- semantics as every other entry-bearing scheme. A custom `find()` replaces the
95
- whole operation only where the scheme owns genuinely different candidate
96
- semantics.
108
+ §read-preparation `prepareRepresentation?` is the optional, operation-neutral
109
+ acquisition seam for one exact resource. Core first resolves its canonical
110
+ pathname and owner, binds `ctx.entries`/channels/subscriptions to that owner,
111
+ and removes the channel fragment. The request is exactly
112
+ `{ target, pathname }`: READ coordinates, FIND matchers, the selected channel,
113
+ and operation intent are structurally unavailable.
114
+
115
+ A stored scheme omits the hook. An acquired scheme writes its complete channel
116
+ topology through `ctx.entries` and returns `200` readiness. A live scheme first
117
+ seeds those same canonical channels, opens a subscription, and may return `102`
118
+ only while production remains live. Other statuses are exact terminal results.
119
+ Per-channel producer status belongs durably on that channel as
120
+ `producerResult`; no transient preparation result may carry content, channel
121
+ selection, or channel outcomes. After `200`, core alone selects the authored
122
+ channel, applies tag/binary/text rules (including markerless `<1,16>`), and
123
+ composes that channel's producer evidence. Cold and warm reads therefore have
124
+ identical semantics.
125
+
126
+ Exact FIND uses the same `prepareRepresentation?` seam and then the standard
127
+ entry query; the queried default channel's durable producer result composes
128
+ with that exact query just as it does with exact READ. `prepareFind?` is only
129
+ the optional broad-scope discovery seam
130
+ used before standard entry selection when no custom broad `find()` exists. A
131
+ custom `find()` may own genuinely different candidate enumeration, but never
132
+ exact-resource materialization or READ coordinates. Every standard query uses
133
+ the same canonical identity as `ctx.entries`; URL authority cannot disappear
134
+ between preparation and lookup.
135
+
136
+ An exact COPY/MOVE source also resolves and prepares through this seam before
137
+ core selects its canonical stored channel. COPY/MOVE retain their own source
138
+ scope algebra: an absent source scope means the complete channel and an explicit
139
+ scope preserves raw source separators. Neither distinction is exposed to the
140
+ producer. A live `102` leaves the composition unapplied; a ready representation
141
+ is selected from the same channels used by READ and exact FIND. A selected
142
+ channel's producer failure aborts before destination mutation; successful
143
+ non-`200` content remains eligible for transfer.
144
+
145
+ §entry-address-resolution `resolveEntryAddress?` gives a client observation the
146
+ same address law as model-facing operations without creating a second CRUD
147
+ protocol. Core removes the channel fragment and target-slot pathname aliases
148
+ {§path-parentheses} before invocation; query and other identity components
149
+ remain exact. The hook returns the canonical stored `pathname` and a semantic
150
+ owner:
151
+
152
+ | Return | Meaning |
153
+ |-----------------------------------------|---------------------------------------------------|
154
+ | `{ pathname, owner: "commons" }` | Resolve in the shared workspace namespace |
155
+ | `{ pathname, owner: "worker" }` | Resolve in the calling worker's namespace |
156
+ | Non-success `SchemeResult` | Preserve an expected address refusal exactly |
157
+ | `null` | The selector names no client-visible entry |
158
+ | Hook absent | Use the standard pathname and `commons` ownership |
159
+
160
+ The consumer supplies the observing worker through `SchemeCtx`, lowers the
161
+ semantic owner to its private storage identity, and performs the read. A plugin
162
+ never receives or returns database owner IDs. Schemes implement the hook only
163
+ when their standard operation path has scheme-specific canonicalization or
164
+ caller ownership; the hook must apply that same rule rather than inventing a
165
+ client-only address vocabulary.
166
+
167
+ A sibling does `export default class X implements SchemeHandler` (with `static manifest: SchemeManifest`) and gets compile-time signature checking. Every registered handler exposes either that static manifest or an instance `manifest` for dynamically derived identities; `Manifest.of` validates the complete resolved declaration and its registration name before the handler becomes dispatchable. The interface is the handler-delegable subset of grammar's operation union. `LOOK`/`BUFF` are client-facing operations, while OPEN/FOLD are core-owned log curation; none is dispatchable to a plugin scheme. **The statement + path types (`ReadStatement`, `SendStatement`, `UrlPath`, …) are re-exported from this barrel**, so a sibling depends on and peers (`^1`) ONLY `@plurnk/plurnk-schemes` — grammar rides underneath as the framework's transitive dep (§3).
168
+
169
+ §handler-lifecycle A registered handler object is a process-lived shared
170
+ instance and may receive overlapping operation calls. Retained handler state is
171
+ ordinary; handlers must make concurrent access and semantic isolation explicit.
172
+
173
+ | Concern | Contract |
174
+ | ------------------ | -------- |
175
+ | Readiness | `ready?()` runs after registration and before capability advertisement. It establishes that advertised resources are usable. |
176
+ | Hook cardinality | The consumer invokes each hook once per unique handler object identity, even when multiple registered names share that object. |
177
+ | Operation context | `SchemeCtx` and capabilities extracted from it remain operation-owned under {§scheme-ctx-lifetime}; only the `StreamSubscription` returned by `subscriptions.open` is retainable. |
178
+ | Retained state | Tenant- or address-selectable state keys every semantic isolation coordinate. A global pool is valid only when it carries no cross-tenant session state. |
179
+ | Live ownership | An address-selectable live resource has one unambiguous owner. Conflicting acquisition fails explicitly; it never overwrites the existing owner. |
180
+ | Shutdown | After in-flight scheme work drains and before backing stores close, `close?()` releases partially or fully initialized handler resources. The consumer attempts and awaits every unique handler close, then aggregates every failure. |
181
+
182
+ Handlers without initialization or retained resources omit the corresponding
183
+ hook.
97
184
 
98
- A sibling does `export default class X implements SchemeHandler` (with `static manifest: SchemeManifest`) and gets compile-time signature checking. The op set is exactly grammar's `PlurnkStatement` dispatch union and moves with the framework's grammar bump (0.74.57 added `work?`/`fork?`; `LOOK`/`BUFF` are grammar `ClientStatement` ops, client-facing and never dispatched to a scheme, so they're intentionally absent here). **The statement + path types (`ReadStatement`, `SendStatement`, `UrlPath`, …) are re-exported from this barrel**, so a sibling depends on and peers (`^1`) ONLY `@plurnk/plurnk-schemes` — grammar rides underneath as the framework's transitive dep (§3).
185
+ The entry CRUD primitives (`readEntry`/`writeEntry`/`deleteEntry`) are not handler operations; schemes use `ctx.entries`. Proposal application is the optional `applyResolution` handler hook described in §3.bis.
99
186
 
100
- `close?()` is the process-lifecycle hook for pooled resources such as browser processes, sockets, and client connections. The consumer calls it once per unique handler instance after in-flight scheme work drains and before backing stores close. Stateless handlers omit it.
187
+ OPEN and FOLD are not handler methods. They curate visibility and tags on the
188
+ core-owned log; an entry scheme has no visibility state and receives 501.
101
189
 
102
- The entry CRUD primitives (`readEntry`/`writeEntry`/`deleteEntry`) are not handler operations; schemes use `ctx.entries`. Proposal application is the optional `applyResolution` handler hook described in §3.bis.
190
+ §scheme-edit-batch-receipt **Regional mutation receipts.** COPY and MOVE are not
191
+ handler methods. The engine composes their source and
192
+ destination resource selections over `ctx.entries` and uses `editBatch` for a
193
+ scoped destination or source mutation. This keeps channel selection, snapshot
194
+ ordering, cross-scheme failures, and proposal sequencing uniform for every data
195
+ scheme. Scheme hooks return their ordinary mutation result and, for a regional
196
+ `editBatch`, an `EditBatchResult` carrying its aggregate `EditBatchReceipt`:
197
+
198
+ ```ts
199
+ type EditReceiptUnit = "lines" | "codePoints";
200
+
201
+ interface EditEffectReceipt {
202
+ readonly requested: string;
203
+ readonly source: string;
204
+ readonly result: string;
205
+ readonly removed: number;
206
+ readonly inserted: number;
207
+ readonly context: string;
208
+ }
209
+
210
+ interface AppliedEditBatchReceipt {
211
+ readonly revision: string;
212
+ readonly unit: EditReceiptUnit;
213
+ readonly before: number;
214
+ readonly after: number;
215
+ readonly effects: readonly EditEffectReceipt[];
216
+ }
217
+
218
+ interface ReviewerReplacementEditBatchReceipt {
219
+ readonly revision: string;
220
+ readonly unit: EditReceiptUnit;
221
+ readonly before: number;
222
+ readonly after: number;
223
+ readonly disposition: "reviewer-replaced";
224
+ readonly superseded: readonly string[];
225
+ readonly replacement: EditEffectReceipt;
226
+ }
227
+
228
+ type EditBatchReceipt =
229
+ | AppliedEditBatchReceipt
230
+ | ReviewerReplacementEditBatchReceipt;
231
+
232
+ interface EditBatchResult extends SchemeResult {
233
+ readonly editReceipt?: EditBatchReceipt | null;
234
+ }
235
+ ```
236
+
237
+ | Batch outcome | Authored correlation | Landed effects |
238
+ | ------------------------------------ | ------------------------------------------------- | --------------------------------------------------------------------- |
239
+ | Applied as proposed | `effects[N]` describes authored EDIT `N` | One truthful effect per authored EDIT |
240
+ | Resolver replaced the proposed body | `superseded[N]` retains authored marker `N` | One `replacement` effect describes the bytes that actually landed |
241
+
242
+ The replacement form is not a heuristic attribution. An arbitrary resolver
243
+ body supersedes every authored EDIT because its correspondence to those edits
244
+ cannot be known. The consumer projects the replacement once on the durable
245
+ proposal-owning row and projects each authored marker's superseded disposition
246
+ on its own row.
247
+
248
+ The engine validates that receipt and owns the ordered COPY/MOVE resource
249
+ effects shown to consumers; plugins do not invent a second effect envelope.
103
250
 
104
251
  ## §3 Helpers exported by this repo
105
252
 
106
253
  ### Types
107
254
 
108
- - Manifest/flags: `SchemeManifest`, `SchemeFlagAffinity`, `WriterTier`, `LoopFlags`, `DEFAULT_LOOP_FLAGS`.
109
- - Optional `PacketSectionTransformer` — a scheme MAY implement `transformSections(sections: PacketSection[]) → PacketSection[] | Promise<…>` to reshape the packet's section list (add/remove/reorder) before the engine measures it; called duck-typed in registration order. `PacketSection` = `{ name; slot: "system"|"user"; header: string|null; content; tokens }`. Contract declares the shape; service conforms (the in-process plugin-packet-control / fork-avoidance seam — schemes#24).
110
- - Behavior contract: `SchemeHandler` (§2). Scheme-facing grammar types re-exported here so siblings pin only this package: `PlurnkStatement` + the per-op statement types (`ReadStatement`, `FindStatement`, `OpenStatement`, `FoldStatement`, `EditStatement`, `CopyStatement`, `MoveStatement`, `SendStatement`, `ExecStatement`, `WorkStatement`, `ForkStatement`, `KillStatement`, `PlanStatement`) and path types (`ParsedPath` = `LocalPath` | `UrlPath` | `RegexPath`).
255
+ - Manifest/flags: `SchemeManifest`, `SchemeFlagAffinity`, and `WriterTier`; contracts-owned `LoopFlags` and `DEFAULT_LOOP_FLAGS` are re-exported for compatibility.
256
+ - Mutation receipts: `EditBatchResult`, `EditBatchReceipt`, `EditReceipt`, `EditEffectReceipt`, and `EditReceiptUnit`.
257
+ - §scheme-packet-transform **Packet-section transformation.** A scheme may implement `transformSections(sections: PacketSectionDraft[]) → PacketSectionDraft[] | Promise<…>` to add, remove, or reorder sections before core measures the packet. The exact draft shape is `{ name; slot: "system"|"user"; header: string|null; content }`; no token measurement exists at this boundary. Core invokes implementations in registration order and applies `PacketSections.assertDrafts` to the initial list and every returned list. Names are non-empty and unique within the packet.
258
+ - Behavior contract: `SchemeHandler` (§2). Scheme-facing grammar types re-exported here so siblings pin only this package: `PlurnkStatement` + the per-op statement types (`ReadStatement`, `FindStatement`, `OpenStatement`, `FoldStatement`, `EditStatement`, `CopyStatement`, `MoveStatement`, `SendStatement`, `ExecStatement`, `WorkStatement`, `ForkStatement`, `KillStatement`, `PlanStatement`) and path types (`ParsedPath` = `LocalPath` | `UrlPath`).
259
+ - Target syntax: contracts-owned `PathSyntax` is re-exported for the shared exact-versus-path-glob classifier {§path-glob}.
111
260
  - Discovery: `SchemeDiscovery` (behavior class) with `SchemeInfo` / `SchemeDiscoveryResult` / `DiscoverOptions` (§6).
112
- - Executor-scheme (RFC schemes#20 — "an executor is a scheme"): `OutputScheme.manifestFromRuntime(decl)` derives a read-only-output `SchemeManifest` from an executor's `RuntimeDecl` (zero scheme-authoring); `DefaultRead.read(content, mimetype, statement, mimetypes)` → `ReadResolution` is the free `<L>`/matcher read over produced output (reuses `Slicer`/`Matcher`); `Summarize.summarize(content, mimetype)` → `OrientIndex` is the structural-only EXEC-receipt index (no content — universal-receipt containment). A per-tag executor-scheme supplies its manifest via instance `get manifest()` (§2 `SchemeHandler.manifest?`).
113
- - Results: `SchemeResult` is the universal `{ status, ...schemeMetadata }` contract. `EntryResult`, `ProposalResult`, and `PassthroughResult` are optional conventional shapes, not engine routing discriminators. Their `error` is a grammar `TelemetryEvent`, present iff `status >= 400`. Guards inspect those optional shapes; proposal routing itself is engine-owned and follows status plus operation semantics.
114
- - Capability ctx (see §3.bis): `SchemeCtx` and its domain capabilities. Entry authors additionally receive `EntryOperationCaps`, semantic `EntryOwner`, and typed standard-operation results. `editBatch` receives every same-turn EDIT for one canonical resource and channel; it validates against one snapshot and commits one revision or none. There is no sequential single-EDIT fallback.
261
+ - §executor-scheme-output Executor-scheme ("an executor is a scheme"): `OutputScheme.manifestFromRuntime(decl)` derives a read-only-output `SchemeManifest` from an executor's `RuntimeDecl` (zero scheme-authoring). Executor output is a canonical entry and therefore inherits core's exact READ projection under {§read-preparation}; no executor-specific READ helper exists. `Summarize.summarize(content, mimetype)` -> `OrientIndex` is the structural-only EXEC-receipt index (no content - universal-receipt containment). A per-tag executor-scheme supplies its manifest via instance `get manifest()` (§2 `SchemeHandler.manifest?`).
262
+ - Results: `SchemeResult` is the universal operation-result contract. Statuses below 400 carry no `problem`; statuses 400–599 require RFC 9457 `ProblemDetails`, and the legacy `error` member is forbidden. `EntryResult`, `ProposalResult`, and `PassthroughResult` are optional conventional shapes, not engine routing discriminators. Guards inspect those optional shapes; proposal routing itself is engine-owned and follows status plus operation semantics.
263
+ - Standard `EntryFindResult` exposes only its paged `results`, complete `matchingPathCount` / `matchLocationCount`, content weights, and typed range. Resource-mode rows are `EntryCatalogItem` or `EntryCatalogScope`; exact matcher mode returns flat `MatchEvidence` locations. Pagination is the materialization bound; no hidden `matches`, `pathnames`, or overflow-only result collection exists.
264
+ - Capability ctx (see §3.bis): `SchemeCtx`, `StreamSubscription`, and the domain capabilities. Entry authors additionally receive `EntryOperationCaps`, semantic `EntryOwner`/`EntryAddress`, and typed standard-operation results. `editBatch` receives every same-turn EDIT for one canonical resource and channel; it validates against one snapshot and commits one revision or none. There is no sequential single-EDIT fallback.
115
265
 
116
266
  Behavior ships as `export default class` (one class per file, static methods) — the ecosystem class paradigm. Type-only modules, the barrel, and the frozen `DEFAULT_LOOP_FLAGS` constant are the only non-class files.
117
267
 
268
+ ### §network-address Network address identity
269
+
270
+ `NetworkAddress.from(target)` is the single non-secret normalization path for
271
+ HTTP and WebSocket targets. It returns the exact addressed `scheme`, the
272
+ canonical entry `pathname`, a fragmentless and credential-free transport
273
+ `url`, and whether userinfo was present so the handler can reject it.
274
+
275
+ | Component | Entry identity | Transport URL | Plurnk channel |
276
+ |------------------|-----------------------------------------------------|---------------|----------------|
277
+ | Scheme | Exact `http`, `https`, `ws`, or `wss` | Yes | No |
278
+ | Host | Canonical WHATWG hostname | Yes | No |
279
+ | Port | Non-default port | Yes | No |
280
+ | Path | Canonical pathname | Yes | No |
281
+ | Query | Serialized order, duplicates, and explicit empty `?` | Yes | No |
282
+ | Fragment | No | No | Yes |
283
+ | Userinfo/headers | No | No | No |
284
+
285
+ The canonical storage form is
286
+ `/<host>[:<port>]<path>[?<query>]`, keyed together with workspace, owner, and
287
+ the exact addressed scheme. `NetworkAddress.render(scheme, pathname)` is its
288
+ model-facing inverse: it applies the Plurnk lexical target spelling
289
+ {§path-parentheses} after reconstructing the exact address. Transport URLs and
290
+ storage identity never carry that syntax layer. Routing aliases select a
291
+ handler; they do not collapse resource identity.
292
+
118
293
  ### Active-scheme resolution — `SchemeResolver`
119
294
 
120
295
  - `SchemeResolver.forLoop(handlers: ReadonlyMap<string, object>, flags: LoopFlags): Set<string>` — applies `manifest.flags` affinity to each handler and returns names of schemes active under the loop's flags.
121
296
 
122
- ### Mimetype classification — `MimetypeClassifier` {§mimetype-classifier}
297
+ ### §mimetype-classifier Mimetype classification — `MimetypeClassifier`
123
298
 
124
- - `MimetypeClassifier.isBinary(mimetype)` — enforces 415 boundary on binary entries. Delegates to `classifyMimetype` from @plurnk/plurnk-mimetypes (the framework owns the text/binary taxonomy — mimetypes#43; the former local allowlists were absorbed upstream verbatim and retired).
125
- - `MimetypeClassifier.isJson(mimetype)` — `application/json` plus `+json` variants. Used by `<L>` dispatch. Scheme semantics — stays local, not delegated.
126
- - `MimetypeClassifier.isLineNavigable(mimetype)` — render-layer decides whether to prefix lines with `N:\t`. Delegates to `classifyMimetype`.
299
+ - `MimetypeClassifier.isBinary(mimetype)` — pure taxonomy for a registry-free boundary. Delegates to `classifyMimetype` from `@plurnk/plurnk-mimetypes`; a consumer with a configured `Mimetypes` service uses `Mimetypes.classify()` so installed handler declarations remain authoritative ({§mimetype-classification}).
300
+ - `MimetypeClassifier.isHtml(mimetype)` — recognizes the normalized web-projection family: `text/html` and `application/xhtml+xml`.
301
+ - `MimetypeClassifier.isJson(mimetype)` - `application/json` plus `+json` variants, used only by result summarization.
127
302
  - `MimetypeClassifier.normalizeAutoText(mimetype)` — `text/plain` / null / undefined → `TEXT_PRIMITIVE_MIMETYPE` (`text/markdown`).
128
303
  - `TEXT_PRIMITIVE_MIMETYPE` — `"text/markdown"` (named export from the same module).
129
304
 
130
- ### `<L>` slicing — `Slicer`
305
+ ### §scheme-projection Projection capability
306
+
307
+ Acquisition schemes delegate model-facing projection to the consumer's one
308
+ configured mimetype family. They neither instantiate readers nor widen durable
309
+ entry channels beyond Unicode text.
310
+
311
+ | Surface | Contract |
312
+ | --------------------------------- | ----------------------------------------------------------------------------------------------------- |
313
+ | `readable(content, mimetype)` | Project an acquired Unicode representation. |
314
+ | `readableBytes(chunks, mimetype)` | Project one async byte source under the mimetype family's bounded-input policy. |
315
+ | `identity(mimetype)` | Return the opaque identity of configured projection behavior for cache and materialization freshness. |
316
+ | `isBinary(mimetype)` | Classify through the configured registry; installed handler declarations remain authoritative. |
317
+ | `ProjectedText` | Derived Unicode plus its output mimetype, source mimetype, and opaque projection identity. |
318
+
319
+ A returned object is present even when its `content` is `""`; only `null`
320
+ denotes absence. Consumers must not infer projection presence from content
321
+ length. A thrown projection call is an execution failure whose cause
322
+ propagates; it must never be converted to `null`.
323
+ `ProjectionInputLimitError` is the projection boundary's exported name for the
324
+ configured mimetype family's typed bounded-input failure.
131
325
 
132
- - `Slicer.lines(content, marker)` — line-navigable slice. Returns `{ status, text?, startLine?, error? }`.
133
- - `Slicer.linesRaw(content, marker)` — same shape; no `N:\t` prefix.
134
- - `Slicer.jsonItems(content, marker)` — JSON-source item slice. Returns `{ status, body?, error? }`.
135
- - `Slicer.lineMarkerEdit(content, marker, body)` — line-navigable EDIT.
136
- - `Slicer.jsonItemEdit(content, marker, body)` — structural JSON EDIT.
326
+ ### §slicer-text-algebra Text-region slicing and replacement - `Slicer`
137
327
 
138
- ### Path-extension mimetype — `PathMimetype`
328
+ `Slicer` owns one text algebra for every textual mimetype:
329
+
330
+ | Scope | Meaning |
331
+ |---|---|
332
+ | `<N>` | whole physical line `N` |
333
+ | `<N,M>` | inclusive whole physical lines `N..M` |
334
+ | `<SL,SC,EL,EC>` | exact exclusive-end region using 1-based Unicode code-point columns |
335
+ | `<0>` / `<-1>` | mutation anchors before the first / after the final line |
336
+
337
+ As an unadvertised ingestion tolerance, `Slicer` accepts
338
+ `<startLine,startColumn,endLine>` and immediately lowers it to the complete
339
+ four-coordinate region ending after the final code point of `endLine`.
340
+ Successful results carry schemes-owned, boundary-validated normalization
341
+ evidence containing both the authored and canonical coordinates. Producers do
342
+ not emit or proactively teach the tolerated form.
343
+
344
+ A harmless end-column overshoot clamps to the line's end rather than erroring
345
+ — the model asked for "through the end" and the line is shorter than it
346
+ guessed. Start-column overshoot and out-of-range lines remain 416 errors.
347
+
348
+ - `Slicer.lines(content, marker)` returns the selected text, source
349
+ `startLine`, and complete resolved `region`; `Slicer.linesRaw` preserves
350
+ original newline separators for COPY/MOVE source transfer.
351
+ - `Slicer.textReplacement(content, marker, body)` lowers a line shorthand or
352
+ exact region to one `{start,end,body}` replacement against the source
353
+ snapshot.
354
+ - `Slicer.lineMarkerEdit` applies one replacement.
355
+ - `Slicer.lineMarkerEditBatch` validates all replacements against one snapshot,
356
+ rejects overlaps, and applies all or none.
357
+ - `Slicer.page(items, marker, options?)` is the separate ordered-result
358
+ pagination helper; it accepts only one or two integer positions and can name
359
+ an operation-owned range unit. `allowEmpty` preserves an otherwise valid
360
+ positive request when an upstream filter yields no items.
361
+ - `Slicer.coversAvailable(range)` derives whether a successful range selected
362
+ the complete available extent; that fact is never serialized separately.
363
+
364
+ A successful line or pagination selection carries the contracts-owned
365
+ `RangeExtent` {§range-extent}; a 416 carries the same extent without
366
+ `returned`. Exact regions use `region` instead. An exact-region failure,
367
+ including a failed tolerated three-coordinate scope, carries
368
+ `requestedCoordinates` exactly as authored; an invalid-arity text scope does
369
+ likewise. Both carry `columnKind: "unicodeCodePoints"`.
370
+
371
+ ### §path-extension-mimetype Path-extension mimetype — `PathMimetype`
139
372
 
140
373
  - `PathMimetype.resolve(pathname, defaultMimetype, mimetypes)` — pathname extension → `Mimetypes.detect({ ext })`; falls back to `defaultMimetype` when no extension. text/plain auto-normalizes to text/markdown.
141
374
 
@@ -143,19 +376,34 @@ Behavior ships as `export default class` (one class per file, static methods)
143
376
 
144
377
  - `Results.isEntry` / `Results.isProposal` / `Results.isPassthrough` — guards for the optional conventional result shapes.
145
378
  - `Results.isErrorStatus(status)` — `status >= 400`.
146
- - `Results.error(scheme, kind, message?, position?)` — build a scheme-sourced `TelemetryEvent` (`source: "scheme:<name>"`).
147
- - `Results.logCoordinate(coordinate, op?)` — build a `LogCoordinate` position.
148
-
149
- ### Matcher dispatch — `Matcher` {§matcher-dispatch}
150
-
151
- - `Matcher.matchAgainstContent(body, content, mimetype, mimetypes, baseLine?)` — body-matcher adapter over `Mimetypes.query` (glob/regex/jsonpath/xpath, all served by the framework). Maps framework errors:
152
- - `UnsupportedDialectError` → status 415
153
- - `InvalidExpressionError` → status 400
154
- - `QueryParseFailureError` → status 203 (soft fallback: raw content as text/markdown with `reason`)
155
- - Empty match array → status 204
156
- - Matches → status 200, body rendered as lean `<source-line>:\t<value>` lines (one match per line, the `N:\t` convention READ emits). Value bare for a single-line string, JSON-encoded otherwise so the one-match-per-line invariant holds (preserves `<L><K>` pick-Kth composition). The resolved query path (`matching`) is dropped — the structured `{matched, matching}` wrapper was a model-legibility barrier (schemes#12).
157
-
158
- ### §3.bis Capability ctx — the stable trusted-extension surface {§capability-ctx}
379
+ - `Results.problem(owner, code, status, detail, extensions?)` — build and validate RFC 9457 Problem Details with a stable `https://problems.plurnk.dev/<owner>/<code>` type.
380
+ - `Results.failure(owner, code, status, detail, fields?, extensions?)` — build and validate a failed operation result.
381
+ - `Results.assert(result)` — validate the complete success/failure discrimination and reject malformed plugin output.
382
+ - `Results.assertMatchEvidence(evidence)` / `assertMatchEvidenceList(evidence)` - enforce the exact `{ locator?, region? }` shape and shared `TextRegion` contract.
383
+ - `Results.assertReadResult(result)` - validate the universal operation result plus any `region` and `matches` it exposes.
384
+ - `Results.attachInstance(result, uri)` — attach the durable occurrence URI to a failed result.
385
+
386
+ A handler owns its failure classification and explanation. The daemon owns the
387
+ durable `instance`, because only it knows the committed log coordinate. A
388
+ malformed handler result is a plugin contract violation and fails hard; the
389
+ consumer does not invent a fallback error or reinterpret arbitrary fields.
390
+ The same discrimination applies to every `SchemeCtx` capability result:
391
+ entries, channels, and tags never return a bare failure status.
392
+
393
+ ### Matcher dispatch - `Matcher` {§matcher-dispatch}
394
+
395
+ - `Matcher.matchAgainstContent(body, content, mimetype, mimetypes)` is the body-matcher adapter over `Mimetypes.query` (glob/regex/jsonpath/xpath).
396
+ - A match returns status 200 and `matches: MatchEvidence[]`.
397
+ `MatchEvidence` is `{locator?, region?}`. `locator` preserves a structural locator;
398
+ `region` is a complete four-coordinate `TextRegion` only when the finding has
399
+ an honest exact or nearest-enclosing mapping into the text the model can READ.
400
+ Each item must contain at least one of `locator` or `region`; other fields violate
401
+ the shared evidence contract.
402
+ - The matcher is a boolean resource selector. It does not replace content with matched values or choose a retrieval window. FIND owns selection and pagination; exact READ owns text projection.
403
+ - Empty results return 204 with `matches: []`; `UnsupportedDialectError` maps to 415; `InvalidExpressionError` maps to 400; `QueryParseFailureError` maps to 203 with raw content, text/markdown, and `reason`.
404
+ - A multi-resource matcher omits candidates that return 415 when at least one candidate supports the dialect. If no candidate supports it, the matcher returns the first exact 415 Problem. An unreadable binary marker therefore cannot poison a repository-wide text search or masquerade as a match.
405
+
406
+ ### §capability-ctx §3.bis Capability ctx — the stable trusted-extension surface
159
407
 
160
408
  Scheme plugins are trusted in-process Node.js code. `SchemeCtx` is not a
161
409
  sandbox or a security boundary; an installed plugin already has the process's
@@ -169,18 +417,37 @@ its implementation.
169
417
  - `entries` — direct storage over the scheme's own namespace
170
418
  (`read`/`write`/`delete`) plus `operations`, the standard PLURNK
171
419
  `READ`/`EDIT`/`FIND`/`SEND` implementation for entry-bearing schemes.
420
+ A write may omit channel state to select the `static` default; a successful
421
+ storage read always returns each channel's persisted lifecycle state.
172
422
  Standard operations are bound to the handler's manifest. Their optional
173
423
  owner is semantic: `"commons"` (default) or the current `"worker"`; database
174
424
  owner IDs are not part of the plugin contract. A handler may implement its
175
- own op method instead. In particular, a handler with `find()` owns FIND and
176
- fan-out; one without it receives the standard stored-entry behavior.
425
+ own op method instead. In particular, a handler with `find()` owns FIND; one
426
+ without it receives the standard stored-entry behavior.
177
427
  - `channels` — content writes + state (`append`/`replace`/`setState`).
178
428
  - `tags` — entry tags (`add`/`remove`/`list`).
179
- - `notify` — between-turn client signal (`streamEvent`, metadata-only); not model-facing. (No `wakeWorker`: the run-wake carries subscription-close context that only exists at stream completion, so it lives on `subscriptions.close`. Only streaming schemes wake a worker, always via close.)
180
- - `projection` — `readable(content, mimetype)` asks the consumer's configured mimetype family for the model-facing text projection. Acquisition schemes own bytes/DOM; they do not instantiate or second-guess the reader family. `null` means no readable projection.
181
- - `subscriptions` — streaming lifecycle: `open(pathname, handle)` atomically binds durable subscription identity to the consumer's process-local live-handle registry, returns a worker+teardown-composed `AbortSignal`, and takes a force-cancel `SubscriptionHandle`; routed cancellation both invokes the handle and aborts that signal. `notifyChunk(channel, chunk, mimetype?)` is **fused** (append + stream/event in one call), with an optional per-call `mimetype` that retypes the channel to the content's actual type (passed statelessly per chunk; the impl writes only on change; the manifest channel mimetype is the pre-fetch seed default — plurnk-service#226). `close("done" | "error" | "cancelled", outcome?)` composites channel state, durable status (200/500/499), live-handle unregister, and run wake. Designed against Exec (two-channel, cancel-tested).
182
-
183
- There is **no `visibility` capability**: entry-level SHOW/HIDE was removed in plurnk-service's index/visibility teardown — SHOW/HIDE now collapse/expand `log://` rows, a log-side concern with no entry-visibility for a scheme to set (plurnk-service#180).
429
+ - `notify` — between-turn client signal (`streamEvent`, metadata-only); not model-facing. (No `wakeWorker`: the worker wake carries subscription-close context that only exists at stream completion, so it lives on `subscriptions.close`. Only streaming schemes wake a worker, always via close.)
430
+ - `projection` — the text and bounded-byte projection capability in {§scheme-projection}. Acquisition schemes own source representations; they do not instantiate or second-guess the reader family. `null` means no readable projection.
431
+ - §scheme-subscriptions `subscriptions` — one streaming lifecycle:
432
+
433
+ | Surface | Lifetime | Contract |
434
+ | --------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------- |
435
+ | `open(pathname, handle)` | Operation | Bind durable and live identity, return the exact object, and route cancellation through its signal/handle. |
436
+ | Returned `StreamSubscription` | Retainable | `AbortSignal` plus fused chunk publication and terminal settlement; the only retainable capability. |
437
+ | `subscriptions.notifyChunk/close` | Operation | Forward to the exact returned object; own no second state or persistence path. |
438
+
439
+ `notifyChunk` appends and emits one stream event. Its optional stateless
440
+ `mimetype` retypes the channel only when the stored type differs.
441
+ `close(result, summary?, channelStates?)` validates and persists the exact
442
+ universal `SchemeResult`, settles channel state (exact named overrides, then the
443
+ result-status default), unregisters the live handle, and wakes the worker.
444
+ `summary` is presentation only; it never replaces or reconstructs the result.
445
+ An override names an existing channel and is exactly `closed` or `errored`;
446
+ invalid overrides fail before any channel state is changed. This permits one
447
+ multi-channel producer to preserve successful evidence beside an independently
448
+ failed representation without inventing another settlement path.
449
+
450
+ There is **no `visibility` capability**: entry-level SHOW/HIDE was removed in the index/visibility teardown — SHOW/HIDE now collapse/expand `log://` rows, a log-side concern with no entry-visibility for a scheme to set.
184
451
 
185
452
  The consumer passes exactly this public context to every handler. Bundled
186
453
  adapters that also implement daemon-owned lifecycle behavior receive those
@@ -188,7 +455,22 @@ collaborators separately at construction or binding time; consumers must not
188
455
  decorate `SchemeCtx` with database handles, registries, tokenizers, notifier
189
456
  callbacks, or other private service state.
190
457
 
191
- **Proposals are not a capability.** A side-effecting scheme proposes by *returning* a `ProposalResult` (status 202); the engine owns the resolution lifecycle. On acceptance it calls the handler's optional `applyResolution({ attrs, body }, ctx)` hook. `attrs` is the payload returned by the proposing operation and `body` is the resolver-approved body, when present. The hook returns `{ status, outcome?, body? }`: a status below 400 completes the accept; a failure rejects it with the returned outcome.
458
+ §scheme-ctx-lifetime `SchemeCtx` belongs to one operation call. A handler may
459
+ use it for the duration of that call, including asynchronous work awaited by
460
+ the call, but must not retain the context or any other extracted capability
461
+ after the handler returns. A streaming handler may retain only the exact
462
+ `StreamSubscription` returned by `subscriptions.open`; its bound methods carry
463
+ that subscription's identity without retaining the general context. Other
464
+ retained handler-owned resources follow {§handler-lifecycle}.
465
+
466
+ **Proposals are not a capability.** A side-effecting scheme proposes by *returning* a `ProposalResult` (status 202); the engine owns the resolution lifecycle. On acceptance it calls the handler's optional `applyResolution({ attrs, body }, ctx)` hook. `attrs` is the payload returned by the proposing operation and `body` is the resolver-approved body, when present. The hook returns a `ProposalApplyResult`: a status below 400 completes the accept; a failure preserves the applying scheme's Problem Details as the proposal's final result.
467
+
468
+ §scheme-edit-proposal-receipt An accepted EDIT proposal may return its final
469
+ aggregate `editReceipt` alongside its per-row model-facing `result`. A resolver
470
+ replacement must use the reviewer-replaced form from
471
+ {§scheme-edit-batch-receipt}. Core consumes that aggregate as transient batch
472
+ coordination and COPY/MOVE composition input; it persists only per-row receipts
473
+ or ordered resource effects.
192
474
 
193
475
  ## §4 What's NOT in this repo
194
476
 
@@ -199,12 +481,12 @@ Raw CRUD methods are not a parallel handler protocol. For `data` schemes whose
199
481
  handlers omit resource-specific storage hooks, engine-owned COPY/MOVE/KILL
200
482
  orchestration uses the manifest-bound `ctx.entries` implementation.
201
483
 
202
- ## §5 Trusted extension contract {§trusted-extension}
484
+ ## §trusted-extension §5 Trusted extension contract
203
485
 
204
486
  Installed schemes may legitimately own network connections, subprocesses,
205
- caches, pools, or other host resources. Use `close()` to release resources
206
- owned by the handler. These powers are why installation is a trust decision and
207
- why contained interoperability belongs in MCP rather than an in-process plugin.
487
+ caches, pools, or other host resources under {§handler-lifecycle}. These powers
488
+ are why installation is a trust decision and why contained interoperability
489
+ belongs in MCP rather than an in-process plugin.
208
490
 
209
491
  The supported compatibility boundary is `@plurnk/plurnk-schemes`. Plugins
210
492
  should not import private service modules, depend on database layout, or call
@@ -212,13 +494,13 @@ prepared statements directly: those are unstable implementation details, not
212
494
  additional plugin capabilities. Use `SchemeCtx` or propose a new semantic
213
495
  capability when the public surface cannot express a coherent extension.
214
496
 
215
- ## §6 Discovery & registration (third-party)
497
+ ## §scheme-discovery §6 Discovery & registration (third-party)
216
498
 
217
499
  A scheme handler is discovered and registered with **zero first-party involvement** — install it, it lights up. The contract:
218
500
 
219
- - **Declare** `plurnk.kind: "scheme"` in `package.json` — or, for a package owning multiple capability families, an array that **includes** `"scheme"` (`kind: ["exec", "scheme"]`, #483); every family scanner accepts a package whose kind is or includes its own, string form as the single-kind sugar. Then name the scheme(s) it owns, in one of two forms: `plurnk.schemes: [{ name, export }, …]` (canonical — one entry per scheme, `export` naming the handler-class export) or `plurnk.name: "<scheme>"` (sugar for exactly one scheme, the `default` export). One package may own several names; each name has exactly one owner (#473). This mirrors execs' `plurnk.runtimes: [{ name, glyph, example }]` — `plurnk.kind` plus a plural family-noun array of named capabilities is the shared manifest covenant across families; `export` is schemes' family-specific field (a scheme instantiates a class per name, where an executor dispatches tags through one).
220
- - **`SchemeDiscovery` owns the scan (this package).** `SchemeDiscovery.discover({ cwd? })` walks *all* of `node_modules` — scoped (`@acme/foo`) and unscoped — and returns `{ schemes: {name, packageName, exportName?, attribution?}[], skipped }` for every package declaring `plurnk.kind === "scheme"`. Scope-agnostic, so a third party under their own scope is found with no first-party allow-list (plurnk-service#227); two names claiming one prefix fail-hard (across packages or within one), as does a malformed `plurnk.schemes` (locality of error, not a silent skip). It returns **descriptors, not handlers** — contract-only, it never imports a scheme package; the consumer imports each `packageName` and registers `new mod[exportName ?? "default"]()`, applying in-tree precedence. The scan primitives — package enumeration, the `PLURNK_PLUGINS_TRUSTED_ONLY` trust gate, the deployment-root `node_modules` walk — are one implementation in `@plurnk/plurnk-meta`, shared by all four family-head scanners; `SchemeDiscovery` adds only the scheme-descriptor shape on top.
221
- - **`attribution` rides the descriptor verbatim.** A package may declare `plurnk.attribution` (a credit string, or an array of them); the scan passes it through untouched as `SchemeInfo.attribution` (`string | string[] | undefined`) — anything that isn't a string or string-array is dropped. The framework neither validates nor normalizes it: the `@plurnk`-tags-only-from-`@plurnk`-packages reservation policy is the **consumer's** to enforce on the surfaced credit (plurnk-service#26).
501
+ - **Declare** the exact string `plurnk.kind: "scheme"` in `package.json` ({§plugin-family-kind}). Then name the scheme(s) it owns in one of two forms: `plurnk.schemes: [{ name, export }, …]` (canonical — one entry per scheme, `export` naming the handler-class export) or `plurnk.name: "<scheme>"` (one-scheme shorthand for the `default` export). One package may own several names inside this family; each name has exactly one owner.
502
+ - **`SchemeDiscovery` owns the scan (this package).** `SchemeDiscovery.discover({ cwd? })` walks *all* of `node_modules` — scoped (`@acme/foo`) and unscoped — and returns `{ schemes: {name, packageName, exportName?, attribution?}[], packageAttributions, skipped }` for every package declaring `plurnk.kind === "scheme"`. Scope-agnostic, so a third party under their own scope is found with no first-party allow-list; two names claiming one prefix fail-hard (across packages or within one), as does a malformed `plurnk.schemes` (locality of error, not a silent skip). It returns **descriptors, not handlers** — contract-only, it never imports a scheme package; the consumer imports each `packageName` and registers `new mod[exportName ?? "default"]()`, applying in-tree precedence. The scan primitives — package enumeration, the `PLURNK_PLUGINS_TRUSTED_ONLY` trust gate, the deployment-root `node_modules` walk — are one implementation in `@plurnk/plurnk-meta`, shared by all four family-head scanners; `SchemeDiscovery` adds only the scheme-descriptor shape on top.
503
+ - **Attribution is package-authored.** `packageAttributions` carries one canonical validated static tag list per admitted package. `SchemeInfo.attribution` remains the published per-scheme projection when a declaration exists; a loaded handler may additionally implement the shared runtime hook ({§plugin-attribution}). Neither the descriptor nor the consumer owns another policy.
222
504
  - **The framework stays contract-only.** `@plurnk/plurnk-schemes` does not depend on scheme plugins. The daemon declares its bundled plugins as direct dependencies, and additional plugins are installed at the application root. Plugins declare the framework as a peer dependency using the repository's normal same-minor compatibility range; the framework itself is ignored by discovery because it has no `plurnk.kind`.
223
505
  - **The default bundle is the daemon's own `dependencies`**, not an aggregator package (the `-all` metapackages are retired). Installing `plurnk-core` surfaces the first-party schemes; any other leaf — first-party or third-party — is added by installing it, and scope-agnostic discovery lights it up identically. No bundle is ever a gate.
224
- - **Trust.** An operator can require host-level trust before a discovered plugin registers (`PLURNK_PLUGINS_TRUSTED_ONLY`, plurnk-service#229) — the scope-agnostic scan widens reach, the trust gate bounds it.
506
+ - **Trust.** The scanner enforces the shared predicate before attribution or scheme-field validation and returns withheld package names in `skipped`; the host owns presentation ({§plugin-trust-boundary}).