@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.
- package/README.md +43 -17
- package/SPEC.md +368 -86
- package/dist/Manifest.d.ts +6 -0
- package/dist/Manifest.d.ts.map +1 -0
- package/dist/Manifest.js +115 -0
- package/dist/Manifest.js.map +1 -0
- package/dist/Matcher.d.ts +5 -6
- package/dist/Matcher.d.ts.map +1 -1
- package/dist/Matcher.js +51 -59
- package/dist/Matcher.js.map +1 -1
- package/dist/MimetypeClassifier.d.ts +1 -1
- package/dist/MimetypeClassifier.d.ts.map +1 -1
- package/dist/MimetypeClassifier.js +11 -15
- package/dist/MimetypeClassifier.js.map +1 -1
- package/dist/NetworkAddress.d.ts +13 -0
- package/dist/NetworkAddress.d.ts.map +1 -0
- package/dist/NetworkAddress.js +43 -0
- package/dist/NetworkAddress.js.map +1 -0
- package/dist/OutputScheme.d.ts.map +1 -1
- package/dist/OutputScheme.js +5 -6
- package/dist/OutputScheme.js.map +1 -1
- package/dist/PacketSections.d.ts +6 -0
- package/dist/PacketSections.d.ts.map +1 -0
- package/dist/PacketSections.js +42 -0
- package/dist/PacketSections.js.map +1 -0
- package/dist/PathMimetype.js +9 -9
- package/dist/PathMimetype.js.map +1 -1
- package/dist/Results.d.ts +42 -8
- package/dist/Results.d.ts.map +1 -1
- package/dist/Results.js +132 -44
- package/dist/Results.js.map +1 -1
- package/dist/SchemeDiscovery.d.ts +2 -0
- package/dist/SchemeDiscovery.d.ts.map +1 -1
- package/dist/SchemeDiscovery.js +40 -42
- package/dist/SchemeDiscovery.js.map +1 -1
- package/dist/SchemeResolver.d.ts.map +1 -1
- package/dist/SchemeResolver.js +4 -5
- package/dist/SchemeResolver.js.map +1 -1
- package/dist/Slicer.d.ts +27 -14
- package/dist/Slicer.d.ts.map +1 -1
- package/dist/Slicer.js +382 -374
- package/dist/Slicer.js.map +1 -1
- package/dist/Summarize.js +3 -3
- package/dist/Summarize.js.map +1 -1
- package/dist/ctx.d.ts +70 -67
- package/dist/ctx.d.ts.map +1 -1
- package/dist/edit-receipt.d.ts +39 -0
- package/dist/edit-receipt.d.ts.map +1 -0
- package/dist/edit-receipt.js +2 -0
- package/dist/edit-receipt.js.map +1 -0
- package/dist/handler.d.ts +13 -10
- package/dist/handler.d.ts.map +1 -1
- package/dist/handler.js +7 -5
- package/dist/handler.js.map +1 -1
- package/dist/index.d.ts +14 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/packet.d.ts +2 -3
- package/dist/packet.d.ts.map +1 -1
- package/dist/packet.js +2 -20
- package/dist/packet.js.map +1 -1
- package/dist/types.d.ts +3 -11
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -7
- package/dist/types.js.map +1 -1
- package/package.json +10 -8
- package/dist/DefaultRead.d.ts +0 -11
- package/dist/DefaultRead.d.ts.map +0 -1
- package/dist/DefaultRead.js +0 -27
- 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
|
|
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
|
|
19
|
+
class Notes {
|
|
20
20
|
static manifest: SchemeManifest = {
|
|
21
|
-
name: "
|
|
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
|
|
39
|
-
| `category` | `"data"` (entry-bearing) \| `"logging"` (`log://` rows) \| `"control"` (addresses sister
|
|
40
|
-
| `
|
|
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
|
|
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
|
|
48
|
-
| `glyph?` |
|
|
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
|
|
50
|
-
| `storedScheme?` | Value persisted to `entries.scheme`, which may differ from the addressing `name`.
|
|
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
|
-
|
|
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.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
**`SchemeHandler`** interface
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
109
|
-
-
|
|
110
|
-
-
|
|
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 (
|
|
113
|
-
- Results: `SchemeResult` is the universal `
|
|
114
|
-
-
|
|
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`
|
|
297
|
+
### §mimetype-classifier Mimetype classification — `MimetypeClassifier`
|
|
123
298
|
|
|
124
|
-
- `MimetypeClassifier.isBinary(mimetype)` —
|
|
125
|
-
- `MimetypeClassifier.
|
|
126
|
-
- `MimetypeClassifier.
|
|
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
|
-
###
|
|
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
|
-
-
|
|
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
|
-
|
|
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.
|
|
147
|
-
- `Results.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
- `
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
176
|
-
|
|
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
|
|
180
|
-
- `projection` —
|
|
181
|
-
- `subscriptions` — streaming lifecycle:
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
206
|
-
|
|
207
|
-
|
|
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`
|
|
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
|
|
221
|
-
-
|
|
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.**
|
|
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}).
|