@plurnk/plurnk-schemes-http 1.4.0 → 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/SPEC.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # plurnk-schemes-http — Specification
2
2
 
3
3
  This package owns the `http(s)://` request/response scheme, the `ws(s)://`
4
- full-duplex scheme, plus automatic entry-acquisition and browser-rendering
5
- foundations. Both handlers implement the DB-free `SchemeCtx` author contract.
4
+ full-duplex scheme, plus automatic entry-acquisition, Tavily Extract, and local
5
+ HTML projection foundations. Both handlers implement the DB-free `SchemeCtx` author contract.
6
6
 
7
7
  ## §http-manifest §1 HTTP manifest
8
8
 
@@ -16,11 +16,11 @@ foundations. Both handlers implement the DB-free `SchemeCtx` author contract.
16
16
  | Requires web | `true` |
17
17
  | Default channel | `body` |
18
18
 
19
- | Channel | Seed type | Meaning |
20
- | -------- | -------------------------- | --------------------------------------------------------------- |
21
- | `body` | `application/octet-stream` | Source text, derived Unicode, binary marker, SSE data, or HTML |
22
- | `header` | `text/plain` | Status line, response headers, and package acquisition metadata |
23
- | `html` | `text/html` | Faithful HTML used to produce the readable body |
19
+ | Channel | Seed type | Meaning |
20
+ | -------- | --------------------------- | ------------------------------------------------------------------------------ |
21
+ | `body` | `application/octet-stream` | Source text, derived Unicode, binary marker, SSE data, or HTML-page Markdown |
22
+ | `header` | `text/plain` | Origin, acquisition, materializer, projection, provider, and usage evidence |
23
+ | `html` | `text/html` | Original server-source HTML when an HTML representation can be acquired |
24
24
 
25
25
  `package.json#plurnk.schemes` registers `http` through the default export and
26
26
  `wss` through `Ws`.
@@ -32,24 +32,23 @@ duplicate names, and an explicit empty `?` remain significant. The fragment is
32
32
  a Plurnk channel selector and never enters network identity or transport.
33
33
  Request metadata affects transport but not identity. URL userinfo is rejected;
34
34
  neither credentials nor metadata are reconstructed from `raw`. Within storage
35
- pathnames, `%28` and `%29` canonicalize to literal parentheses and renderers
36
- encode them again.
35
+ pathnames, `%28` and `%29` canonicalize to literal parentheses.
37
36
 
38
37
  ## §op-surface §2 HTTP operation surface
39
38
 
40
- | Operation | Remote action | Contract |
41
- | --------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------ |
42
- | Unscoped `READ(url)` | GET unless a fresh GET copy is usable | Seed, subscribe, materialize every channel, publish the selected channel, and return `102` |
43
- | Scoped `READ(url)<scope>` | None | Delegate selected-channel presence and scope projection to universal READ; never acquire |
44
- | `FIND(url)` / matcher `READ(url)` | GET only when acquisition is required | Prepare an exact entry, then use universal query, matcher, weighting, and pagination |
45
- | `FIND(pattern-url)` | None | Query already-materialized web entries; a path pattern does not discover the remote web |
46
- | `SEND[200](url):body:` | POST | Stream and persist the response under the addressed URL |
47
- | `EDIT(url):body:` | PUT | Replace the whole remote resource; a line marker is invalid |
48
- | `KILL(url)` | DELETE | Delete the remote resource and stream its response |
49
- | `SEND[410](url)` | None | Delete the local stored entry |
50
- | `SEND[499](url)` | Cancellation is engine-routed | The routed subscription handle aborts acquisition; scheme dispatch itself is a `200` no-op |
51
-
52
- Every direct network method uses the same streaming path. Request
39
+ | Operation | Remote action | Contract |
40
+ | --------------------------------- | ------------------------------------- | --------------------------------------------------------------------------------------- |
41
+ | Exact `READ(url)<scope?>` | GET unless a fresh GET copy is usable | Prepare one complete canonical representation, then let core select and project it |
42
+ | Exact `FIND(url)` | GET only when acquisition is required | Use the same preparation, then universal query, matcher, weighting, and pagination |
43
+ | `FIND(pattern-url)` | None | Query already-materialized web entries; a path pattern does not discover the remote web |
44
+ | `SEND[200](url):body:` | POST | Stream and persist the response under the addressed URL |
45
+ | `EDIT(url):body:` | PUT | Replace the whole remote resource; a line marker is invalid |
46
+ | `KILL(url)` | DELETE | Delete the remote resource and stream its response |
47
+ | `SEND[410](url)` | None | Delete the local stored entry |
48
+ | `SEND[499](url)` | Cancellation is engine-routed | The routed subscription handle aborts the live owner; scheme dispatch is a `200` no-op |
49
+
50
+ Finite GET uses scope-blind representation preparation; POST, PUT, DELETE,
51
+ and genuinely live GET responses retain the subscription path. Request
53
52
  headers are ordered target metadata: one trailing `{Key: value}` block per
54
53
  header. The loop `SEND[code]` is never the remote HTTP status; remote status and
55
54
  headers are persisted in `header`.
@@ -61,90 +60,63 @@ path-pattern grammar.
61
60
 
62
61
  ```mermaid
63
62
  flowchart TD
64
- op{"HTTP operation"}
65
-
66
- op -->|scoped READ| selected["Universal READ selects the channel,<br/>requires it to exist, and applies scope"]
67
-
68
- op -->|FIND or matcher READ| pattern{"Path pattern?"}
69
- pattern -->|yes| query["Universal catalog, matcher,<br/>weight, and pagination"]
70
- pattern -->|no| usable{"Usable exact entry?"}
71
- usable -->|yes| query
72
- usable -->|no| prefetch["WebFetcher GET"]
73
-
74
- op -->|unscoped READ| fresh{"Fresh stored GET?"}
75
- fresh -->|yes| replay["Seed → open → replay stored channels → close"]
76
- fresh -->|no| stream["Seed entry → open subscription"]
77
- op -->|POST, PUT, or DELETE| stream
78
-
79
- prefetch --> guard["Guard.fetch checks the target<br/>and every followed redirect"]
80
- guard -->|refused or unavailable| dead
81
- guard -->|response| prefetchType{"Accepted response?"}
82
- prefetchType -->|2xx HTML| prefetchHtml["Shared HTML materialization;<br/>render lazily only if absent"]
83
- prefetchType -->|2xx non-HTML| prefetchClass{"Configured media class"}
84
- prefetchType -->|dead| dead["404 not-materialized"]
85
- prefetchClass -->|text| prefetchText["Materialize complete UTF-8 text"]
86
- prefetchClass -->|binary| prefetchBinary["Bounded readable-byte projection"]
87
- prefetchHtml -->|present, including empty| materialize["Write entry"]
88
- prefetchHtml -->|absent| noProjection["422 no-readable-projection"]
89
- prefetchText --> materialize
90
- prefetchBinary -->|present, including empty| materialize
91
- prefetchBinary -->|absent| noProjection
92
- prefetchBinary -->|input ceiling| inputLimit["413 projection-input-limit"]
93
- materialize --> query
94
-
95
- stream --> response{"Response path"}
96
- response -->|304 with stored GET| restore["Restore stored channels and refresh stamp"]
97
- response -->|GET + HTML| render["Browser → rendered HTML"]
98
- response -->|GET + SSE| acquired["Persist header → READ 102"]
99
- acquired --> events["Detached event data → body chunks"]
100
- response -->|other| representation{"Body representation"}
101
- representation -->|none| empty["Header only"]
102
- representation -->|configured text| text["UTF-8 response chunks → body"]
103
- representation -->|configured binary| binaryProjection["Bounded readable-byte projection"]
104
- binaryProjection -->|present, including empty| projected["Derived Unicode → body"]
105
- binaryProjection -->|absent| binary["Cancel bytes → typed empty marker + 415"]
106
- binaryProjection -->|input ceiling| binaryLimit["Typed empty marker + 413"]
107
- restore --> close["Persist/publish → close exact result"]
108
- render --> directHtml["Shared HTML materialization"]
109
- directHtml -->|present, including empty| close
110
- directHtml -->|absent| noProjection
111
- events --> close
112
- empty --> close
113
- text --> close
114
- projected --> close
115
- binary --> close
116
- binaryLimit --> close
63
+ get["GET acquisition"] --> live{"text/event-stream?"}
64
+ live -->|yes| subscription["Seed canonical channels<br/>retain subscription; return 102"]
65
+ live -->|no| origin{"Finite origin outcome"}
66
+ origin -->|text/markdown| markdown["Use origin Markdown as body<br/>and request an HTML variant"]
67
+ origin -->|server HTML| source["Retain exact server HTML"]
68
+ origin -->|transport unavailable<br/>after public admission| noSource["No HTML source"]
69
+ source --> eligible{"Generic public request<br/>with Tavily configured?"}
70
+ noSource --> eligible
71
+ eligible -->|no| local["Installed HTML projection"]
72
+ eligible -->|yes| tavily["Tavily Extract Markdown"]
73
+ tavily -->|success| providerBody["Tavily body"]
74
+ tavily -->|recoverable failure<br/>and source exists| recovery["Local projection; body 203"]
75
+ tavily -->|hard failure or<br/>no recovery source| bodyError["Body errored"]
76
+ local -->|present, including empty| localBody["Local body"]
77
+ local -->|absent or no source| bodyError
78
+ markdown --> settle["Write complete canonical channels<br/>and durable producer results"]
79
+ providerBody --> settle
80
+ recovery --> settle
81
+ localBody --> settle
82
+ bodyError --> settle
83
+ settle --> core["Core selects channel<br/>and projects authored text scope"]
117
84
  ```
118
85
 
119
- | Consumer / response | `body` | Auxiliary materialization | Completion |
120
- | -------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------- |
121
- | Direct GET + HTML | Present projection of the rendered DOM, including `""` | Rendered DOM in `html`; render/projection metadata | `102`; absent projection is `422` |
122
- | Direct GET + `text/event-stream` | One `data` value plus newline per `text/plain` chunk | Initial response in `header` | `102` after header; origin close settles |
123
- | Direct request + no response body | Empty seed | Response and package metadata in `header` | Subscription closes; op is `102` |
124
- | Direct request + configured textual type | Incremental UTF-8 text under the declared type | Response and package metadata in `header` | Response-body end closes the stream |
125
- | Direct request + readable binary type | Derived Unicode under the projection output type | Origin type and projection identity in `header` | `102`; bytes are never durable |
126
- | Direct request + unreadable binary/unknown | Empty marker; declared type or `application/octet-stream` | Response and package metadata in `header` | `415 binary-response-unsupported` |
127
- | Direct binary input exceeds configured bound | Empty marker under the source type | Limit evidence in the terminal Problem | `413 projection-input-limit` |
128
- | Exact WebFetcher + non-empty 2xx HTML | Present projection of server HTML or the lazy rendered fallback | Selected HTML plus origin/projection metadata | Materialize; absent projection is `422` |
129
- | Exact WebFetcher + configured text | Complete UTF-8 response | Byte response in `header` | Materialize, then universal query |
130
- | Exact WebFetcher + readable binary | Derived Unicode under the projection output type | Origin type and projection identity in `header` | Materialize; absent projection is `422` |
131
-
132
- A fragmentless direct operation publishes only `body`. An explicit fragment
133
- publishes that named channel. Every acquired channel remains durable even when
134
- it is not published to the requesting loop. FIND returns standard JSON metadata
135
- and match coordinates; matcher READ returns selected content and navigation
136
- evidence.
86
+ The same page producer serves exact GET preparation, exact FIND, and executor
87
+ entry acquisition. Their outer policies differ: authored GET accepts explicit
88
+ targets and HTTP error responses; automatic acquisition first admits a public
89
+ credential-free target and treats non-2xx responses as unavailable. READ scope
90
+ and channel selection never enter the producer. Freshness may avoid transport,
91
+ but cold and warm representations pass through the same core projection.
92
+
93
+ | Response | `body` | Auxiliary materialization | Completion |
94
+ | --------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------------- |
95
+ | Negotiated origin Markdown | Exact origin Markdown | Independently acquired server source in `html` when found | Static representation, then core projection |
96
+ | HTML-page production | Tavily Markdown, local Markdown, or independent error | Exact server source in `html` when origin supplied it | Static representation, then core projection |
97
+ | `text/event-stream` | One `data` value plus newline per `text/plain` chunk | Initial response in `header` | `102`; origin close settles subscription |
98
+ | No response body | Present empty text | Response and package metadata in `header` | Static representation; body READ is `204` |
99
+ | Origin HTTP `4xx`/`5xx` | Preserve available origin or independently produced text | Exact response evidence; origin-backed channels errored | Selected channel's durable outcome |
100
+ | Configured textual type | Complete Fetch-decoded Unicode | Response and package metadata in `header` | Static representation |
101
+ | Readable binary type | Derived Unicode under the projection output type | Origin type and projection identity in `header` | Static representation; bytes never durable |
102
+ | Unreadable binary or unknown bytes | No representation is fabricated | Exact non-retryable Problem | `415 binary-response-unsupported` |
103
+ | Binary input exceeds configured bound | No representation is fabricated | Exact size evidence in the Problem | `413 projection-input-limit` |
104
+
105
+ The producer persists every available channel; core publishes only the channel
106
+ selected by the fragment, or `body` by default. Broad FIND returns standard
107
+ resource metadata; exact matcher FIND returns flat match locations. Exact READ
108
+ returns the selected channel's requested text projection.
137
109
 
138
110
  ### §http-text-decoding Text response decoding
139
111
 
140
112
  | Surface | Contract |
141
113
  | ----------------------------- | ------------------------------------------------------------------------------------------------ |
142
114
  | Response media type | WHATWG `MIMEType` essence; absent or unparseable metadata becomes `application/octet-stream` |
143
- | Direct textual response | Incremental replacement-mode UTF-8 through `TextDecoder`, preserving response backpressure |
144
- | WebFetcher textual response | Fetch `Response.text()` UTF-8 decoding; buffering does not select a different character encoding |
115
+ | Finite GET text | Fetch `Response.text()` UTF-8 decoding; buffering does not select a different character encoding |
116
+ | Streamed mutation response | Incremental replacement-mode UTF-8 through `TextDecoder`, preserving response backpressure |
145
117
  | `charset` parameter | Preserve in `header` as origin evidence; it does not replace Fetch text decoding |
146
118
  | JSON and XML textual families | Use the same HTTP byte-to-string rule; format projections consume the resulting Unicode string |
147
- | Direct HTML GET | Browser navigation owns the separate HTML document encoding algorithm |
119
+ | Direct HTML GET | Fetch UTF-8 decoding of origin server HTML |
148
120
  | Server-sent events | UTF-8 only under the HTML event-stream standard |
149
121
  | Malformed UTF-8 | Preserve the Encoding Standard's replacement-character behavior |
150
122
 
@@ -156,62 +128,127 @@ durable channel.
156
128
  ### §html-materialization Readable materialization
157
129
 
158
130
  `WebFetcher.materialize` is the shared readable-representation seam for exact
159
- query preparation and executor entry acquisition. Direct GET uses the same
160
- HTML and binary projection results while retaining incremental text streaming.
161
-
162
- | Input or event | Action | Result |
163
- | ------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------- |
164
- | Configured non-HTML text | Decode with Fetch UTF-8 | Original representation in `body` |
165
- | Configured binary with reader | Apply the bounded byte projection | Derived Unicode plus source type and identity |
166
- | Configured binary without reader | Cancel without retaining bytes | `null`; the consumer applies its absence policy |
167
- | Binary input exceeds the common bound | Cancel and preserve the typed cause | `WebMaterializationError` caused by `ProjectionInputLimitError` |
168
- | HTML with a present projection | Stop | Projection in `body`; selected HTML in `html` |
169
- | HTML with an absent projection | Render once when the caller supplies a renderer | Rendered projection in `body`; rendered `html` |
170
- | HTML still absent after fallback | Stop | `null`; the consumer applies its absence policy |
171
- | Projection implementation throws | Stop and preserve the cause | `WebMaterializationError` with stage `projection` |
172
- | Lazy renderer throws | Stop before another projection attempt | `WebMaterializationError` with stage `render` |
131
+ GET/FIND preparation and executor entry acquisition. It returns complete
132
+ Unicode channel material for one atomic canonical entry write; only SSE and
133
+ mutation responses retain incremental streaming.
134
+
135
+ | Input or event | Action | Result |
136
+ | ------------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------ |
137
+ | Configured non-HTML text | Decode with Fetch UTF-8 | Original representation in `body` |
138
+ | Configured binary with reader | Apply the bounded byte projection | Derived Unicode plus source type and identity |
139
+ | Configured binary without reader | Cancel without retaining bytes | `null`; the consumer applies its absence policy |
140
+ | Binary input exceeds the common bound | Cancel and preserve the typed cause | `WebMaterializationError` caused by `ProjectionInputLimitError` |
141
+ | Negotiated origin `text/markdown` | Accept it without Tavily | Exact Markdown in `body`; independently requested source in `html` |
142
+ | Eligible HTML with Tavily configured | Use Tavily as the body producer | Tavily Markdown in `body`; origin server source in `html` |
143
+ | Ineligible HTML or Tavily not configured | Use the installed HTML projection | Local Markdown in `body`; origin server source in `html` |
144
+ | Recoverable Tavily failure with source HTML | Use the installed projection as recovery | Local Markdown in `body` with terminal `203`; source in `html` |
145
+ | Hard Tavily failure | Preserve evidence; do not run local recovery | `body` errored; independently successful `html`/`header` survive |
146
+ | Tavily success after origin transport loss | Preserve provider body and origin failure | `body` static; `html` errored |
147
+ | Projection implementation throws | Stop and preserve the cause | `WebMaterializationError` with stage `projection` |
173
148
 
174
149
  A projection object is present even when its content is `""`; only `null`
175
150
  denotes absence. A materialization exception retains its original `cause`; it
176
151
  never enters the absence channel.
177
152
 
153
+ #### §tavily-extract Tavily Extract boundary
154
+
155
+ Tavily is eligible only for a credential-free generic request whose target has
156
+ been admitted as public. Authored request metadata—including an authored
157
+ `Accept` field—makes the request ineligible. The package-generated Markdown
158
+ negotiation, browser-compatible `User-Agent`, and conditional cache fields are
159
+ transport mechanics, not authored metadata. No target headers or origin
160
+ credentials cross the provider boundary.
161
+
162
+ When no `Accept` is authored, origin acquisition offers
163
+ `text/markdown, text/html;q=0.9, */*;q=0.1`. An origin `text/markdown`
164
+ representation wins and Tavily is skipped; a second origin request with
165
+ `Accept: text/html` may populate `html`. An authored `Accept` value is sent
166
+ unchanged and makes any returned HTML use the local projection route.
167
+
168
+ One provider request is `POST https://api.tavily.com/extract` with bearer
169
+ authentication and exactly one URL, configured `basic` or `advanced` depth,
170
+ `format: "markdown"`, and `include_usage: true`. A package-owned abort timeout
171
+ bounds the call. Success requires Markdown plus `request_id` and
172
+ `usage.credits`; those facts remain durable in `header`.
173
+
174
+ | Provider outcome | Classification | Body behavior when server HTML exists |
175
+ | ---------------------------------------- | -------------- | ------------------------------------------------------ |
176
+ | Success with required evidence | Success | Use Tavily Markdown |
177
+ | Caller cancellation | Cancelled | Return exact `499`; no recovery |
178
+ | Client timeout or transport failure | Recoverable | Local projection, terminal `203` |
179
+ | `429`, `5xx`, or `failed_results` | Recoverable | Local projection, terminal `203` |
180
+ | `401` or `403` | Hard | `tavily-authentication-failed`; no local projection |
181
+ | Other `4xx` | Hard | `tavily-provider-rejected`; no local projection |
182
+ | Malformed success or missing evidence | Hard | `tavily-invalid-response`; no local projection |
183
+
184
+ Without server HTML, no local recovery input exists. A Tavily success can still
185
+ close `body`, but every Tavily failure remains the body failure and `html`
186
+ closes errored.
187
+
188
+ #### §http-channel-outcomes Channel outcomes
189
+
190
+ For finite HTML-page production, `body`, `header`, and `html` have independent
191
+ durable outcomes and are written atomically. The selected channel alone
192
+ determines the projected operation result: successful `#html` or `#header` can
193
+ be read when `body` failed, and a successful default `body` is not invalidated
194
+ by unavailable `html`. Unselected failures remain `errored` channels carrying
195
+ their exact `producerResult`. A recoverable local body remains a static readable
196
+ channel with producer status `203`, so cold and warm READ both return its
197
+ content with that same status.
198
+
199
+ Exact READ, exact FIND, and executor materialization preserve the same channel
200
+ representation. A missing source variant is an explicit empty `errored`
201
+ channel, never an absent fact that later cache use can reinterpret as successful
202
+ empty content. A direct non-success HTTP response likewise remains available as
203
+ evidence, while each origin-backed channel carries an exact
204
+ `http-response-status` producer Problem. A Tavily-produced body and the
205
+ acquisition `header` remain independent of an unavailable origin source.
206
+
178
207
  ## §http-status §4 HTTP status mapping
179
208
 
180
- | Outcome | Operation status |
181
- | ---------------------------------------------------------- | ------------------------------------------------ |
182
- | Scoped READ | Exact universal selected-channel READ result |
183
- | Exact FIND / matcher READ after preparation | Exact universal query result |
184
- | Exact acquisition returns no WebFetcher value | `404` (`not-materialized`) |
185
- | Direct HTML or exact preparation has no readable projection | `422` (`no-readable-projection`) |
186
- | Direct textual, projected-binary, or empty response | `102` |
187
- | Direct binary response has no readable projection | `415` (`binary-response-unsupported`) |
188
- | Binary projection input exceeds the configured byte bound | `413` (`projection-input-limit`) |
189
- | `SEND[410]` | Exact entry-delete result |
190
- | Routed `SEND[499]` dispatch | `200` |
191
- | Client-cancelled acquisition | Direct `499` (`cancelled`) |
192
- | SSE cancellation after acquisition | `102` initial; terminal `499` |
193
- | SSE parser/transfer failure after acquisition | `102` initial; terminal `502` |
194
- | Multi-statement HTTP edit batch | `409` (`non-atomic-edit-batch`) |
195
- | Invalid target, channel, line edit, or URL userinfo | `400` with the corresponding stable Problem kind |
196
- | Non-corresponding 304 | `502` (`fetch-failed`) |
197
- | Direct network or acquisition exception | `502` (`fetch-failed`) |
198
- | Direct or prepared HTML render exception | `502` (`render-failed`) |
199
- | Direct or prepared projection exception | `500` (`projection-failed`) |
200
- | Uninterpreted SEND status | `501` (`send-status-unsupported`) |
209
+ | Outcome | Operation status |
210
+ | ------------------------------------------------------------- | -------------------------------------------------------- |
211
+ | Finite exact READ after preparation | Universal selected-channel result (`200`, `204`, or producer status) |
212
+ | Exact FIND after preparation | Exact universal query result |
213
+ | Exact acquisition returns no WebFetcher value | `404` (`not-materialized`) |
214
+ | Selected origin-backed channel received HTTP `4xx`/`5xx` | Exact durable `http-response-status` Problem |
215
+ | Selected HTML-page channel fails | That channel's exact durable producer Problem |
216
+ | Local HTML projection is absent | `422` (`no-readable-projection`) |
217
+ | Recoverable Tavily failure uses local projection | Readable body with durable status `203` |
218
+ | Finite textual or projected-binary response | Universal READ result |
219
+ | Finite empty text response | `204` |
220
+ | Finite binary response has no readable projection | `415` (`binary-response-unsupported`) |
221
+ | Binary projection input exceeds the configured byte bound | `413` (`projection-input-limit`) |
222
+ | `SEND[410]` | Exact entry-delete result |
223
+ | Routed `SEND[499]` dispatch | `200` |
224
+ | Client-cancelled finite acquisition | `499` (`cancelled`) |
225
+ | SSE after acquisition | `102` initial; terminal selected-channel result |
226
+ | SSE cancellation after acquisition | `102` initial; terminal `499` |
227
+ | SSE parser/transfer failure after acquisition | `102` initial; terminal `502` |
228
+ | Multi-statement HTTP edit batch | `409` (`non-atomic-edit-batch`) |
229
+ | Invalid target, channel, line edit, or URL userinfo | `400` with the corresponding stable Problem kind |
230
+ | Non-corresponding 304 | `502` (`fetch-failed`) |
231
+ | Acquisition failure without a successful provider body | `502` (`fetch-failed`) |
232
+ | Projection exception | `500` (`projection-failed`) |
233
+ | Uninterpreted SEND status | `501` (`send-status-unsupported`) |
201
234
 
202
235
  An HTTP error status is still a successfully acquired direct response: its
203
- status remains in `header` and its body streams normally. WebFetcher instead
204
- treats a non-2xx response as unmaterializable. Handler failures use RFC 9457
205
- Problem Details. Caught direct-acquisition diagnostics are bounded by
236
+ content and response evidence are preserved, and origin-backed channels carry
237
+ that exact status as durable producer evidence. HTML still enters page
238
+ production, so a successful Tavily `body` can remain readable while the origin
239
+ `html` records the HTTP error. Automatic WebFetcher acquisition instead treats
240
+ a non-2xx response as unavailable. Handler failures use RFC 9457 Problem Details.
241
+ Caught direct-acquisition diagnostics are bounded by
206
242
  `PLURNK_SCHEMES_HTTP_ERROR_DETAIL_LIMIT` in model-facing detail while complete
207
243
  errors remain in daemon diagnostics.
208
244
 
209
- A direct binary response first asks the installed mimetype family for a bounded
245
+ A binary response first asks the installed mimetype family for a bounded
210
246
  readable projection. A present result stores only its derived Unicode and
211
247
  appends authoritative `x-plurnk-projection-id` evidence after the origin and
212
- acquisition fields. Absence preserves an empty marker under the source type and
213
- returns non-retryable `415`; exceeding the input ceiling preserves the same
214
- marker and returns non-retryable `413` with configured and observed sizes.
248
+ acquisition fields. A finite GET with no projection returns non-retryable `415`
249
+ without fabricating a text entry; exceeding the input ceiling similarly returns
250
+ `413` with configured and observed sizes. A streamed mutation response already
251
+ owns a seeded lifecycle entry and settles its typed channel as errored instead.
215
252
  These statuses describe Plurnk's materialization boundary, not the remote HTTP
216
253
  outcome—a POST, PUT, or DELETE might already have changed the remote resource.
217
254
  Missing or malformed `Content-Type` becomes `application/octet-stream`; the
@@ -223,36 +260,17 @@ handler does not sniff or guess unknown bytes.
223
260
  | ----------------- | --------------------------------------------------------------------------------------------------------- |
224
261
  | Platform | Node ≥26 native fetch, streams, abort signals, decoding, DNS, and `WebSocket` |
225
262
  | SSE | `eventsource-parser` for bounded WHATWG event-stream framing |
226
- | Browser rendering | Required lazy-loaded `playwright` client; executable provisioning follows {§browser-provisioning} |
263
+ | Tavily Extract | Optional Tavily Extract API primitive when `TAVILY_API_KEY` is present |
227
264
 
228
265
  ### §http-config Operator configuration
229
266
 
230
- | Concern | Contract |
231
- | -------------------- | -------------------------------------------------------------------------------------------------------- |
232
- | Canonical registry | Shipped `.env.defaults`; the daemon assembles it as a set-if-unset floor {§operator-config-env-defaults} |
233
- | Required values | Missing or invalid required values fail at their owning read; code carries no hidden fallback |
234
- | Browser method | Exactly one of `launch`, `connect`, `connectOverCDP`, or `disabled` |
235
- | Method-specific data | Endpoint belongs to connection methods; channel/executable belong to launch and are mutually exclusive |
236
- | Browser location | Operator-set Playwright location variables remain authoritative |
237
-
238
- `Http.ready()` verifies the selected browser route before the handler is
239
- advertised. Disabled rendering is a valid configured route and fails clearly
240
- if an HTML operation later requires the browser.
241
-
242
- ### §browser-provisioning Browser provisioning
243
-
244
- | Component or method | Contract |
245
- | ------------------- | ---------------------------------------------------------------------------------------------------- |
246
- | Playwright client | Required runtime dependency; lazy import keeps non-render paths from initializing it |
247
- | `launch` | Requires an operator-provisioned compatible browser, selected by Playwright, channel, or exact path |
248
- | `connect` | Requires an external Playwright endpoint; no local browser executable |
249
- | `connectOverCDP` | Requires an external Chromium CDP endpoint; no local browser executable |
250
- | `disabled` | Requires no browser executable; a render attempt fails clearly |
251
-
252
- The package has no browser-install lifecycle script and never assigns
253
- `PLAYWRIGHT_BROWSERS_PATH`. Operators may provision the standard compatible
254
- Chromium with `npx playwright install chromium`; methods never fall back into
255
- one another.
267
+ | Concern | Contract |
268
+ | --------------------- | -------------------------------------------------------------------------------------------------------- |
269
+ | Canonical registry | Shipped `.env.defaults`; the daemon assembles it as a set-if-unset floor {§operator-config-env-defaults} |
270
+ | Required values | Missing or invalid required values fail at readiness/owning read; code carries no hidden fallback |
271
+ | Tavily API Key | Optional `# TAVILY_API_KEY=`; enables Tavily Extract for eligible public HTML |
272
+ | Tavily Extract Depth | `PLURNK_SCHEMES_HTTP_TAVILY_DEPTH=basic`; only `basic` or `advanced` |
273
+ | Tavily client timeout | Positive `PLURNK_SCHEMES_HTTP_TAVILY_TIMEOUT_MS=30000` |
256
274
 
257
275
  ### §automatic-fetch-check Automatic acquisition URL check
258
276
 
@@ -260,10 +278,10 @@ one another.
260
278
  acquisition, it accepts credential-free HTTP(S) targets only when every
261
279
  resolved address is ordinary globally reachable unicast, and repeats the check
262
280
  before every manually followed redirect. A refused or unresolvable target is
263
- the ordinary unavailable `null` result.
281
+ the ordinary unavailable `null` result. A transport failure after public
282
+ admission may still enter Tavily page production.
264
283
 
265
- Direct HTTP operations, WebSocket connections, Playwright navigation, browser
266
- subresources, and Playwright/CDP endpoints do not use this check. Loopback,
284
+ Direct HTTP operations and WebSocket connections do not use this check. Loopback,
267
285
  private, and link-local destinations are therefore valid explicit targets.
268
286
 
269
287
  Redirect transitions follow WHATWG Fetch: 301/302 rewrite POST to GET; 303
@@ -272,26 +290,24 @@ A body rewrite removes body headers, cross-origin redirects remove
272
290
  `Authorization`, and followed redirect bodies are cancelled. The configured
273
291
  hop limit returns the last redirect rather than following beyond the limit.
274
292
  Validation-time DNS answers are not pinned to connection-time resolution. This
275
- check therefore makes no DNS-rebinding, browser-sandbox, or total-egress claim.
293
+ check therefore makes no DNS-rebinding or total-egress claim.
276
294
 
277
- ## §render-lifecycle §6 Render lifecycle
295
+ ## §materialization-lifecycle §6 Materialization lifecycle
278
296
 
279
- | Concern | Contract |
280
- | ------------- | ------------------------------------------------------------------------------------------------------------- |
281
- | Direct gate | A GET HTML response cancels the byte probe body and navigates through the browser |
282
- | Prefetch gate | Server HTML is primary; browser rendering is a lazy fallback only when its readable projection is absent |
283
- | Pool | One warm browser per `Browser`; one atomically acquired context per worker; prefetch uses worker `0` |
284
- | Navigation | Mobile-emulated by default; `networkidle` with bounded substantive-DOM timeout salvage |
285
- | Projection | A present result, including `""`, becomes `body`; exact HTML used becomes `html` |
286
- | Cancellation | The render-owned page close aborts in-flight navigation; an already-aborted render never navigates |
287
- | Page cleanup | Close each opened page once and await it; preserve its failure alone or aggregate it after a primary failure |
288
- | Shutdown | Attempt every context and browser close, await all, then aggregate failures under {§handler-lifecycle} |
297
+ | Concern | Contract |
298
+ | -------------- | ---------------------------------------------------------------------------------------------------------- |
299
+ | Direct gate | Explicit targets use native origin transport; only a generic public request grants Tavily authority |
300
+ | Automatic gate | Target and redirects require public admission; accepted generic HTML follows the same page producer |
301
+ | Body owner | Origin Markdown wins; otherwise configured eligible Tavily is structural, with one local projection floor |
302
+ | Source owner | `html` is exact server-source HTML; it is never provider-generated or DOM-generated |
303
+ | Projection | A present projection, including `""`, becomes `body`; `null` alone means absence |
304
+ | Cancellation | One caller signal spans origin, auxiliary origin, projection, and Tavily work |
289
305
 
290
306
  ### §host-rewrite Acquisition target rewrite
291
307
 
292
308
  Only acquisition GETs are eligible for host rewriting. A GitHub
293
309
  `…/blob/…` address uses the corresponding `raw.githubusercontent.com` source for
294
- both byte and browser transport. Direct READ, exact FIND, matcher READ, and
310
+ byte transport. Direct READ, exact FIND, and
295
311
  WebFetcher prefetch share that rule. The originally addressed GitHub URL remains
296
312
  entry identity. POST, PUT, and DELETE are never retargeted. Rewritten targets
297
313
  are checked under {§automatic-fetch-check} only when `WebFetcher` acquires them.
@@ -300,8 +316,10 @@ are checked under {§automatic-fetch-check} only when `WebFetcher` acquires them
300
316
 
301
317
  Acquired responses append package-owned `x-plurnk-request-method`,
302
318
  `x-plurnk-fetched-at`, and `x-plurnk-cache-variant` fields after origin headers.
303
- Derived responses then append `x-plurnk-projection-id`. Only package metadata
304
- after the acquisition stamp is authoritative, so an origin cannot spoof it.
319
+ Page bodies append `x-plurnk-materializer-id`; local projections append
320
+ `x-plurnk-projection-id`; Tavily appends status, route, request, usage, timing,
321
+ and bounded failure evidence. Only package metadata after the acquisition stamp
322
+ is authoritative.
305
323
  Plurnk stores one representation per canonical URL rather than a variant set:
306
324
 
307
325
  | Acquisition context | Package variant | Later cache use |
@@ -318,11 +336,11 @@ restored representation's package marker to `bypass`.
318
336
 
319
337
  Only an eligible GET representation can supply a direct READ's body, TTL stamp,
320
338
  conditional validators, or exact-FIND preparation. Completion comes from
321
- channel lifecycle, not body length: every stored channel must be final (`static`
322
- after exact materialization or `closed` after a successful stream); `active`,
323
- `errored`, or unknown state is ineligible. Durable operation evidence and HTTP
324
- reuse eligibility remain distinct: an acquired response stays in the entry even
325
- when its origin policy prevents later cache use.
339
+ channel lifecycle, not body length: `body` and `header` must be successful
340
+ (`static` or `closed`), and every channel must be terminal. An auxiliary channel
341
+ may be `errored`; `active` or unknown state is ineligible. Durable operation
342
+ evidence and HTTP reuse eligibility remain distinct: an acquired response stays
343
+ in the entry even when its origin policy prevents later cache use.
326
344
 
327
345
  | Stored origin policy | Direct READ after acquisition | Exact FIND after acquisition |
328
346
  | ------------------------------------- | ------------------------------------------------------ | ---------------------------------------- |
@@ -340,10 +358,10 @@ stamp. A representation is fresh only while both origin lifetime and operator
340
358
  ceiling permit it. Unknown cache extensions are inert. Stale content is never
341
359
  served, so `must-revalidate` requires no separate path.
342
360
 
343
- Outside the fresh window, direct READ sends a stored ETag or Last-Modified only
344
- when that field is singular and syntactically valid. A 304 can restore the
345
- stored channels only when its validator corresponds to the nominated stored
346
- representation:
361
+ Outside the fresh window, a representation without a materializer identity may
362
+ send a stored ETag or Last-Modified only when that field is singular and
363
+ syntactically valid. A 304 can restore those stored channels only when its
364
+ validator corresponds to the nominated stored representation:
347
365
 
348
366
  | 304 validator | Correspondence requirement |
349
367
  | ----------------------------------------------------- | ------------------------------------------- |
@@ -352,7 +370,7 @@ representation:
352
370
  | No ETag; Last-Modified | Same valid stored Last-Modified instant |
353
371
  | Missing, malformed, unsolicited, or non-corresponding | Invalid acquisition; `502` (`fetch-failed`) |
354
372
 
355
- A corresponding 304 restores the stored channels without rendering and
373
+ A corresponding 304 restores the stored channels without rematerializing and
356
374
  updates the header under the following ownership rule; any other response
357
375
  replaces the channels:
358
376
 
@@ -362,16 +380,25 @@ replaces the channels:
362
380
  | Origin fields absent from the 304 | Preserve |
363
381
  | `Content-Type`, `Content-Encoding`, `Content-Range`, `Content-Length` | Preserve metadata describing the already-processed body |
364
382
  | Package method, acquisition stamp, and variant | Rebuild authoritatively; refresh stamp and variant |
365
- | Projection identity | Preserve |
383
+ | Projection evidence | Preserve |
366
384
 
367
385
  A 304-provided `Vary`, `no-store`, `no-cache`, expiry, or validator
368
386
  therefore governs the next operation without relabeling derived Unicode as a
369
387
  different source representation.
370
388
 
371
- A stored derived representation is reusable only while its projection identity
372
- matches the currently installed reader for the origin media type. A mismatch
373
- invalidates the body, TTL shortcut, and origin validators together: a 304 can
374
- certify unchanged source bytes, not output from a different projection.
389
+ A stored derived representation requires the current projection identity. A
390
+ stored page body also requires its current route: negotiated origin Markdown,
391
+ local projection while Tavily is unconfigured, metadata-ineligible local
392
+ projection, configured Tavily depth/version, or the corresponding recoverable
393
+ local-fallback route. Enabling Tavily or changing depth invalidates the affected
394
+ route.
395
+
396
+ Once a page representation leaves its fresh window, Plurnk performs complete
397
+ reacquisition without old origin validators. Origin `304` can certify origin
398
+ bytes, but it cannot certify a composite that may also contain a fresh Tavily
399
+ extraction, a newly negotiated Markdown representation, or an independently
400
+ acquired HTML variant. Projection or materializer mismatch likewise invalidates
401
+ body, TTL, and validators together.
375
402
 
376
403
  POST, PUT, and DELETE responses retain their method marker but cannot satisfy a
377
404
  later GET or exact-FIND acquisition. An unmarked authored entry and an eligible
@@ -392,26 +419,25 @@ operation is cancelled.
392
419
 
393
420
  ## §prefetch §7 WebFetcher
394
421
 
395
- | Result | Meaning |
396
- | ---------------------------- | ---------------------------------------------------------------------------------------- |
397
- | Non-empty 2xx HTML | Server text, MIME type, package-stamped headers, and a lazy browser renderer |
398
- | Other 2xx body | One unconsumed byte stream, MIME type, package-stamped headers, and cancellation owner |
399
- | Materialized configured text | Complete Fetch-decoded UTF-8 body |
400
- | Materialized readable binary | Bounded derived Unicode plus source type, projection identity, and enriched header |
401
- | Lazy renderer value | Non-empty rendered HTML, or `null` only when rendering yields no HTML |
402
- | Lazy renderer failure | Rejects with the complete browser failure; materialization preserves stage and cause |
403
- | Top-level fetch `null` | Refused, unreachable, timed-out, non-2xx, missing body, or empty HTML byte response |
404
- | Materialization `null` | Empty configured text or no final readable projection |
405
- | Caller-cancelled acquisition | Rejects with the caller signal's exact reason |
406
-
407
- Top-level `null` is a liveness value rather than a thrown failure. Caller
408
- cancellation is not liveness: a pre-aborted caller fails before acquisition, and
409
- the first abort reason selected between the caller and configured byte-probe
410
- deadline owns the outcome. The probe deadline remains `null`. WebFetcher owns
411
- no entry identity, registry selection, or query policy; its consumer supplies
412
- the projection capability and materializes the returned value. Lazy-render
413
- exceptions remain distinct from a renderer that honestly returns no HTML. The
414
- handler lifecycle closes the shared browser.
422
+ | Result | Meaning |
423
+ | ------------------------------------ | ---------------------------------------------------------------------------------------- |
424
+ | Origin HTML | Exact source text, MIME type, package-stamped evidence, and page-producer eligibility |
425
+ | Negotiated origin Markdown | Exact body plus independent HTML-variant outcome |
426
+ | Other accepted body | One unconsumed byte stream, MIME type, package-stamped headers, and cancellation owner |
427
+ | Admitted origin transport failure | Bounded origin failure plus eligibility for provider-only page production |
428
+ | Materialized configured text | Complete Fetch-decoded UTF-8 body |
429
+ | Materialized readable binary | Bounded derived Unicode plus source type, projection identity, and enriched header |
430
+ | Materialized HTML page | Independent body/html outcomes and complete route/provider evidence |
431
+ | Automatic top-level `null` | Refused target, non-2xx response, or unavailable response with no provider route |
432
+ | Non-page materialization `null` | No final readable binary projection |
433
+ | Caller-cancelled acquisition | Rejects with the caller signal's exact reason |
434
+
435
+ Top-level `null` is an automatic-acquisition liveness value rather than a thrown
436
+ failure. Caller cancellation is not liveness: a pre-aborted caller fails before
437
+ acquisition and a caller abort wins with its exact reason. Package-owned origin
438
+ and Tavily timeouts are ordinary classified acquisition outcomes. WebFetcher
439
+ owns no entry identity, registry selection, selected-channel policy, or query
440
+ policy; its consumers supply those boundaries and the projection capability.
415
441
 
416
442
  ## §ws §8 WebSocket
417
443
 
@@ -443,7 +469,7 @@ stateDiagram-v2
443
469
  | `SEND[499](ws(s)://…)` | Engine-routed cancellation closes the owning READ; scheme dispatch returns `200` |
444
470
  | `KILL(ws(s)://…)` | Close/cancel the claimed owner; no owner is `404`; an attempted close throw is `502` |
445
471
  | Inbound frame after native `open` | Join the owner's persistence chain; the next write begins only after the preceding write succeeds |
446
- | Binary frame after native `open` | Retain the text prefix, prune the binary and later frames, settle `415 binary-frame-unsupported`, close `1003` |
472
+ | Binary frame after native `open` | Retain the text prefix, prune the binary and later frames, settle `415 binary-frame-unsupported`, close with private-use code `4003` |
447
473
  | First inbound persistence failure | Retain the successful prefix, prune queued and later frames, and settle with `500 message-persistence-failed` |
448
474
  | Socket closes before `open` | Close subscription with `502 connection-failed`; the pending READ returns that exact failure |
449
475
  | Socket closes after `open` | Drain the accepted frame prefix; initial READ remains `102`; settle with `200` unless a drained write fails |
@@ -461,7 +487,7 @@ transport-close failures under {§handler-lifecycle}.
461
487
 
462
488
  | Transport limit | Current contract |
463
489
  | ------------------ | ------------------------------------------------------------------------------------------------------- |
464
- | Payload projection | String event data only into `text/plain`; binary data terminates with Plurnk `415` and WebSocket `1003` |
490
+ | Payload projection | String event data only into `text/plain`; binary data terminates with Plurnk `415` and private-use WebSocket code `4003` |
465
491
  | Reconnection | None; READ again after terminal cleanup |
466
492
  | Handshake metadata | Default global-WebSocket identity; custom target headers are not applied |
467
493
  | Runtime | Node ≥26 |