@plurnk/plurnk-schemes 1.3.12 → 1.4.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 +25 -14
  2. package/SPEC.md +311 -75
  3. package/dist/DefaultRead.d.ts +8 -4
  4. package/dist/DefaultRead.d.ts.map +1 -1
  5. package/dist/DefaultRead.js +37 -13
  6. package/dist/DefaultRead.js.map +1 -1
  7. package/dist/Manifest.d.ts +6 -0
  8. package/dist/Manifest.d.ts.map +1 -0
  9. package/dist/Manifest.js +115 -0
  10. package/dist/Manifest.js.map +1 -0
  11. package/dist/Matcher.d.ts +5 -6
  12. package/dist/Matcher.d.ts.map +1 -1
  13. package/dist/Matcher.js +51 -59
  14. package/dist/Matcher.js.map +1 -1
  15. package/dist/MimetypeClassifier.d.ts +1 -1
  16. package/dist/MimetypeClassifier.d.ts.map +1 -1
  17. package/dist/MimetypeClassifier.js +11 -15
  18. package/dist/MimetypeClassifier.js.map +1 -1
  19. package/dist/NetworkAddress.d.ts +13 -0
  20. package/dist/NetworkAddress.d.ts.map +1 -0
  21. package/dist/NetworkAddress.js +43 -0
  22. package/dist/NetworkAddress.js.map +1 -0
  23. package/dist/OutputScheme.d.ts.map +1 -1
  24. package/dist/OutputScheme.js +5 -6
  25. package/dist/OutputScheme.js.map +1 -1
  26. package/dist/PacketSections.d.ts +6 -0
  27. package/dist/PacketSections.d.ts.map +1 -0
  28. package/dist/PacketSections.js +42 -0
  29. package/dist/PacketSections.js.map +1 -0
  30. package/dist/PathMimetype.js +9 -9
  31. package/dist/PathMimetype.js.map +1 -1
  32. package/dist/Results.d.ts +29 -8
  33. package/dist/Results.d.ts.map +1 -1
  34. package/dist/Results.js +102 -44
  35. package/dist/Results.js.map +1 -1
  36. package/dist/SchemeDiscovery.d.ts +2 -0
  37. package/dist/SchemeDiscovery.d.ts.map +1 -1
  38. package/dist/SchemeDiscovery.js +40 -42
  39. package/dist/SchemeDiscovery.js.map +1 -1
  40. package/dist/SchemeResolver.d.ts.map +1 -1
  41. package/dist/SchemeResolver.js +4 -5
  42. package/dist/SchemeResolver.js.map +1 -1
  43. package/dist/Slicer.d.ts +36 -14
  44. package/dist/Slicer.d.ts.map +1 -1
  45. package/dist/Slicer.js +330 -362
  46. package/dist/Slicer.js.map +1 -1
  47. package/dist/Summarize.js +3 -3
  48. package/dist/Summarize.js.map +1 -1
  49. package/dist/ctx.d.ts +60 -60
  50. package/dist/ctx.d.ts.map +1 -1
  51. package/dist/edit-receipt.d.ts +39 -0
  52. package/dist/edit-receipt.d.ts.map +1 -0
  53. package/dist/edit-receipt.js +2 -0
  54. package/dist/edit-receipt.js.map +1 -0
  55. package/dist/handler.d.ts +7 -8
  56. package/dist/handler.d.ts.map +1 -1
  57. package/dist/handler.js +4 -2
  58. package/dist/handler.js.map +1 -1
  59. package/dist/index.d.ts +13 -5
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +6 -0
  62. package/dist/index.js.map +1 -1
  63. package/dist/packet.d.ts +2 -3
  64. package/dist/packet.d.ts.map +1 -1
  65. package/dist/packet.js +2 -20
  66. package/dist/packet.js.map +1 -1
  67. package/dist/types.d.ts +3 -11
  68. package/dist/types.d.ts.map +1 -1
  69. package/dist/types.js +1 -7
  70. package/dist/types.js.map +1 -1
  71. package/package.json +10 -8
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,21 +34,39 @@ 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 or READ is a collection scope. Absent/false means `/` is ordinary resource syntax. Matcher bodies and explicit globs remain queries independently. |
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
 
@@ -61,21 +78,19 @@ dispatch. An absent method returns **501**, except FIND on an entry-bearing
61
78
  FIND automatically. The op-dispatch surface is the exported
62
79
  **`SchemeHandler`** interface — every method optional, each
63
80
  `(statement, ctx) => Promise<SchemeResult>`, the per-op statement type from
64
- grammar:
81
+ grammar. `editBatch` returns the typed `EditBatchResult` specialization:
65
82
 
66
83
  ```ts
67
- import type { SchemeHandler } from "@plurnk/plurnk-schemes";
84
+ import type { EditBatchResult, SchemeHandler } from "@plurnk/plurnk-schemes";
68
85
 
69
86
  export interface SchemeHandler {
87
+ ready?(): Promise<void>;
70
88
  close?(): Promise<void>;
89
+ resolveEntryAddress?(target: ParsedPath, ctx: SchemeCtx): Promise<EntryAddress | null>;
71
90
  prepareFind?(statement: FindStatement, ctx: SchemeCtx): Promise<SchemeResult>;
72
91
  read?(statement: ReadStatement, ctx: SchemeCtx): Promise<SchemeResult>;
73
92
  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>;
93
+ editBatch?(statements: readonly EditStatement[], ctx: SchemeCtx): Promise<EditBatchResult>;
79
94
  send?(statement: SendStatement, ctx: SchemeCtx): Promise<SchemeResult>;
80
95
  exec?(statement: ExecStatement, ctx: SchemeCtx): Promise<SchemeResult>;
81
96
  work?(statement: WorkStatement, ctx: SchemeCtx): Promise<SchemeResult>;
@@ -87,55 +102,232 @@ export interface SchemeHandler {
87
102
  ```
88
103
 
89
104
  `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
105
+ discovery/materialization seam invoked before the consumer's standard resource
106
+ selection when a data scheme has no custom `find()`. Stored schemes omit it.
107
+ FIND and matcher READ both invoke it before selecting entries. Acquisition
92
108
  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.
109
+ then receive the same catalog, matcher evidence, weight, pagination, and status
110
+ semantics as every other entry-bearing scheme. The consumer resolves the
111
+ standard query through the same canonical entry identity used by
112
+ `ctx.entries`: a URL authority remains part of that identity and cannot be
113
+ dropped between preparation and lookup. A custom `find()` replaces the whole
114
+ operation only where the scheme owns genuinely different candidate semantics.
115
+
116
+ §entry-address-resolution `resolveEntryAddress?` gives a client observation the
117
+ same address law as model-facing operations without creating a second CRUD
118
+ protocol. It returns the canonical stored `pathname` and a semantic owner:
119
+
120
+ | Return | Meaning |
121
+ |-----------------------------------------|---------------------------------------------------|
122
+ | `{ pathname, owner: "commons" }` | Resolve in the shared workspace namespace |
123
+ | `{ pathname, owner: "worker" }` | Resolve in the calling worker's namespace |
124
+ | `null` | The selector names no client-visible entry |
125
+ | Hook absent | Use the standard pathname and `commons` ownership |
126
+
127
+ The consumer supplies the observing worker through `SchemeCtx`, lowers the
128
+ semantic owner to its private storage identity, and performs the read. A plugin
129
+ never receives or returns database owner IDs. Schemes implement the hook only
130
+ when their standard operation path has scheme-specific canonicalization or
131
+ caller ownership; the hook must apply that same rule rather than inventing a
132
+ client-only address vocabulary.
133
+
134
+ 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).
135
+
136
+ §handler-lifecycle A registered handler object is a process-lived shared
137
+ instance and may receive overlapping operation calls. Retained handler state is
138
+ ordinary; handlers must make concurrent access and semantic isolation explicit.
139
+
140
+ | Concern | Contract |
141
+ | ------------------ | -------- |
142
+ | Readiness | `ready?()` runs after registration and before capability advertisement. It establishes that advertised resources are usable. |
143
+ | Hook cardinality | The consumer invokes each hook once per unique handler object identity, even when multiple registered names share that object. |
144
+ | Operation context | `SchemeCtx` and capabilities extracted from it remain operation-owned under {§scheme-ctx-lifetime}; only the `StreamSubscription` returned by `subscriptions.open` is retainable. |
145
+ | 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. |
146
+ | Live ownership | An address-selectable live resource has one unambiguous owner. Conflicting acquisition fails explicitly; it never overwrites the existing owner. |
147
+ | 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. |
148
+
149
+ Handlers without initialization or retained resources omit the corresponding
150
+ hook.
97
151
 
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).
152
+ 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
153
 
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.
154
+ OPEN and FOLD are not handler methods. They curate visibility and tags on the
155
+ core-owned log; an entry scheme has no visibility state and receives 501.
101
156
 
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.
157
+ §scheme-edit-batch-receipt **Regional mutation receipts.** COPY and MOVE are not
158
+ handler methods. The engine composes their source and
159
+ destination resource selections over `ctx.entries` and uses `editBatch` for a
160
+ scoped destination or source mutation. This keeps channel selection, snapshot
161
+ ordering, cross-scheme failures, and proposal sequencing uniform for every data
162
+ scheme. Scheme hooks return their ordinary mutation result and, for a regional
163
+ `editBatch`, an `EditBatchResult` carrying its aggregate `EditBatchReceipt`:
164
+
165
+ ```ts
166
+ type EditReceiptUnit = "lines" | "codePoints";
167
+
168
+ interface EditEffectReceipt {
169
+ readonly requested: string;
170
+ readonly source: string;
171
+ readonly result: string;
172
+ readonly removed: number;
173
+ readonly inserted: number;
174
+ readonly context: string;
175
+ }
176
+
177
+ interface AppliedEditBatchReceipt {
178
+ readonly revision: string;
179
+ readonly unit: EditReceiptUnit;
180
+ readonly before: number;
181
+ readonly after: number;
182
+ readonly effects: readonly EditEffectReceipt[];
183
+ }
184
+
185
+ interface ReviewerReplacementEditBatchReceipt {
186
+ readonly revision: string;
187
+ readonly unit: EditReceiptUnit;
188
+ readonly before: number;
189
+ readonly after: number;
190
+ readonly disposition: "reviewer-replaced";
191
+ readonly superseded: readonly string[];
192
+ readonly replacement: EditEffectReceipt;
193
+ }
194
+
195
+ type EditBatchReceipt =
196
+ | AppliedEditBatchReceipt
197
+ | ReviewerReplacementEditBatchReceipt;
198
+
199
+ interface EditBatchResult extends SchemeResult {
200
+ readonly editReceipt?: EditBatchReceipt | null;
201
+ }
202
+ ```
203
+
204
+ | Batch outcome | Authored correlation | Landed effects |
205
+ | ------------------------------------ | ------------------------------------------------- | --------------------------------------------------------------------- |
206
+ | Applied as proposed | `effects[N]` describes authored EDIT `N` | One truthful effect per authored EDIT |
207
+ | Resolver replaced the proposed body | `superseded[N]` retains authored marker `N` | One `replacement` effect describes the bytes that actually landed |
208
+
209
+ The replacement form is not a heuristic attribution. An arbitrary resolver
210
+ body supersedes every authored EDIT because its correspondence to those edits
211
+ cannot be known. The consumer projects the replacement once on the durable
212
+ proposal-owning row and projects each authored marker's superseded disposition
213
+ on its own row.
214
+
215
+ The engine validates that receipt and owns the ordered COPY/MOVE resource
216
+ effects shown to consumers; plugins do not invent a second effect envelope.
103
217
 
104
218
  ## §3 Helpers exported by this repo
105
219
 
106
220
  ### Types
107
221
 
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`).
222
+ - Manifest/flags: `SchemeManifest`, `SchemeFlagAffinity`, and `WriterTier`; contracts-owned `LoopFlags` and `DEFAULT_LOOP_FLAGS` are re-exported for compatibility.
223
+ - Mutation receipts: `EditBatchResult`, `EditBatchReceipt`, `EditReceipt`, `EditEffectReceipt`, and `EditReceiptUnit`.
224
+ - §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.
225
+ - 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`).
226
+ - Target syntax: contracts-owned `PathSyntax` is re-exported for the shared exact-versus-path-glob classifier {§path-glob}.
111
227
  - 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.
228
+ - §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); `DefaultRead.read(content, mimetype, statement, mimetypes)` -> `ReadResolution` is the free text-scope/matcher read over produced output (reuses `Slicer`/`Matcher`). A matcher selects the complete output resource before `<scope>` projects text; without a scope, READ returns the complete resource. Match evidence remains metadata for a model-chosen follow-up READ. `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?`).
229
+ - 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.
230
+ - Standard FIND results may carry `omittedItems` and `maximumItems` when a selected catalog is too large to enumerate. `omittedItems` is the exact selected-resource count and `maximumItems` is the active materialization limit. These names cannot collide with the model-facing string `overflow` metadata used for truncated packet bodies.
231
+ - 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
232
 
116
233
  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
234
 
235
+ ### §network-address Network address identity
236
+
237
+ `NetworkAddress.from(target)` is the single non-secret normalization path for
238
+ HTTP and WebSocket targets. It returns the exact addressed `scheme`, the
239
+ canonical entry `pathname`, a fragmentless and credential-free transport
240
+ `url`, and whether userinfo was present so the handler can reject it.
241
+
242
+ | Component | Entry identity | Transport URL | Plurnk channel |
243
+ |------------------|-----------------------------------------------------|---------------|----------------|
244
+ | Scheme | Exact `http`, `https`, `ws`, or `wss` | Yes | No |
245
+ | Host | Canonical WHATWG hostname | Yes | No |
246
+ | Port | Non-default port | Yes | No |
247
+ | Path | Canonical pathname | Yes | No |
248
+ | Query | Serialized order, duplicates, and explicit empty `?` | Yes | No |
249
+ | Fragment | No | No | Yes |
250
+ | Userinfo/headers | No | No | No |
251
+
252
+ The canonical storage form is
253
+ `/<host>[:<port>]<path>[?<query>]`, keyed together with workspace, owner, and
254
+ the exact addressed scheme. `NetworkAddress.render(scheme, pathname)` is its
255
+ model-facing inverse: it applies the Plurnk lexical target spelling
256
+ {§path-parentheses} after reconstructing the exact address. Transport URLs and
257
+ storage identity never carry that syntax layer. Routing aliases select a
258
+ handler; they do not collapse resource identity.
259
+
118
260
  ### Active-scheme resolution — `SchemeResolver`
119
261
 
120
262
  - `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
263
 
122
- ### Mimetype classification — `MimetypeClassifier` {§mimetype-classifier}
264
+ ### §mimetype-classifier Mimetype classification — `MimetypeClassifier`
123
265
 
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`.
266
+ - `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}).
267
+ - `MimetypeClassifier.isHtml(mimetype)` — recognizes the normalized web-projection family: `text/html` and `application/xhtml+xml`.
268
+ - `MimetypeClassifier.isJson(mimetype)` - `application/json` plus `+json` variants, used only by result summarization.
127
269
  - `MimetypeClassifier.normalizeAutoText(mimetype)` — `text/plain` / null / undefined → `TEXT_PRIMITIVE_MIMETYPE` (`text/markdown`).
128
270
  - `TEXT_PRIMITIVE_MIMETYPE` — `"text/markdown"` (named export from the same module).
129
271
 
130
- ### `<L>` slicing — `Slicer`
272
+ ### §scheme-projection Projection capability
273
+
274
+ Acquisition schemes delegate model-facing projection to the consumer's one
275
+ configured mimetype family. They neither instantiate readers nor widen durable
276
+ entry channels beyond Unicode text.
277
+
278
+ | Surface | Contract |
279
+ | --------------------------------- | ----------------------------------------------------------------------------------------------------- |
280
+ | `readable(content, mimetype)` | Project an acquired Unicode representation. |
281
+ | `readableBytes(chunks, mimetype)` | Project one async byte source under the mimetype family's bounded-input policy. |
282
+ | `identity(mimetype)` | Return the opaque identity of configured projection behavior for cache and materialization freshness. |
283
+ | `isBinary(mimetype)` | Classify through the configured registry; installed handler declarations remain authoritative. |
284
+ | `ProjectedText` | Derived Unicode plus its output mimetype, source mimetype, and opaque projection identity. |
285
+
286
+ A returned object is present even when its `content` is `""`; only `null`
287
+ denotes absence. Consumers must not infer projection presence from content
288
+ length. A thrown projection call is an execution failure whose cause
289
+ propagates; it must never be converted to `null`.
290
+ `ProjectionInputLimitError` is the projection boundary's exported name for the
291
+ configured mimetype family's typed bounded-input failure.
292
+
293
+ ### §slicer-text-algebra Text-region slicing and replacement - `Slicer`
131
294
 
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.
295
+ `Slicer` owns one text algebra for every textual mimetype:
137
296
 
138
- ### Path-extension mimetype — `PathMimetype`
297
+ | Scope | Meaning |
298
+ |---|---|
299
+ | `<N>` | whole physical line `N` |
300
+ | `<N,M>` | inclusive whole physical lines `N..M` |
301
+ | `<SL,SC,EL,EC>` | exact exclusive-end region using 1-based Unicode code-point columns |
302
+ | `<0>` / `<-1>` | mutation anchors before the first / after the final line |
303
+
304
+ As an unadvertised ingestion tolerance, `Slicer` accepts
305
+ `<startLine,startColumn,endLine>` and immediately lowers it to the complete
306
+ four-coordinate region ending after the final code point of `endLine`.
307
+ Successful results carry schemes-owned, boundary-validated normalization
308
+ evidence containing both the authored and canonical coordinates. Producers do
309
+ not emit or proactively teach the tolerated form.
310
+
311
+ - `Slicer.lines(content, marker)` returns the selected text, source
312
+ `startLine`, and complete resolved `region`; `Slicer.linesRaw` preserves
313
+ original newline separators for COPY/MOVE source transfer.
314
+ - `Slicer.textReplacement(content, marker, body)` lowers a line shorthand or
315
+ exact region to one `{start,end,body}` replacement against the source
316
+ snapshot.
317
+ - `Slicer.lineMarkerEdit` applies one replacement.
318
+ - `Slicer.lineMarkerEditBatch` validates all replacements against one snapshot,
319
+ rejects overlaps, and applies all or none.
320
+ - `Slicer.page(items, marker)` is the separate ordered-result pagination
321
+ helper; it accepts only one or two integer positions.
322
+
323
+ A line/pagination 416 carries `range: { unit, requested: { first, last },
324
+ available: { first, last, total } }`. An exact-region failure, including a
325
+ failed tolerated three-coordinate scope, carries `requestedCoordinates`
326
+ exactly as authored; an invalid-arity text scope does likewise. Both carry
327
+ `columnKind: "unicodeCodePoints"`. Empty sources use `null` line-range
328
+ endpoints and `total: 0`.
329
+
330
+ ### §path-extension-mimetype Path-extension mimetype — `PathMimetype`
139
331
 
140
332
  - `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
333
 
@@ -143,19 +335,34 @@ Behavior ships as `export default class` (one class per file, static methods)
143
335
 
144
336
  - `Results.isEntry` / `Results.isProposal` / `Results.isPassthrough` — guards for the optional conventional result shapes.
145
337
  - `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}
338
+ - `Results.problem(owner, code, status, detail, extensions?)` — build and validate RFC 9457 Problem Details with a stable `https://problems.plurnk.dev/<owner>/<code>` type.
339
+ - `Results.failure(owner, code, status, detail, fields?, extensions?)` — build and validate a failed operation result.
340
+ - `Results.assert(result)` — validate the complete success/failure discrimination and reject malformed plugin output.
341
+ - `Results.assertMatchEvidence(evidence)` / `assertMatchEvidenceList(evidence)` - enforce the exact `{ path?, region? }` shape and shared `TextRegion` contract.
342
+ - `Results.assertReadResult(result)` - validate the universal operation result plus any `region` and `matches` it exposes.
343
+ - `Results.attachInstance(result, uri)` — attach the durable occurrence URI to a failed result.
344
+
345
+ A handler owns its failure classification and explanation. The daemon owns the
346
+ durable `instance`, because only it knows the committed log coordinate. A
347
+ malformed handler result is a plugin contract violation and fails hard; the
348
+ consumer does not invent a fallback error or reinterpret arbitrary fields.
349
+ The same discrimination applies to every `SchemeCtx` capability result:
350
+ entries, channels, and tags never return a bare failure status.
351
+
352
+ ### Matcher dispatch - `Matcher` {§matcher-dispatch}
353
+
354
+ - `Matcher.matchAgainstContent(body, content, mimetype, mimetypes)` is the body-matcher adapter over `Mimetypes.query` (glob/regex/jsonpath/xpath).
355
+ - A match returns status 200 and `matches: MatchEvidence[]`.
356
+ `MatchEvidence` is `{path?, region?}`. `path` preserves a structural locator;
357
+ `region` is a complete four-coordinate `TextRegion` only when the finding has
358
+ an honest exact or nearest-enclosing mapping into the text the model can READ.
359
+ Each item must contain at least one of `path` or `region`; other fields violate
360
+ the shared evidence contract.
361
+ - The matcher is a boolean resource selector. It does not replace content with matched values or choose a retrieval window. `DefaultRead` returns the complete selected resource unless the authored READ supplies `<L>`.
362
+ - 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`.
363
+ - 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.
364
+
365
+ ### §capability-ctx §3.bis Capability ctx — the stable trusted-extension surface
159
366
 
160
367
  Scheme plugins are trusted in-process Node.js code. `SchemeCtx` is not a
161
368
  sandbox or a security boundary; an installed plugin already has the process's
@@ -169,6 +376,8 @@ its implementation.
169
376
  - `entries` — direct storage over the scheme's own namespace
170
377
  (`read`/`write`/`delete`) plus `operations`, the standard PLURNK
171
378
  `READ`/`EDIT`/`FIND`/`SEND` implementation for entry-bearing schemes.
379
+ A write may omit channel state to select the `static` default; a successful
380
+ storage read always returns each channel's persisted lifecycle state.
172
381
  Standard operations are bound to the handler's manifest. Their optional
173
382
  owner is semantic: `"commons"` (default) or the current `"worker"`; database
174
383
  owner IDs are not part of the plugin contract. A handler may implement its
@@ -176,11 +385,23 @@ its implementation.
176
385
  fan-out; one without it receives the standard stored-entry behavior.
177
386
  - `channels` — content writes + state (`append`/`replace`/`setState`).
178
387
  - `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).
388
+ - `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.)
389
+ - `projection` — the text and bounded-byte projection capability in {§scheme-projection}. Acquisition schemes own bytes/DOM; they do not instantiate or second-guess the reader family. `null` means no readable projection.
390
+ - §scheme-subscriptions `subscriptions` — one streaming lifecycle:
182
391
 
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).
392
+ | Surface | Lifetime | Contract |
393
+ | --------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------- |
394
+ | `open(pathname, handle)` | Operation | Bind durable and live identity, return the exact object, and route cancellation through its signal/handle. |
395
+ | Returned `StreamSubscription` | Retainable | `AbortSignal` plus fused chunk publication and terminal settlement; the only retainable capability. |
396
+ | `subscriptions.notifyChunk/close` | Operation | Forward to the exact returned object; own no second state or persistence path. |
397
+
398
+ `notifyChunk` appends and emits one stream event. Its optional stateless
399
+ `mimetype` retypes the channel only when the stored type differs.
400
+ `close` validates and persists the exact universal `SchemeResult`, settles
401
+ channel state, unregisters the live handle, and wakes the worker. `summary` is
402
+ presentation only; it never replaces or reconstructs the result.
403
+
404
+ 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
405
 
185
406
  The consumer passes exactly this public context to every handler. Bundled
186
407
  adapters that also implement daemon-owned lifecycle behavior receive those
@@ -188,7 +409,22 @@ collaborators separately at construction or binding time; consumers must not
188
409
  decorate `SchemeCtx` with database handles, registries, tokenizers, notifier
189
410
  callbacks, or other private service state.
190
411
 
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.
412
+ §scheme-ctx-lifetime `SchemeCtx` belongs to one operation call. A handler may
413
+ use it for the duration of that call, including asynchronous work awaited by
414
+ the call, but must not retain the context or any other extracted capability
415
+ after the handler returns. A streaming handler may retain only the exact
416
+ `StreamSubscription` returned by `subscriptions.open`; its bound methods carry
417
+ that subscription's identity without retaining the general context. Other
418
+ retained handler-owned resources follow {§handler-lifecycle}.
419
+
420
+ **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.
421
+
422
+ §scheme-edit-proposal-receipt An accepted EDIT proposal may return its final
423
+ aggregate `editReceipt` alongside its per-row model-facing `result`. A resolver
424
+ replacement must use the reviewer-replaced form from
425
+ {§scheme-edit-batch-receipt}. Core consumes that aggregate as transient batch
426
+ coordination and COPY/MOVE composition input; it persists only per-row receipts
427
+ or ordered resource effects.
192
428
 
193
429
  ## §4 What's NOT in this repo
194
430
 
@@ -199,12 +435,12 @@ Raw CRUD methods are not a parallel handler protocol. For `data` schemes whose
199
435
  handlers omit resource-specific storage hooks, engine-owned COPY/MOVE/KILL
200
436
  orchestration uses the manifest-bound `ctx.entries` implementation.
201
437
 
202
- ## §5 Trusted extension contract {§trusted-extension}
438
+ ## §trusted-extension §5 Trusted extension contract
203
439
 
204
440
  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.
441
+ caches, pools, or other host resources under {§handler-lifecycle}. These powers
442
+ are why installation is a trust decision and why contained interoperability
443
+ belongs in MCP rather than an in-process plugin.
208
444
 
209
445
  The supported compatibility boundary is `@plurnk/plurnk-schemes`. Plugins
210
446
  should not import private service modules, depend on database layout, or call
@@ -212,13 +448,13 @@ prepared statements directly: those are unstable implementation details, not
212
448
  additional plugin capabilities. Use `SchemeCtx` or propose a new semantic
213
449
  capability when the public surface cannot express a coherent extension.
214
450
 
215
- ## §6 Discovery & registration (third-party)
451
+ ## §scheme-discovery §6 Discovery & registration (third-party)
216
452
 
217
453
  A scheme handler is discovered and registered with **zero first-party involvement** — install it, it lights up. The contract:
218
454
 
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).
455
+ - **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.
456
+ - **`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.
457
+ - **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
458
  - **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
459
  - **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.
460
+ - **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}).
@@ -1,9 +1,13 @@
1
- import type { ReadStatement } from "@plurnk/plurnk-grammar";
1
+ import type { ReadStatement } from "@plurnk/plurnk-contracts";
2
+ import type { TextRegion } from "@plurnk/plurnk-contracts";
2
3
  import type { Mimetypes } from "@plurnk/plurnk-mimetypes";
3
- export interface ReadResolution {
4
- readonly status: number;
4
+ import type { RangeExtent } from "./Slicer.ts";
5
+ import type { SchemeResult } from "./Results.ts";
6
+ export interface ReadResolution extends SchemeResult {
5
7
  readonly body?: string;
6
- readonly error?: string;
8
+ readonly startLine?: number;
9
+ readonly region?: TextRegion;
10
+ readonly range?: RangeExtent;
7
11
  }
8
12
  export default class DefaultRead {
9
13
  static read(content: string, mimetype: string, statement: ReadStatement, mimetypes: Mimetypes): Promise<ReadResolution>;
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultRead.d.ts","sourceRoot":"","sources":["../src/DefaultRead.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAI1D,MAAM,WAAW,cAAc;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,CAAC,OAAO,OAAO,WAAW;IAK5B,OAAa,IAAI,CACb,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,aAAa,EACxB,SAAS,EAAE,SAAS,GACrB,OAAO,CAAC,cAAc,CAAC,CAUzB;CACJ"}
1
+ {"version":3,"file":"DefaultRead.d.ts","sourceRoot":"","sources":["../src/DefaultRead.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAI1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,MAAM,WAAW,cAAe,SAAQ,YAAY;IAChD,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;CAChC;AAED,MAAM,CAAC,OAAO,OAAO,WAAW;IAK5B,OAAa,IAAI,CACb,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,aAAa,EACxB,SAAS,EAAE,SAAS,GACrB,OAAO,CAAC,cAAc,CAAC,CA6BzB;CACJ"}