@plurnk/plurnk-contracts 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +119 -0
  3. package/SPEC.md +954 -0
  4. package/bin/plurnk-contracts.js +43 -0
  5. package/dist/plurnk.gbnf +394 -0
  6. package/dist/schema/ClientDisplayCapabilities.json +54 -0
  7. package/dist/schema/ClientStatement.json +68 -0
  8. package/dist/schema/EntryReadResult.json +101 -0
  9. package/dist/schema/LineMarker.json +16 -0
  10. package/dist/schema/LoopFlags.json +16 -0
  11. package/dist/schema/MatcherBody.json +84 -0
  12. package/dist/schema/Notice.json +67 -0
  13. package/dist/schema/OperationResult.json +44 -0
  14. package/dist/schema/ParsedPath.json +64 -0
  15. package/dist/schema/PlurnkStatement.json +264 -0
  16. package/dist/schema/Position.json +20 -0
  17. package/dist/schema/ProblemDetails.json +51 -0
  18. package/dist/schema/ProposalDisposition.json +26 -0
  19. package/dist/schema/ProposalProjection.json +44 -0
  20. package/dist/schema/ProviderCost.json +69 -0
  21. package/dist/schema/ResourceSelection.json +20 -0
  22. package/dist/schema/SendBody.json +13 -0
  23. package/dist/schema/TextRegion.json +31 -0
  24. package/dist/src/AstBuilder.d.ts +24 -0
  25. package/dist/src/AstBuilder.d.ts.map +1 -0
  26. package/dist/src/AstBuilder.js +609 -0
  27. package/dist/src/AstBuilder.js.map +1 -0
  28. package/dist/src/PathSyntax.d.ts +9 -0
  29. package/dist/src/PathSyntax.d.ts.map +1 -0
  30. package/dist/src/PathSyntax.js +36 -0
  31. package/dist/src/PathSyntax.js.map +1 -0
  32. package/dist/src/PlurnkErrorStrategy.d.ts +10 -0
  33. package/dist/src/PlurnkErrorStrategy.d.ts.map +1 -0
  34. package/dist/src/PlurnkErrorStrategy.js +147 -0
  35. package/dist/src/PlurnkErrorStrategy.js.map +1 -0
  36. package/dist/src/PlurnkParseError.d.ts +17 -0
  37. package/dist/src/PlurnkParseError.d.ts.map +1 -0
  38. package/dist/src/PlurnkParseError.js +24 -0
  39. package/dist/src/PlurnkParseError.js.map +1 -0
  40. package/dist/src/PlurnkParser.d.ts +9 -0
  41. package/dist/src/PlurnkParser.d.ts.map +1 -0
  42. package/dist/src/PlurnkParser.js +396 -0
  43. package/dist/src/PlurnkParser.js.map +1 -0
  44. package/dist/src/Problems.d.ts +8 -0
  45. package/dist/src/Problems.d.ts.map +1 -0
  46. package/dist/src/Problems.js +23 -0
  47. package/dist/src/Problems.js.map +1 -0
  48. package/dist/src/RecordingListener.d.ts +9 -0
  49. package/dist/src/RecordingListener.d.ts.map +1 -0
  50. package/dist/src/RecordingListener.js +19 -0
  51. package/dist/src/RecordingListener.js.map +1 -0
  52. package/dist/src/Validator.d.ts +49 -0
  53. package/dist/src/Validator.d.ts.map +1 -0
  54. package/dist/src/Validator.js +211 -0
  55. package/dist/src/Validator.js.map +1 -0
  56. package/dist/src/generated/plurnkLexer.d.ts +129 -0
  57. package/dist/src/generated/plurnkLexer.d.ts.map +1 -0
  58. package/dist/src/generated/plurnkLexer.js +808 -0
  59. package/dist/src/generated/plurnkLexer.js.map +1 -0
  60. package/dist/src/generated/plurnkParser.d.ts +525 -0
  61. package/dist/src/generated/plurnkParser.d.ts.map +1 -0
  62. package/dist/src/generated/plurnkParser.js +3896 -0
  63. package/dist/src/generated/plurnkParser.js.map +1 -0
  64. package/dist/src/generated/plurnkParserVisitor.d.ts +298 -0
  65. package/dist/src/generated/plurnkParserVisitor.d.ts.map +1 -0
  66. package/dist/src/generated/plurnkParserVisitor.js +257 -0
  67. package/dist/src/generated/plurnkParserVisitor.js.map +1 -0
  68. package/dist/src/index.d.ts +13 -0
  69. package/dist/src/index.d.ts.map +1 -0
  70. package/dist/src/index.js +10 -0
  71. package/dist/src/index.js.map +1 -0
  72. package/dist/src/types.d.ts +29 -0
  73. package/dist/src/types.d.ts.map +1 -0
  74. package/dist/src/types.generated.d.ts +450 -0
  75. package/dist/src/types.generated.d.ts.map +1 -0
  76. package/dist/src/types.generated.js +4 -0
  77. package/dist/src/types.generated.js.map +1 -0
  78. package/dist/src/types.js +23 -0
  79. package/dist/src/types.js.map +1 -0
  80. package/package.json +87 -0
  81. package/plurnk.md +220 -0
package/SPEC.md ADDED
@@ -0,0 +1,954 @@
1
+ # PLURNK Contracts Specification
2
+
3
+ ## 1. Overview
4
+
5
+ §contract-authority This package is the single authority for PLURNK's language, schemas, generated
6
+ types, parser, model rail, and runtime-neutral wire envelopes. Its package root
7
+ is the single code API for those contracts.
8
+
9
+ | Surface | Canonical export or artifact |
10
+ | ------------------------------------------------------------------------------- | --------------------------------------------------- |
11
+ | Parser, AST, validators, Problems, results, Notices, text regions | `@plurnk/plurnk-contracts` |
12
+ | Effective loop policy and its default | `LoopFlags`, `DEFAULT_LOOP_FLAGS` |
13
+ | Stopped-world client contract | `ProposalDisposition`, `ProposalProjection` |
14
+ | Client capability presentation | `ClientDisplayCapabilities` |
15
+ | JSON Schemas | `@plurnk/plurnk-contracts/schema/*.json` |
16
+ | Local-model rail | `@plurnk/plurnk-contracts/plurnk.gbnf` |
17
+ | Model language reference | `plurnk.md` in the package |
18
+
19
+ §contract-representations JSON Schema is authoritative for shared data shapes. TypeScript types are
20
+ generated from the schemas; ANTLR is authoritative for accepted model-language
21
+ syntax; GBNF remains the bounded generation aid described in §1.2.
22
+
23
+ ## §contract-layers 1.1 Contract layers and admission boundary
24
+
25
+ PLURNK uses one contract with deliberately different projections. A tolerant
26
+ ingester accepting a spelling does not make that spelling canonical model
27
+ teaching, and a generation rail admitting a sentence does not make its runtime
28
+ semantics valid.
29
+
30
+ ```mermaid
31
+ flowchart LR
32
+ canon["Canonical model teaching<br/>plurnk.md"]
33
+ rail["Optional raw generation rail<br/>plurnk.gbnf"]
34
+ free["Other admitted input"]
35
+ syntax["ANTLR lexer + parser<br/>syntax and document tier"]
36
+ ast["AstBuilder<br/>typed, serializable AST"]
37
+ runtime["Runtime owners<br/>stateful semantics and effects"]
38
+ canon --> rail
39
+ rail --> syntax
40
+ canon --> free
41
+ free --> syntax
42
+ syntax --> ast
43
+ ast --> runtime
44
+ ```
45
+
46
+ | Layer | Owner or artifact | Contract |
47
+ |--------------------------|-------------------------------------|---------------------------------------------------------------------------------|
48
+ | Stable current law | `SPEC.md` | Owns invariants and boundaries; forge issues retain history |
49
+ | Canonical model teaching | `plurnk.md` | Teaches the lean spelling and operational model the model should emit |
50
+ | Constrained generation | generated `plurnk.gbnf` | Increases likely ANTLR compliance without reproducing all parser/runtime checks |
51
+ | Accepted syntax | `plurnkLexer.g4`, `plurnkParser.g4` | Recognizes document tiers, statement shape, slot shape, and body fencing |
52
+ | Typed admission | `AstBuilder` | Produces JSON-serializable unions and validates deterministic body/path syntax |
53
+ | Shared wire data | `schema/*.json` | Defines runtime-neutral data shapes projected into generated TypeScript |
54
+ | Stateful behavior | consuming runtime | Resolves addresses, permissions, selection arithmetic, effects, and lifecycle |
55
+
56
+ ANTLR owns statement structure, suffix matching, slot multiplicity, accepted
57
+ slot permutations, scope-number syntax, and interstatement text recognition.
58
+ AstBuilder owns URL decomposition and deterministic matcher validation through
59
+ WHATWG `URL`, ECMAScript `RegExp`, XPath 1.0, and RFC 9535 JSONPath parsers.
60
+
61
+ The runtime owner decides facts that require state or operation-specific
62
+ meaning, including registered scheme resolution, target existence, tag
63
+ selection, text-region bounds, result ordering, semantic similarity, mutation
64
+ effects, executor behavior, and numeric operation-code semantics.
65
+
66
+ ### §contract-proposal-projection Loop policy and stopped-world projection
67
+
68
+ The schemas own the runtime-neutral shapes; core owns their stateful values.
69
+
70
+ | Contract | Shape invariant | Runtime responsibility |
71
+ | ------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
72
+ | `LoopFlags` | Complete effective `mode`, `auto`, `noWeb`, `noInteraction`, and `noProposals` | Validate/expand persisted partial policy before use |
73
+ | `ProposalDisposition` | Client authority, or the loop's exact automatic accept/reject | Compute precedence from effective loop policy, proposal kind, and stale-target truth |
74
+ | `ProposalProjection` | Identity, review target/body/attrs, effective flags, stale signal, disposition | Derive one validated projection for live delivery and durable reconnect discovery |
75
+ | `ProviderCost` | Discriminated authoritative, estimated, free, or unknown monetary evidence | Acquire evidence, preserve it per attempt, and derive nullable aggregate projections |
76
+
77
+ `DEFAULT_LOOP_FLAGS` is the contracts-owned effective default value. A consumer may persist a partial object as an implementation detail, but it never exposes or acts on that partial representation as though it were the complete contract.
78
+
79
+ §provider-cost `ProviderCost` represents one provider attempt's monetary truth.
80
+ An authoritative result preserves the provider's canonical decimal amount and
81
+ currency plus its decimal USD equivalent; an estimate carries a decimal USD
82
+ amount; free requires an explicit source; unknown requires a reason. Decimal
83
+ strings preserve evidence without binary floating-point rewriting. Unknown is
84
+ not zero, and no consumer may infer free from absent rates or a legacy numeric
85
+ zero.
86
+
87
+ The parser returns ordered statement, error, and text items. It recovers at a
88
+ trustworthy statement boundary when possible and sets `unparsedTail` when a
89
+ boundary-destroying failure makes later input undefined. SEND operation codes
90
+ and parse diagnostics are separate contracts.
91
+
92
+ ## 1.2 GBNF Generation Rail
93
+
94
+ §gbnf-rail-purpose ANTLR and AstBuilder define accepted PLURNK input. The generated
95
+ `dist/plurnk.gbnf` is an optional local llama.cpp sampling rail: it is kept lean
96
+ to make useful, ANTLR-compliant turns more likely without reproducing every
97
+ parser or semantic validator. Parse compatibility is a design goal balanced
98
+ against rail size and sampling efficiency, not a language-subset guarantee. A
99
+ rail-legal operation can therefore produce a parser or AstBuilder error; consumers
100
+ apply their ordinary admission and bounded-operation recovery contract.
101
+
102
+
103
+ The shipped raw turn has one shape:
104
+
105
+ ```ebnf
106
+ root-turn ::= channel sep plan sep tail-0
107
+ ```
108
+
109
+ §gbnf-turn-shape `channel` is exactly one Gemma Harmony enclosure at byte zero, beginning
110
+ `<|channel>thought\n` and ending `<channel|>`. Its body may be empty but cannot
111
+ contain another opener or the closer. `sep` is zero through seven whitespace
112
+ characters. No channel is legal after the leading one. `tail-0` is unchanged
113
+ apart from that removal: zero through fourteen internal statements, separated
114
+ only by `sep`, followed by exactly one terminal SEND under the existing terminal
115
+ eligibility rules.
116
+
117
+ ```mermaid
118
+ flowchart LR
119
+ raw["Raw constrained decode<br/>channel · sep · PLAN · sep · tail-0"]
120
+ split["llama.cpp<br/>reasoning_format: auto"]
121
+ reasoning["reasoning_content<br/>channel body"]
122
+ content["content<br/>PLAN through terminal SEND"]
123
+ parser["ANTLR + AstBuilder<br/>admission and diagnostics"]
124
+ raw --> split
125
+ split --> reasoning
126
+ split --> content
127
+ content --> parser
128
+ ```
129
+
130
+ §gbnf-reasoning-boundary GBNF applies to the raw sentence on the left, before projection. The two
131
+ projected fields are not separate GBNF languages and `content` alone is not
132
+ revalidated as though it still contained the required channel. Provider and
133
+ core own the projection evidence and rail-verdict boundary; this package owns
134
+ only the raw language and the parser/AstBuilder result.
135
+
136
+ §plan-body-no-openers On the GBNF rail, PLAN is unsuffixed and its body excludes
137
+ the literal `<<`. A single `<` remains legal. This prevents an omitted `:PLAN`
138
+ from consuming following operations as intended-goals text; ANTLR remains
139
+ tolerant during ingestion.
140
+
141
+ ## §canonical-statement 2. Canonical statement form
142
+
143
+ ```text
144
+ <<OPsuffix[signal]?(path)?<scope>?:body?:OPsuffix
145
+ ```
146
+
147
+ §body-fence The two colons fence the body. Everything after the opening colon and
148
+ before the matching `:OPsuffix` close is preserved verbatim and is opaque to
149
+ operation keywords, slot characters, and nested PLURNK-looking text. An empty
150
+ body retains both delimiters: `::OPsuffix`.
151
+
152
+ | Element | Canonical contract |
153
+ |-------------|--------------------------------------------------------------------|
154
+ | `<<` | Required statement opener |
155
+ | `OP` | One protocol operation |
156
+ | `suffix` | Optional enclosure suffix, joined directly to `OP` |
157
+ | `[signal]` | Optional operation-specific signal |
158
+ | `(path)` | Optional syntax slot whose operation contract may require a target |
159
+ | `<scope>` | Optional operation-specific numeric scope |
160
+ | `:` | Required body opener, even when the body is empty |
161
+ | `body` | Operation-specific, character-perfect content |
162
+ | `:OPsuffix` | Required close matching the opener exactly |
163
+
164
+ The following constraints are structural:
165
+
166
+ - §close-tag-match The close `:OPsuffix` must character-match the opening operation and suffix.
167
+ - Each admitted signal, target, and scope slot appears at most once.
168
+ - OPEN, FOLD, WORK, FORK, and KILL do not admit a scope slot.
169
+ - A suffix is `[A-Za-z0-9_]*`; generation canon uses digit suffixes when enclosure is needed.
170
+
171
+ §slot-order Canonical producers and the GBNF rail emit signal, then target, then
172
+ scope. The tolerant ANTLR ingester accepts any permutation of the slots admitted
173
+ by that operation, at most once each. Accepted permutation is not a second
174
+ canonical spelling.
175
+
176
+ The ingester also accepts several bounded noncanonical forms so it can explain
177
+ or safely execute understandable input:
178
+
179
+ | Tolerated input | Canonical or runtime disposition |
180
+ |--------------------------------------------------|---------------------------------------------------------------------|
181
+ | Reordered admitted slots | Producers retain signal → target → scope order |
182
+ | Missing target on a generally targeted operation | AST carries `null`; the runtime rejects when the target is required |
183
+ | PLAN modifiers or suffix | Model canon keeps PLAN slotless and unsuffixed |
184
+ | KILL annotation body | AST preserves it; model teaching uses an empty body |
185
+ | Dash-separated or comma-space scope numbers | Producers use adjacent comma-separated numbers |
186
+ | One-colon empty body at a statement boundary | Producers retain the explicit `::OPsuffix` empty body |
187
+
188
+ ## 3. Lexical elements
189
+
190
+ | Element | Accepted shape or role |
191
+ |-------------|--------------------------------------------------------------------|
192
+ | `OP` | `FIND READ EDIT COPY MOVE OPEN FOLD SEND EXEC WORK FORK KILL PLAN` |
193
+ | `suffix` | `[A-Za-z0-9_]*`, adjacent to `OP` |
194
+ | `[signal]` | Operation-specific tags, identifier, branch, or integer |
195
+ | `(path)` | Local path or scheme URL target; detailed in §5 |
196
+ | `<scope>` | One or more signed integers or decimals; detailed in §7 |
197
+ | `body` | Opaque text between the body opener and matching close |
198
+ | `:OPsuffix` | Adjacent colon plus the exact opening operation and suffix |
199
+
200
+ ## §op-shapes 4. Per-operation semantics
201
+
202
+ The model-facing forms below are the canonical projection. Parser tolerance is
203
+ governed by {§canonical-statement}; runtime conditions remain explicit below.
204
+
205
+ | OP | `[signal]` | `(path)` | `<scope>` | `body` |
206
+ |------|-------------------------------|----------------------------------------------|---------------------------------|--------------------------------|
207
+ | PLAN | none | none | none | required intended goals |
208
+ | FIND | optional filter tags | required target or glob | optional result range | optional matcher |
209
+ | READ | optional filter tags | required target or glob | optional text region | optional matcher |
210
+ | EDIT | optional apply tags | required file or entry | required for an existing target | literal text |
211
+ | COPY | optional apply tags | required source | optional source region | required destination selection |
212
+ | MOVE | optional apply tags | required source | optional source region | required destination selection |
213
+ | FOLD | optional apply tags | required log selection | none | optional matcher |
214
+ | OPEN | optional filter tags | optional log selection when tags are present | none | optional matcher |
215
+ | EXEC | optional executor | optional local working path | optional timeout, poll | optional executor input |
216
+ | WORK | optional Git branch | required fresh `worker://name` | none | required prompt |
217
+ | FORK | optional Git branch | required context-inheriting `worker://name` | none | required prompt |
218
+ | KILL | optional target-specific code | required target, including a log item | none | empty |
219
+ | SEND | optional target-specific code | optional recipient | optional timeout, poll | message; terminal is nonempty |
220
+
221
+ §operation-code-polymorphism SEND and KILL share a numeric wire slot, not one universal numeric vocabulary.
222
+ For pathless terminal SEND, the code is the loop disposition defined in §9.
223
+ Directed SEND and KILL delegate any present code to the addressed target's
224
+ operation contract; a live process may interpret a KILL code as a Unix signal,
225
+ but that interpretation does not define KILL generally.
226
+
227
+ §plan-intended-goals **PLAN records intended goals.** The PLAN body is the model's
228
+ concise statement of intended goals. It is public, durable log content—not provider
229
+ reasoning. Dispatch records it and has no other runtime effect.
230
+
231
+ OPEN and FOLD are log-curation operations owned by the core log surface. FOLD
232
+ selects a set, hides its bodies, and applies any signal tags. OPEN filters by its
233
+ signal tags and may omit the target to recall that tagged working set across the
234
+ worker log. FOLD requires a target or matcher; OPEN requires a target, matcher,
235
+ or filter tags. Neither operation exposes a positional scope.
236
+
237
+ The `<scope>` slot is optional where admitted and its domain is OP-specific. FIND
238
+ scopes ordered results. EXEC and SEND scope timing. READ, EDIT, COPY, and
239
+ MOVE use one universal text algebra independent of mimetype:
240
+
241
+ | Arity | Surface meaning | Endpoint rule |
242
+ |---------------|---------------------------------------------------------------------|------------------------------------------------------|
243
+ | one integer | One whole physical line, or the documented `0`/`-1` mutation anchor | Exactly one ordinal line |
244
+ | two integers | Whole physical lines `firstLine..lastLine` | Both lines are included |
245
+ | four integers | Exact `startLine,startColumn,endLine,endColumn` region | Start included, end excluded; equality is zero-width |
246
+
247
+ §text-scope-semantics Exact regions use 1-based lines and Unicode code-point columns. One- and
248
+ two-integer line selections normalize to the same exclusive-end `TextRegion`
249
+ used by four-coordinate selections. Whole-line replacement deliberately
250
+ accounts for newline separators; it is an ergonomic projection over exact
251
+ replacement, not a different mimetype navigation mode. As an unadvertised
252
+ ingestion tolerance, the runtime accepts three integers as
253
+ `startLine,startColumn,endLine` and immediately normalizes them to the complete
254
+ four-coordinate region ending after the final code point of `endLine`.
255
+ Producers never emit that form. Other arities and decimal text coordinates are
256
+ runtime 416 failures.
257
+
258
+ For READ, a body matcher selects files, entries, or log items against their
259
+ complete readable content. A non-semantic `<scope>` then projects text from each
260
+ selection; it never paginates the match set or limits where the matcher searches.
261
+ Without `<scope>`, READ returns each selection's complete readable content.
262
+ Semantic READ reserves a leading decimal for an optional similarity threshold;
263
+ the remaining one, two, or four integers project text. Without a leading decimal, every
264
+ integer belongs to READ projection and selection uses the configured default.
265
+
266
+ Mutation semantics:
267
+
268
+ - No scope and a target address that does not yet exist creates a file or entry from the body. This is the only unscoped EDIT.
269
+ - §unscoped-edit-create-only No scope and an existing target is refused. Replacing existing content requires a precise text scope or `<1,-1>`; core owns the existence check.
270
+ - `<N>` replaces whole line `N`; `<N,M>` replaces inclusive whole lines `N` through `M`.
271
+ - An empty body deletes the selected text.
272
+ - `<0>` prepends and `<-1>` appends.
273
+ - `<SL,SC,EL,EC>` deletes the exact exclusive-end region and inserts the body at its start.
274
+ - COPY and MOVE parse the body as a destination `ResourceSelection`: a target plus an optional destination scope. Header target, fragment, and scope independently select the source resource, channel, and region.
275
+
276
+ ### §operation-observation Per-operation observations
277
+
278
+ | OP | Successful observation |
279
+ |------|-----------------------------------------------------------------------------------|
280
+ | FIND | Catalog rows plus optional structural locators and text match evidence |
281
+ | READ | Complete or scoped body projections plus optional text match evidence |
282
+ | EDIT | Status plus a bounded receipt for the effect that landed |
283
+ | COPY | Source and destination selections plus ordered destination effects |
284
+ | MOVE | Source and destination selections plus ordered destination and source effects |
285
+ | OPEN | Status and matched log-item count |
286
+ | FOLD | Status and matched log-item count |
287
+ | SEND | Status and recipient acknowledgement when applicable |
288
+ | EXEC | Spawn acknowledgement; output arrives through named stream channels |
289
+ | WORK | Spawn acknowledgement; the deliverable arrives through the log |
290
+ | FORK | Spawn acknowledgement; the inherited worker's deliverable arrives through the log |
291
+ | KILL | Status of deletion or termination |
292
+ | PLAN | Status of durable intended-goals logging |
293
+
294
+ §copy-move-observation COPY and MOVE log projections preserve both admitted operand selections,
295
+ including their independent scopes, whether the result changed state, was a
296
+ 304 no-op, or failed after admission. Operands identify the request; `effects`
297
+ describe only mutations that landed. If either operand uses textual scope,
298
+ each landed textual create or update carries the same bounded receipt used by
299
+ EDIT. Whole-channel transfers remain bodyless structural effects; runtime
300
+ owners reject binary markers rather than treating a text field as a byte lane.
301
+
302
+
303
+ Every operation returns the runtime-neutral `OperationResult` defined by
304
+ {§operation-result}. Its `status` belongs to the result envelope and is not a
305
+ SEND signal. Durable operation observations are projected into a later packet;
306
+ retrieval never returns inline within the emitting turn.
307
+
308
+ ## §path-syntax 5. Target and path grammar
309
+
310
+ The target slot contains either a local path or a scheme URL. Exact addresses
311
+ and path globs share the slot; content matchers belong in the body.
312
+
313
+ | Form | Typed admission | Runtime meaning |
314
+ |-------------------------|---------------------------------------------------------------------|------------------------------------------------------|
315
+ | Bare path | `LocalPath { kind: "local", raw }` | Resolves through the runtime's file surface |
316
+ | `scheme://…` | WHATWG-decomposed `UrlPath` | Resolves only when a runtime scheme owns the address |
317
+ | Path glob | Preserved in either path kind | Scheme defines collection selection and ordering |
318
+ | `#channel` fragment | Preserved as `UrlPath.fragment` | Selects a named channel when the scheme supports it |
319
+ | Trailing `{key: value}` | Removed before URL parsing and preserved as ordered `headers` pairs | Addressed scheme interprets request metadata |
320
+ | `?query` | Preserved as ordered `UrlPath.query`; `null` = absent, `""` = `?` | Participates in scheme resource identity |
321
+
322
+ AstBuilder recognizes a URL with the case-insensitive prefix
323
+ `[a-z][a-z0-9+.-]*://`, passes it through WHATWG `URL`, and surfaces malformed
324
+ URL structure as a visitor error. A target without that prefix remains a raw
325
+ local path. Grammar acceptance does not register a scheme; the runtime scheme
326
+ catalogue and packet-time scheme teaching remain dynamically owned elsewhere.
327
+
328
+ §path-query The serialized query component is the lossless representation.
329
+ Ordering, duplicate names, encoded spelling, and the distinction between an
330
+ absent query and an explicit empty query survive parsing. Consumers that need
331
+ key/value access may construct `URLSearchParams`; the shared AST does not replace
332
+ URI identity with a grouped object projection.
333
+
334
+ §path-parentheses An unescaped depth-zero `)` closes the target slot. The
335
+ tolerant lexer preserves balanced unescaped parentheses, while canonical
336
+ producers use one lossless Plurnk lexical layer before target interpretation:
337
+
338
+ | Target-slot spelling | Interpreted character |
339
+ | -------------------- | --------------------- |
340
+ | `\\` | `\` |
341
+ | `\(` | `(` |
342
+ | `\)` | `)` |
343
+
344
+ Decoding consumes only those three pairs in one left-to-right pass; unknown
345
+ pairs such as `\*` retain both characters for glob interpretation. Encoding
346
+ escapes backslashes before parentheses, so every target string round-trips.
347
+ Pathname producers retain the deliberate `%28`/`%29` alias and `%3C` spelling,
348
+ but must use the lexical layer for identity-bearing query and fragment text
349
+ rather than changing their percent-encoded spelling. Newlines and `<<` are
350
+ never target content. Glob metacharacters remain legal path data.
351
+
352
+ §path-glob `PathSyntax` owns exact-path versus path-pattern classification.
353
+
354
+ | Method | Contract |
355
+ | -------------------------------- | ------------------------------------------------------------------------ |
356
+ | `hasGlob(pathname)` | Recognizes `*`, `?`, character-class, brace, extglob, and escape syntax |
357
+ | `globMagicIndex(pathname)` | First such position, solely for conservative candidate-prefix selection |
358
+
359
+ Matching and folder-scope semantics remain runtime concerns.
360
+
361
+ §path-request-metadata A scheme URL may append one or more
362
+ `{key: value}` request-metadata blocks. AstBuilder removes the blocks before
363
+ WHATWG decomposition and preserves them as ordered pairs so order and duplicate
364
+ names survive. Local paths retain braces as ordinary path text. Scheme handlers,
365
+ not the language parser, define the meaning and authorization of the metadata.
366
+ The admitted AST retains exact values for execution; malformed-metadata visitor
367
+ diagnostics identify only the structural fault and source position, never quote
368
+ metadata contents or a native URL parser's input-bearing diagnostic.
369
+
370
+ §worker-name The exported `WORKER_NAME` contract governs names minted for URI
371
+ authority slots: a lowercase DNS label matching
372
+ `[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?`. `RESERVED_AUTHORITIES` contains the
373
+ authority-shaped internal worker names `commons` and `plurnk`, which are
374
+ unavailable for minting. `~` is the sole current-worker sigil and falls outside
375
+ the mintable alphabet; every matching unreserved value, including `self`, is an
376
+ ordinary literal worker name. This is a minting and registry invariant, not an
377
+ ingestion restriction: the parser decomposes arbitrary URL authorities.
378
+
379
+ ## §matcher-prefix-claims 6. Bulk pattern matching
380
+
381
+ FIND, READ, OPEN, and FOLD accept an optional body matcher. The lexer preserves
382
+ the body opaquely; AstBuilder assigns the dialect from its leading characters.
383
+ A leading prefix claims its dialect. Invalid claimed syntax is a positioned
384
+ visitor error and never falls back to glob matching.
385
+
386
+ | Prefix | Dialect | Canonical body | Typed admission | Runtime owner |
387
+ |-----------|----------|--------------------------------------|-----------------------------------|---------------------|
388
+ | `//` | XPath | `//selector` | XPath 1.0 `xpath.parse()` | Mimetype projection |
389
+ | `/` | Regex | `/pattern/flags` | ECMAScript `RegExp` construction | Mimetype projection |
390
+ | `$` | JSONPath | RFC 9535 expression | `json-p3` compilation | Mimetype projection |
391
+ | `~` | Semantic | `~phrase` | Any text after the prefix | Embedding index |
392
+ | `@` | Graph | `@symbol`, `@<symbol`, or `@>symbol` | Direction and symbol preserved | Symbol index |
393
+ | none | Glob | Shell glob or literal text | Raw string | Mimetype projection |
394
+
395
+ XPath is classified before regex because its prefix is two slashes. Regex
396
+ splitting respects escapes and character classes; `\/` represents a literal
397
+ slash. The AST stores regex `pattern` and `flags`, not a compiled object.
398
+ Semantic and graph matchers require no parse step. Scope carries semantic
399
+ threshold and result-range information rather than changing the matcher body.
400
+
401
+ AstBuilder validation is compile-only and never evaluates a document. Matcher
402
+ evaluation belongs to the runtime's selected mimetype, embedding, or symbol
403
+ implementation. A matcher admission error is local to its statement; later
404
+ statements remain recoverable when their boundaries are trustworthy.
405
+
406
+ - §pattern-body-single-line The GBNF rail permits only single-line matcher
407
+ bodies. A regex that matches a newline uses the two-character `\n` escape.
408
+ ANTLR tolerates multiline matcher bodies during ingestion.
409
+ - §pattern-body-leading-colon The GBNF rail forbids `:` as the first matcher
410
+ character. Empty matchers and later colons remain valid; a regex such as
411
+ `/^:needle/` expresses a pattern beginning with a literal colon.
412
+
413
+ ## §scope-slot 7. Scope markers
414
+
415
+ The model-facing slot is `<scope>`; the AST field remains the historical
416
+ `lineMarker: { marks: number[] }`. The parser preserves ordered numeric
417
+ components, while the operation owner assigns their roles.
418
+
419
+ | Operation | Canonical components | Meaning |
420
+ |-----------------------|----------------------------------------------|--------------------------------------------------------------|
421
+ | FIND | optional threshold, then 0–2 positions | Inclusive positions in a deterministic result order |
422
+ | READ | optional threshold, then 0/1/2/4 coordinates | Text projection from every selected file, entry, or log item |
423
+ | EDIT | text coordinates | Text replacement, deletion, prepend, or append |
424
+ | COPY/MOVE source | text coordinates | Region copied or moved from the selected source |
425
+ | COPY/MOVE destination | text coordinates after destination | Region replaced or insertion point at the destination |
426
+ | EXEC | `timeout[,poll]` | Spawn lifetime bound and poll cadence in seconds |
427
+ | Terminal SEND `[202]` | `timeout[,poll]` | Bounded or indefinite wait and optional poll cadence |
428
+
429
+ Text coordinates use the algebra in {§text-scope-semantics}: one integer is a
430
+ whole line, two integers are an inclusive whole-line range, and four integers
431
+ are an exact start-inclusive/end-exclusive region. Mutation scopes additionally
432
+ admit `0` as prepend and `-1` as append. A leading decimal on semantic FIND or
433
+ READ is a similarity threshold; any remaining integers retain the operation's
434
+ ordinary result or text meaning.
435
+
436
+ §scope-marker-forms Canonical producers separate components with commas and no
437
+ spaces. ANTLR tolerates a dash separator and one space after a comma. Each
438
+ component greedily consumes an optional leading minus sign, digits, and an
439
+ optional decimal fraction, so the ingester preserves even noncanonical numeric
440
+ shapes for runtime validation.
441
+
442
+ Apart from the unadvertised three-coordinate text-scope tolerance in
443
+ {§text-scope-semantics}, the runtime rejects invalid arity, out-of-range or
444
+ inverted positions, and decimal text coordinates rather than rounding or
445
+ reinterpreting them. FIND owns a deterministic result order so the same
446
+ inclusive range selects the same positions from unchanged state. The parser
447
+ does not enforce either condition.
448
+
449
+ ## §suffix-discipline 8. Suffix Discipline
450
+
451
+ The `:body:` fencing handles the vast majority of grammatical-enclosure
452
+ concerns: body content is fully opaque to OP keywords and modifier-like
453
+ characters. The suffix is reserved for the residual edge case where
454
+ body content literally contains the close-tag pattern `:OPkeyword`.
455
+ That happens in two scenarios:
456
+
457
+ 1. **Nesting plurnk statements inside a body** (recording a plurnk
458
+ transcript, storing examples, etc.). The inner statement's close
459
+ `:OP` would prematurely terminate the outer's body.
460
+ 2. **Body content contains `:OPkeyword` as literal text** (e.g., a
461
+ stored JSON object with a value mentioning plurnk syntax).
462
+
463
+ Suffix rules:
464
+
465
+ - `suffix` is `[A-Za-z0-9_]*`, concatenated to `OP` with no separator, on both open and close.
466
+ - Open `<<OPsuffix` and close `:OPsuffix` must character-match.
467
+ - A non-empty suffix on the outer statement ensures its close tag
468
+ (`:OPsuffix`) is distinct from any `:OP` substring that may appear in
469
+ body content (whether as nested plurnk or as literal text).
470
+ - The body of a statement cannot contain its own exact close-tag
471
+ literal; choose a suffix that does not collide.
472
+ - Empty suffix is the default. Most statements need no suffix.
473
+ - Generation-side canon dictates **digit** suffixes (`<<EDIT1 … :EDIT1`)
474
+ so the shipped GBNF (`dist/plurnk.gbnf`) can enumerate close tags —
475
+ the HEREDOC tag match is not context-free. The parser remains
476
+ permissive: any matching `[A-Za-z0-9_]*` suffix is valid.
477
+
478
+ Example — a nested operation inside a suffixed outer body:
479
+
480
+ ```
481
+ <<SEND1[400]:
482
+ The following is a quoted plurnk operation, preserved verbatim:
483
+ <<SEND[400](worker://reviewer):still working:SEND
484
+ :SEND1
485
+ ```
486
+
487
+ The inner's `:SEND` close is ordinary body text because the outer close
488
+ is `:SEND1`. This rule belongs to the body fence and applies to every
489
+ operation, not to EDIT semantics.
490
+
491
+ ## 9. SEND Codes
492
+
493
+ Pathless terminal SEND disposition codes align with HTTP semantics so that model training
494
+ transfers directly:
495
+
496
+ | Class | Terminal meaning | Disposition used by the model |
497
+ |-------|-----------------------------------------------------------------|-------------------------------|
498
+ | `1xx` | Continue after submitted operations | `102 Processing` |
499
+ | `2xx` | Conclude successfully or wait on live obligations | `200 OK`, `202 Accepted` |
500
+ | `3xx` | Ask the interactive user a stop-the-world multiple choice | `300 Multiple Choices` |
501
+ | `4xx` | Abandon the loop after a model-side inability | `499` |
502
+ | `5xx` | Runtime or infrastructure failure; never a model terminal claim | none |
503
+
504
+ ### §waitpid-dispositions The terminal contract (waitpid)
505
+
506
+ The model signals one intention per turn — **continue (102)**, **done
507
+ (200)**, **wait (202)**, or **give up (499)**, plus the operator-facing
508
+ **question (300)** — and the engine verifies the claim against the
509
+ loop's live obligations (spawned children, open streams, pending
510
+ retrievals); the grammar polices *shape* only. The shape rules ARE
511
+ structural:
512
+
513
+ - §send-mid-reservation The five disposition codes `{102, 200, 202, 300, 499}` lex as a
514
+ distinct `DISPOSITION` token, making a disposition-coded SEND
515
+ **structurally terminal**: a statement after it is a parse error
516
+ (the mid-termination rule), and the GBNF reserves the five from
517
+ mid-position SENDs (`status-mid` is their complement over `DDD`).
518
+ This keeps the grammar's last-SEND model and the dispatcher's
519
+ first-disposition model coincident.
520
+ - A **mid** SEND (before the terminal) is comms: statusless, or any
521
+ non-disposition code, targeted or pathless, empty body allowed.
522
+ - §terminal-body-nonempty The GBNF rail requires a non-empty terminal SEND body — a constrained
523
+ turn cannot end empty-handed. ANTLR remains tolerant during ingestion.
524
+ - §park-202-only The **park** rides `[202]` only: `<T>` (wait up to T seconds),
525
+ `<T,P>` (adds a poll cadence, mirroring EXEC's slot), `<-1>`
526
+ (indefinite; the join's own liveness bounds it). See §7 for the
527
+ GBNF-strict / ANTLR-tolerant split.
528
+ - §no-idle-102 A **zero-statement turn may not conclude `[102]`** — "continue"
529
+ with nothing submitted is a spin. The GBNF's `tail-0` exits through
530
+ a terminal trie without the `[102]` tail, so the idle turn (`PLAN`
531
+ straight into `SEND[102]`) is unemittable; one statement restores
532
+ the full disposition set. The other four stay legal bare (a zero-op
533
+ `[202]` is the engine's obligation check). ANTLR stays tolerant
534
+ (ingest side). A dispatch-emptied turn — ops emitted but failing
535
+ downstream validation — survives the rail by nature; the engine's
536
+ idle-turn 409 backstops that class.
537
+
538
+ SEND with no `(path)` broadcasts to the default control channel — the
539
+ turn's disposition. SEND with `(path)` directs the message at a
540
+ specific recipient URI (a worker, a stream, a peer).
541
+
542
+ ### §send-body SEND body projection
543
+
544
+ SEND body syntax is opaque. AstBuilder preserves the exact `raw` string and
545
+ also exposes a best-effort `json` value when `JSON.parse` succeeds; invalid JSON
546
+ leaves `json: null` without invalidating an otherwise legal SEND. Plain text and
547
+ JSON are both messages, not implicitly stored resources, and the language
548
+ defines no synthetic scheme or READ-back convention for them.
549
+
550
+ ## §parser-architecture 10. Parser architecture
551
+
552
+ `plurnkLexer.g4` owns tokens and modes; `plurnkParser.g4` owns document tiers
553
+ and statement composition; AstBuilder projects parse-tree leaves into the public
554
+ AST. Generated TypeScript targets the `antlr4ng` runtime.
555
+
556
+ ```mermaid
557
+ stateDiagram-v2
558
+ [*] --> DEFAULT
559
+ DEFAULT --> DEFAULT: whitespace or TEXT
560
+ DEFAULT --> SLOTS: minted <<OPsuffix
561
+ SLOTS --> SIGNAL: signal opener
562
+ SIGNAL --> SLOTS: signal close
563
+ SLOTS --> TARGET: target opener
564
+ TARGET --> TARGET: balanced literals / target escapes
565
+ TARGET --> SLOTS: target close at depth zero
566
+ SLOTS --> SLOTS: scope token
567
+ SLOTS --> BODY: body-opening colon
568
+ BODY --> DEFAULT: matching close tag
569
+ ```
570
+
571
+ SLOTS admits operation-appropriate signal, target, and scope openers in any
572
+ order; the parser grammar enforces at-most-once multiplicity. Signal submodes
573
+ select tags, integer, or identifier tokens by operation family. TARGET preserves
574
+ balanced inner parentheses and recognized target escapes. BODY emits opaque text until the semantic predicate
575
+ recognizes the exact close from the captured opener.
576
+
577
+ TURN is the single exception to opaque-body mode: its opener returns to DEFAULT
578
+ so a complete inner turn parses structurally, and a suffix stack recognizes its
579
+ matching close. The narrow one-colon empty-body tolerance closes directly from
580
+ SLOTS only at a trustworthy statement boundary.
581
+
582
+ DEFAULT recognizes minted operation openers and otherwise emits non-whitespace
583
+ input as TEXT. A `<<word` whose word is not a minted operation remains TEXT.
584
+ Complete native reasoning enclosures are likewise one TEXT token so an operation
585
+ drafted inside pre-PLAN reasoning cannot become the turn anchor.
586
+
587
+ RecordingListener captures lexer and parser failures; AstBuilder adds visitor
588
+ failures. PlurnkErrorStrategy recovers at statement boundaries where possible.
589
+ An unfinished signal, target, body, or TURN leaves the lexer outside DEFAULT and
590
+ therefore produces `unparsedTail`; no later input is trustworthy.
591
+
592
+ ## §whitespace-contract 11. Whitespace and interstatement text
593
+
594
+ | Location | Canonical generation | Tolerant ANTLR ingestion |
595
+ |-------------------------|--------------------------------------|-------------------------------------------------------------|
596
+ | Between header slots | Adjacent | Spaces, tabs, and newlines are ignored |
597
+ | Between `OP` and suffix | Adjacent | Must remain adjacent |
598
+ | Inside signal | Adjacent values | Horizontal whitespace is ignored; newline is invalid |
599
+ | Inside target | Path alias plus target escapes | Balanced literals tolerated; newline and `<<` are invalid |
600
+ | Inside scope | Comma-separated numbers | Dash separator and one post-comma space are also accepted |
601
+ | Inside body | Character-perfect | Character-perfect |
602
+ | Inside close tag | Colon and `OPsuffix` are adjacent | Must remain adjacent and match the opener |
603
+ | Between turn statements | GBNF `sep`: zero to seven whitespace | Whitespace is hidden; non-whitespace may surface as TEXT |
604
+
605
+ PLURNK never escape-decodes body text: `\n` reaches the owning operation as
606
+ backslash plus `n`. A matcher or executor may interpret those characters under
607
+ its own body dialect. Producers that need a physical newline in literal EDIT
608
+ content emit an actual newline.
609
+
610
+ `parse` and `parseLog` admit TEXT around their PLAN-anchored operations and
611
+ return it as ordered text items without assigning semantics. `parseStatements`
612
+ and `parseClient` admit statements and hidden whitespace only. PLURNK defines no
613
+ comment syntax; a consumer may treat script-tier text as commentary, but that is
614
+ consumer policy rather than language syntax.
615
+
616
+ ## §public-api 12. Public API
617
+
618
+ The package root is the single JavaScript and TypeScript entry point. Shared AST
619
+ and wire types come from generated schemas; the small hand-maintained parser
620
+ types cover ordered parse items and `PlurnkParseError`, which JSON Schema cannot
621
+ express. Consumers never receive ANTLR parse-tree or token types.
622
+
623
+ §turn-shape `PlurnkParser.parse` accepts exactly one model turn. PLAN is the
624
+ first operation, a disposition-coded SEND is the terminal operation, and PLAN
625
+ cannot recur mid-turn. Tolerated TEXT may appear before PLAN, between operations,
626
+ and after SEND; it remains an ordered text item. Missing either anchor or placing
627
+ an operation after the terminal SEND is an error.
628
+
629
+ §tier-entrypoints Each parser entry point owns one document tier:
630
+
631
+ | Entry point | Accepted document | Result statement type |
632
+ |--------------------------------|----------------------------------------------------------------|-----------------------|
633
+ | `PlurnkParser.parse` | One PLAN-anchored model turn with optional TEXT | `PlurnkStatement` |
634
+ | `PlurnkParser.parseStatements` | Zero or more protocol statements and hidden whitespace | `PlurnkStatement` |
635
+ | `PlurnkParser.parseLog` | One or more TURN-wrapped model turns | `PlurnkStatement` |
636
+ | `PlurnkParser.parseClient` | Protocol statements plus read-shaped client LOOK/BUFF commands | `ClientStatement` |
637
+
638
+ Every entry point returns ordered `statement`, `error`, and, where admitted,
639
+ `text` items. When present, {§unparsed-tail-boundary} governs the result's item
640
+ extent. The statement `op` field discriminates the generated per-operation
641
+ union.
642
+
643
+ §root-value-api The package-root runtime namespace is closed and consists of the
644
+ following supported consumer values. All other root exports are TypeScript types.
645
+
646
+ | Root value(s) | Consumer contract | Exact owner |
647
+ |---------------------------------------|---------------------------------------------------------------------|---------------------------------------------|
648
+ | `PlurnkParser` | Four document-tier entry points listed above | {§parser-architecture}, {§tier-entrypoints} |
649
+ | `PlurnkParseError` | JSON-serializable positioned parser diagnostic | {§parse-diagnostics} |
650
+ | `parsePath`, `parseResourceSelection` | Parser-equivalent target and COPY/MOVE destination admission | {§path-syntax}, {§tier-entrypoints} |
651
+ | `PathSyntax` | Target-slot spelling and exact-versus-glob classification | {§path-parentheses}, {§path-glob} |
652
+ | `Validator` | Validation and assertion against the owning JSON Schemas | {§wire-entrypoint} |
653
+ | `InvalidNoticeError` | Typed failure from `Validator.assertNotice` | {§notice} |
654
+ | `InvalidProblemDetailsError` | Typed failure from `Validator.assertProblemDetails` | {§problem-details} |
655
+ | `InvalidOperationResultError` | Typed failure from `Validator.assertOperationResult` | {§operation-result} |
656
+ | `InvalidTextRegionError` | Typed failure from `Validator.assertTextRegion` | {§text-region} |
657
+ | `Problems` | RFC 9457 Problem construction | {§problem-details} |
658
+ | `PLURNK_OPS` | Runtime tuple from which the closed `PlurnkOp` union is derived | {§canonical-statement} |
659
+ | `WORKER_NAME`, `RESERVED_AUTHORITIES` | Authority minting predicate and internal reserved names | {§worker-name} |
660
+ | `UNKNOWN_POSITION` | Frozen sentinel for an AST statement without retained parsed source | {§parser-position} |
661
+
662
+ §parser-construction-boundary Parser construction components are internal rather
663
+ than alternate consumer entry points:
664
+
665
+ | Internal component | Boundary |
666
+ |--------------------------------------------|---------------------------------------------------------------------------------------------------------------|
667
+ | `AstBuilder` | Consumes generated ANTLR contexts; `PlurnkParser`, `parsePath`, and `parseResourceSelection` own its API |
668
+ | `PlurnkErrorStrategy`, `RecordingListener` | Assemble parser recovery and diagnostics around `antlr4ng`; consumers receive `PlurnkParseError` values |
669
+ | `Jsonplurnk` test helper | Independently checks the Core-owned {§jsonplurnk} renderer corpus; it is neither shipped code nor a root API |
670
+
671
+ ### CLI
672
+
673
+ ```text
674
+ plurnk-contracts [file] parse a file, or standard input when omitted
675
+ plurnk-contracts --help show usage
676
+ ```
677
+
678
+ The CLI prints the parse result as JSON. It exits `0` when no error item or
679
+ `unparsedTail` exists and `1` otherwise.
680
+
681
+ ## 13. Runtime-neutral wire contracts
682
+
683
+ §wire-entrypoint The package root exports generated wire types, `Problems`, and
684
+ `Validator` alongside the parser and AST. Their owning JSON Schemas are published
685
+ through `@plurnk/plurnk-contracts/schema/*.json`, not re-exported as root values.
686
+
687
+ ### §text-region 13.1 Text regions
688
+
689
+ `TextRegion` identifies one contiguous region of textual content:
690
+
691
+ | Required field | Coordinate |
692
+ |----------------|-----------------------------------------------------------|
693
+ | `startLine` | Line containing the included start |
694
+ | `startColumn` | Unicode code-point column of the included start |
695
+ | `endLine` | Line containing the excluded end |
696
+ | `endColumn` | Unicode code-point column immediately after the selection |
697
+
698
+ Lines and columns are positive safe integers and 1-based. Columns count Unicode
699
+ code points. LF, CRLF, and CR are line separators; CRLF is one indivisible
700
+ separator, and separator code units are not column positions. The end is
701
+ exclusive; equal start and end coordinates identify a zero-length insertion
702
+ point. A producer supplies all four coordinates or omits the region. It never
703
+ substitutes UTF-16 offsets, readable-row indices, or partial coordinates.
704
+ `Validator.assertTextRegion` rejects an end before its start.
705
+
706
+ ### §entry-read-result 13.2 Client entry reads
707
+
708
+ `EntryReadResult` is the exact transport-neutral projection of one entry. It
709
+ does not expose workspace IDs, storage owner IDs, split scheme/pathname fields,
710
+ scope, or other persistence columns.
711
+
712
+ | Outcome | Exact shape |
713
+ |---------|------------------------------------------------------------------|
714
+ | Success | `{ status: 200, entry: ClientEntry }` |
715
+ | Failure | `{ status: 400..599, problem: ProblemDetails, entry: null }` |
716
+
717
+ | `ClientEntry` field | Contract |
718
+ |---------------------|----------------------------------------------------------------------------------------------|
719
+ | `entryId` | Positive durable entry identifier |
720
+ | `target` | Client selector for the resolved entry, with any channel fragment removed |
721
+ | `channels` | Every channel for a full read, or exactly the selected channel for a sliced read |
722
+ | `tags` | Unique non-empty entry tags |
723
+
724
+ | Channel field | Contract |
725
+ |-----------------|---------------------------------------------------------------------------------------------------------------|
726
+ | `content` | Full content, or the suffix beginning at `contentOffset` |
727
+ | `contentOffset` | Actual Unicode-code-point offset of `content`; zero for a full read and capped at `contentLength` |
728
+ | `contentLength` | Unicode-code-point length of the complete stored channel |
729
+ | `mimetype` | Stored channel mimetype |
730
+ | `tokens` | Stored token count for the complete channel |
731
+ | `state` | `static`, `active`, `closed`, or `errored` |
732
+
733
+ For every returned channel,
734
+ `contentOffset + codePointLength(content) === contentLength`. Therefore an
735
+ offset beyond the current end returns empty content at `contentLength`, not the
736
+ unbounded requested offset. `Validator.assertEntryReadResult` enforces the
737
+ schema, this suffix invariant, and Problem status equality.
738
+
739
+ ### 13.3 Operation results
740
+
741
+ §operation-result Every public PLURNK operation returns one `OperationResult`:
742
+
743
+ | Status | Required shape |
744
+ |---------|-------------------------------------------|
745
+ | 100–399 | `problem` is forbidden |
746
+ | 400–599 | One RFC 9457 `problem` object is required |
747
+
748
+ The legacy top-level `error` field is forbidden. Producer-specific success
749
+ fields and Problem Details extension members remain open. A malformed result is
750
+ an internal producer contract violation; it is not converted into a second
751
+ model-facing failure envelope.
752
+
753
+ ### 13.4 Problem Details
754
+
755
+ §problem-details `ProblemDetails` requires `type`, `title`, `status`, and `detail`;
756
+ `instance` is optional until a durable host can attach the occurrence URI.
757
+
758
+ | Field | Contract |
759
+ |-------------|-----------------------------------------------------------------------------------------------------------------------------------------|
760
+ | `type` | Stable absolute URI for the problem class |
761
+ | `title` | Stable summary with no occurrence data or instruction |
762
+ | `status` | Equals the containing operation status |
763
+ | `detail` | Tersely states the failed subject, observed fact, and violated constraint at the layer that knows the cause |
764
+ | `instance` | Durable URI for this occurrence |
765
+ | `stage` | Stable failed stage, only when neighboring stages imply different recovery |
766
+ | `recovery` | One generally valid next action; omitted when the producer cannot know |
767
+ | `retryable` | `true` only when the producer recommends automatically retrying the identical request; otherwise false or unknown/omitted as applicable |
768
+ | extensions | Factual producer-known operands or constraints |
769
+
770
+ `detail` is failure truth; `recovery` is not a second explanation. Producers do
771
+ not infer motives, blame the model, restate status, or expose an implementation
772
+ accident as the cause. General syntax and workflow teaching remain in the model
773
+ packet rather than being repeated in every Problem.
774
+
775
+ `Problems.create(owner, code, status, detail, extensions?, options?)` derives a
776
+ stable title from `code` unless an established type supplies
777
+ `options.title`. Occurrence-specific text never belongs in `title`.
778
+
779
+ Internal invariant violations throw and preserve their cause. An external
780
+ protocol may require its own error envelope; its adapter maps that envelope to
781
+ or from the canonical Problem without creating another PLURNK failure contract.
782
+
783
+ ### 13.5 Notices
784
+
785
+ §notice A `Notice` is a transient, nonterminal observation. It cannot determine durable
786
+ failure truth, lifecycle, scheduling, or recovery. Sharing a renderer with
787
+ Problems does not merge their semantics.
788
+
789
+ ### §client-display-capabilities 13.6 Client display capabilities
790
+
791
+ `ClientDisplayCapabilities` is the transport-neutral installed-capability
792
+ projection used by external clients. It is an ordered array of closed,
793
+ discriminated values:
794
+
795
+ | `kind` | Identity field | `display` |
796
+ |--------|----------------|-----------|
797
+ | `scheme` | Non-empty `scheme` URI-family name | `CapabilityDisplay` |
798
+ | `mimetype` | Non-empty `mimetype` media type | `CapabilityDisplay` |
799
+
800
+ `CapabilityDisplay` is a closed object whose optional `glyph` is a non-empty,
801
+ opaque string. Capability frameworks own and validate their intrinsic
802
+ declarations; Core owns composition of the installed families; interface
803
+ modules expose this exact shape; clients own rendering, font support, theme,
804
+ and identity fallback when `glyph` is absent. Empty framework sentinels are
805
+ normalized to absence at composition. Display metadata is client state, never
806
+ model-language syntax or model packet teaching.
807
+
808
+ ## 14. Parse diagnostics
809
+
810
+ §parse-diagnostics `PlurnkParseError` is a JSON-serializable Error subclass.
811
+ Its `message` contains only the parser-owned diagnostic; position, source, and
812
+ severity remain separate facts.
813
+
814
+ ```typescript
815
+ type ErrorSource = "lexer" | "parser" | "visitor";
816
+ type Severity = "error" | "warning";
817
+
818
+ class PlurnkParseError extends Error {
819
+ readonly line: number;
820
+ readonly column: number;
821
+ readonly source: ErrorSource;
822
+ readonly severity: Severity;
823
+ }
824
+ ```
825
+
826
+ §parser-position Parser source locations are points, not text regions. An AST
827
+ statement's `position` identifies the first `<` of its open tag; a diagnostic
828
+ identifies the offending or recovery point; a text item and `unparsedTail.from`
829
+ identify the first point at which that item or undefined tail begins. A
830
+ statement constructed without retained parsed source uses `UNKNOWN_POSITION`,
831
+ the unknown sentinel; its dispatch origin remains a separate fact.
832
+
833
+ | Representation | Line | Column | Absence or extent |
834
+ |-------------------------------------|--------------------------|------------------------------------------|--------------------------------------------------------|
835
+ | Parser/AST `Position` | 1-based | 0-based Unicode code points | `{ line: 0, column: 0 }` is the sole unknown sentinel |
836
+ | Notice `content-offset` | 1-based | 0-based Unicode code points | Omit or set `position` to null when unknown |
837
+ | Contracts `TextRegion` | 1-based | 1-based Unicode code points | Start included, end excluded; equality is zero-width |
838
+ | SARIF 2.1.0 line/column region | 1-based | 1-based, declared by `columnKind` | End excluded; equality is zero-width |
839
+
840
+ Parser columns count code points, not UTF-16 code units or grapheme clusters.
841
+ LF and CRLF delimit source lines; a lone CR occupies one column. A point may sit
842
+ immediately after the final code point and carries no implicit character extent.
843
+ Consumers preserve parser points unchanged inside PLURNK. A SARIF adapter that
844
+ preserves one must add one to `column`, declare `columnKind` as
845
+ `"unicodeCodePoints"`, and emit equal start/end coordinates rather than
846
+ inventing an extent. `TextRegion` already uses SARIF's base and exclusive-end algebra but
847
+ remains a distinct contracts-owned representation. See [SARIF 2.1.0 §§3.14.26–27
848
+ and 3.30.2](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html).
849
+
850
+ | Source | Boundary |
851
+ |-------------|----------------------------------------------------------------------------------------|
852
+ | `"lexer"` | Token-level failure, such as an unrecognized character or malformed `<L>` integer. |
853
+ | `"parser"` | Structural failure, such as a missing close tag or wrong token order. |
854
+ | `"visitor"` | Semantic AST-construction failure, such as an invalid matcher dialect or signal shape. |
855
+
856
+ `severity` distinguishes a hard error from a non-fatal advisory. The parser is
857
+ the sole and complete owner of syntax-error messaging because it holds the
858
+ parse state, lexer mode, and expected-token set that no consumer has. It
859
+ produces the final diagnostic message, deduplicated expected-token lists,
860
+ turn-shape imperatives (begin with `<<PLAN`, end with a terminal `<<SEND`), and
861
+ these targeted diagnostics:
862
+
863
+ - §invented-closer-advisory **Invented closer.** When the forgiving parser
864
+ swallows a `<<Word…:Word` heredoc whose keyword is a known op confusion
865
+ (`<<CLOSE` → did you mean `<<FOLD`), it emits a `warning`-severity near-miss
866
+ advisory. On a never-closed body, when the swallowed text carries an
867
+ `:ALLCAPS` tag that is not the op's closer, the `unparsedTail` reason names it
868
+ (`found \`:COMPARISON_TASK\`, which is body text - the closer echoes the op's
869
+ name`) so a cap-cut runaway's recovery turn learns what happened, not just
870
+ that something did.
871
+ - §signal-scope-redirect **EXEC scope in the signal slot.** When EXEC's
872
+ `[signal]` slot (executor-ident mode) hits a leading `-` or digit —
873
+ mark-shaped `<timeout, poll>` scope content mistyped into the brackets — the
874
+ lexer message becomes `timeout/poll ride the \`<scope>\` slot; try
875
+ \`EXEC<-1,300>\`` instead of a raw `unrecognized character`. The redirect is
876
+ EXEC-scoped because its signal mode is exclusive; SEND/KILL are untouched.
877
+ - §matcher-body-redirect **Matcher body in the slot region.** When the
878
+ post-target slot region begins with `$`, `~`, or `@`, the lexer redirects the
879
+ unambiguous body matcher into `:body:` instead of returning the generic slot
880
+ list. Slash-led regex and XPath are excluded because the same character can
881
+ be a forgotten target wrap.
882
+ - §plan-body-op-advisory **Operation text swallowed by PLAN.** When an
883
+ unsuffixed PLAN body contains op-shaped text (`<<EXEC`…), an advisory reports
884
+ that the likely omitted `:PLAN` swallowed the turn's ops (`ops belong after
885
+ the plan closes; did you omit \`:PLAN\`?`). A non-empty suffix deliberately
886
+ invokes {§suffix-discipline} and suppresses this advisory.
887
+ - §misplaced-target-advisory **Mutation target in the signal slot.** When a
888
+ mutating op (EDIT/COPY/MOVE) parses with a null `(target)` and a path-shaped
889
+ `[signal]` element (a `/` or a dotted extension), the message redirects the
890
+ path into `(…)` (`\`<<EDIT\` has no \`(target)\` - that path sits in the
891
+ \`[…]\` tag slot; a target goes in \`(…)\`. Try \`EDIT(path):…\``). This
892
+ catches the markdown `[label](url)` reading at the parse where the engine
893
+ otherwise returns only a bare 400. It is gated on a path-shaped signal so a
894
+ genuine tags-only slip is not mis-steered toward a path it lacks.
895
+
896
+ §error-shape The diagnostic class determines how much guidance the parser may
897
+ provide:
898
+
899
+ | Class | Surface | Message contract |
900
+ |------------------------|-----------------------|-------------------------------------------------------------------------------------------|
901
+ | Hard fact | `severity: "error"` | One concise observed fact and violated constraint in PLURNK vocabulary. |
902
+ | Targeted hard redirect | `severity: "error"` | One canonical correction only when parser state makes the intended structure unambiguous. |
903
+ | Non-fatal advisory | `severity: "warning"` | One narrowly gated likely mistake and canonical alternative; input remains admitted. |
904
+ | Boundary loss | `unparsedTail` | Where trust ends, what remains open, and the required closer; later input is undefined. |
905
+
906
+ All messages use PLURNK protocol vocabulary: open tag, close tag, signal,
907
+ target, scope, line marker, body, statement header, or space between
908
+ statements. They never expose ANTLR rule or token names. They refer to a slot or
909
+ feature rather than an implementation rule. Generic tutoring, speculative
910
+ intent, coordinate restatement, and multiple repair strategies are forbidden.
911
+
912
+ Examples of canonical hard facts:
913
+
914
+ - `unrecognized character '<<' in target`
915
+ - `unrecognized character ':' in signal`
916
+ - `unrecognized character 'X' in statement header`
917
+ - `expected close tag; got end of input`
918
+ - `expected ')'; got ':'`
919
+
920
+ Each malformed statement produces at most one hard error. The first recorded
921
+ hard lexer or parser error within its source range wins; later failures in that
922
+ same range are consumed rather than projected as a cascade. A visitor failure
923
+ surfaces when syntax admitted the statement but AST construction did not.
924
+ Independent malformed statements each retain one hard error. Advisories remain
925
+ separate because they do not represent failed admission.
926
+
927
+ §unparsed-tail-boundary When the lexer cannot determine where a malformed
928
+ statement ends, the result's `unparsedTail` marks the position from which
929
+ parsing gave up. `ParseResult.items` contains only facts that begin strictly
930
+ before that point; recovered contexts and diagnostics at or beyond it are not
931
+ public results. The tail is one separate boundary fact, not an additional
932
+ malformed-statement diagnostic. Consumers must treat anything from that point
933
+ onward as undefined and must never dispatch a recovered statement from it.
934
+
935
+ | Consumer duty | Contract |
936
+ |--------------------|----------------------------------------------------------------------------------------------------------------|
937
+ | Diagnostic text | Project `message` verbatim; do not strip prefixes, restate coordinates, or synthesize generic syntax recovery. |
938
+ | Structured context | Preserve `line`, `column`, `source`, and `severity` as separate fields. |
939
+ | Runtime recovery | Attach only a separately owned fact, such as Core knowing that bounded sibling operations were retained. |
940
+ | Durable projection | Map bounded hard errors to failed operation results; warnings may become Notices with `level: "warn"`. |
941
+ | Presentation | Normalize or bound the diagnostic only when the surface requires it, without changing its meaning. |
942
+
943
+ Serialization convention for transmission to the model (the agent
944
+ runtime constructs this; the parser provides the fields):
945
+
946
+ ```json
947
+ {
948
+ "line": 1,
949
+ "column": 12,
950
+ "source": "parser",
951
+ "severity": "error",
952
+ "message": "expected close tag `:OPsuffix`; got end of input"
953
+ }
954
+ ```