@plurnk/plurnk-contracts 1.16.5 → 1.18.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 (92) hide show
  1. package/README.md +13 -62
  2. package/SPEC.md +633 -529
  3. package/dist/conformance/agui-v1.json +3 -50
  4. package/dist/schema/CapabilityDescriptor.json +6 -1
  5. package/dist/schema/CapabilityProjection.json +2 -4
  6. package/dist/schema/CapabilitySelector.json +6 -1
  7. package/dist/schema/ClientStatement.json +5 -52
  8. package/dist/schema/FunctionalityDefinitionState.json +10 -2
  9. package/dist/schema/LineMarker.json +1 -1
  10. package/dist/schema/LoopPolicy.json +2 -3
  11. package/dist/schema/MatcherBody.json +6 -6
  12. package/dist/schema/McpServerDefinition.json +17 -0
  13. package/dist/schema/ModelCatalogPage.json +10 -1
  14. package/dist/schema/ModelRoute.json +9 -0
  15. package/dist/schema/Notice.json +1 -1
  16. package/dist/schema/ParsedPath.json +3 -2
  17. package/dist/schema/Plan.json +10 -6
  18. package/dist/schema/PlurnkStatement.json +95 -143
  19. package/dist/schema/ProposalProjection.json +6 -1
  20. package/dist/schema/ResourceSelection.json +51 -27
  21. package/dist/schema/SkillDefinition.json +3 -3
  22. package/dist/src/AcpPlanValue.d.ts +0 -1
  23. package/dist/src/AcpPlanValue.d.ts.map +1 -1
  24. package/dist/src/AcpPlanValue.js +14 -13
  25. package/dist/src/AcpPlanValue.js.map +1 -1
  26. package/dist/src/ApplicationPort.d.ts +17 -14
  27. package/dist/src/ApplicationPort.d.ts.map +1 -1
  28. package/dist/src/JsonDocument.d.ts +2 -0
  29. package/dist/src/JsonDocument.d.ts.map +1 -0
  30. package/dist/src/JsonDocument.js +14 -0
  31. package/dist/src/JsonDocument.js.map +1 -0
  32. package/dist/src/LoopLifecycle.d.ts +3 -0
  33. package/dist/src/LoopLifecycle.d.ts.map +1 -0
  34. package/dist/src/LoopLifecycle.js +14 -0
  35. package/dist/src/LoopLifecycle.js.map +1 -0
  36. package/dist/src/PlanValue.d.ts +1 -1
  37. package/dist/src/PlanValue.d.ts.map +1 -1
  38. package/dist/src/PlanValue.js +10 -6
  39. package/dist/src/PlanValue.js.map +1 -1
  40. package/dist/src/PlurnkParseError.d.ts +3 -1
  41. package/dist/src/PlurnkParseError.d.ts.map +1 -1
  42. package/dist/src/PlurnkParseError.js +4 -1
  43. package/dist/src/PlurnkParseError.js.map +1 -1
  44. package/dist/src/TurnDisposition.d.ts +11 -0
  45. package/dist/src/TurnDisposition.d.ts.map +1 -0
  46. package/dist/src/TurnDisposition.js +32 -0
  47. package/dist/src/TurnDisposition.js.map +1 -0
  48. package/dist/src/Validator.js +1 -1
  49. package/dist/src/Validator.js.map +1 -1
  50. package/dist/src/index.d.ts +5 -4
  51. package/dist/src/index.d.ts.map +1 -1
  52. package/dist/src/index.js +5 -5
  53. package/dist/src/index.js.map +1 -1
  54. package/dist/src/types.d.ts +13 -8
  55. package/dist/src/types.d.ts.map +1 -1
  56. package/dist/src/types.generated.d.ts +172 -86
  57. package/dist/src/types.generated.d.ts.map +1 -1
  58. package/dist/src/types.js +9 -4
  59. package/dist/src/types.js.map +1 -1
  60. package/package.json +5 -23
  61. package/plurnk.md +106 -119
  62. package/bin/plurnk-contracts.js +0 -43
  63. package/dist/plurnk.gemma.gbnf +0 -141
  64. package/dist/plurnk.qwen.gbnf +0 -130
  65. package/dist/src/AstBuilder.d.ts +0 -20
  66. package/dist/src/AstBuilder.d.ts.map +0 -1
  67. package/dist/src/AstBuilder.js +0 -723
  68. package/dist/src/AstBuilder.js.map +0 -1
  69. package/dist/src/PlurnkErrorStrategy.d.ts +0 -11
  70. package/dist/src/PlurnkErrorStrategy.d.ts.map +0 -1
  71. package/dist/src/PlurnkErrorStrategy.js +0 -358
  72. package/dist/src/PlurnkErrorStrategy.js.map +0 -1
  73. package/dist/src/PlurnkParser.d.ts +0 -11
  74. package/dist/src/PlurnkParser.d.ts.map +0 -1
  75. package/dist/src/PlurnkParser.js +0 -335
  76. package/dist/src/PlurnkParser.js.map +0 -1
  77. package/dist/src/RecordingListener.d.ts +0 -9
  78. package/dist/src/RecordingListener.d.ts.map +0 -1
  79. package/dist/src/RecordingListener.js +0 -19
  80. package/dist/src/RecordingListener.js.map +0 -1
  81. package/dist/src/generated/plurnkLexer.d.ts +0 -174
  82. package/dist/src/generated/plurnkLexer.d.ts.map +0 -1
  83. package/dist/src/generated/plurnkLexer.js +0 -1215
  84. package/dist/src/generated/plurnkLexer.js.map +0 -1
  85. package/dist/src/generated/plurnkParser.d.ts +0 -477
  86. package/dist/src/generated/plurnkParser.d.ts.map +0 -1
  87. package/dist/src/generated/plurnkParser.js +0 -3298
  88. package/dist/src/generated/plurnkParser.js.map +0 -1
  89. package/dist/src/generated/plurnkParserVisitor.d.ts +0 -284
  90. package/dist/src/generated/plurnkParserVisitor.d.ts.map +0 -1
  91. package/dist/src/generated/plurnkParserVisitor.js +0 -245
  92. package/dist/src/generated/plurnkParserVisitor.js.map +0 -1
@@ -1,723 +0,0 @@
1
- /// <reference path="./json-p3-esm.d.ts" />
2
- import { ParserRuleContext, TerminalNode } from "antlr4ng";
3
- import * as xpath from "xpath";
4
- import { JSONPathEnvironment } from "json-p3/dist/json-p3.esm.js";
5
- import { BodyContext, LineMarkerContext, MetadataContext, MidSendContext, PlanStatementContext, SendStatementContext, TargetContext, } from "./generated/plurnkParser.js";
6
- import { plurnkLexer } from "./generated/plurnkLexer.js";
7
- import PlurnkParseError from "./PlurnkParseError.js";
8
- import PathSyntax from "./PathSyntax.js";
9
- import PlanValue from "./PlanValue.js";
10
- export default class AstBuilder {
11
- // {§misplaced-annotation-advisory} — advisories raised while building one statement; the
12
- // parser drains them right after the statement so the model sees WHAT it did on the first try.
13
- static #advisories = [];
14
- static takeAdvisories() {
15
- const taken = AstBuilder.#advisories;
16
- AstBuilder.#advisories = [];
17
- return taken;
18
- }
19
- // A body that is solely an HTML comment can never be a matcher. Preserve it
20
- // as the operation annotation and report only that deterministic normalization.
21
- static #annotationBody(op, delimiter, annotation, raw, position) {
22
- if (raw === null)
23
- return { annotation, raw };
24
- const comment = /^\s*<!--([\s\S]*?)-->\s*$/u.exec(raw);
25
- if (comment === null)
26
- return { annotation, raw };
27
- AstBuilder.#advisories.push(new PlurnkParseError(position.line, position.column, "parser", `The ${op} body contained only an HTML comment; it was applied as the operation annotation.`, "warning"));
28
- return { annotation: annotation ?? (comment[1] ?? "").trim(), raw: null };
29
- }
30
- static #SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:\/\//i;
31
- // Compile-only RFC 9535 admission using the runtime's JSONPath engine. {§matcher-prefix-claims}
32
- static #JSONPATH = new JSONPathEnvironment();
33
- static #GRAPH_MATCHER = /^&[<>]?[^\s<>]\S*$/u;
34
- static build(ctx) {
35
- // The strict turn root attaches the leading PLAN and the terminal SEND as direct
36
- // children (not wrapped in `statement`), so dispatch those by type first.
37
- if (ctx instanceof PlanStatementContext)
38
- return AstBuilder.#buildPlan(ctx);
39
- if (ctx instanceof SendStatementContext)
40
- return AstBuilder.#buildSend(ctx);
41
- if (ctx instanceof MidSendContext)
42
- return AstBuilder.#buildMidSend(ctx);
43
- const midSend = ctx.midSend();
44
- if (midSend)
45
- return AstBuilder.#buildMidSend(midSend);
46
- const find = ctx.findStatement();
47
- if (find)
48
- return AstBuilder.#buildFind(find);
49
- const read = ctx.readStatement();
50
- if (read)
51
- return AstBuilder.#buildRead(read);
52
- const edit = ctx.editStatement();
53
- if (edit)
54
- return AstBuilder.#buildEdit(edit);
55
- const copy = ctx.copyStatement();
56
- if (copy)
57
- return AstBuilder.#buildCopy(copy);
58
- const move = ctx.moveStatement();
59
- if (move)
60
- return AstBuilder.#buildMove(move);
61
- const exec = ctx.execStatement();
62
- if (exec)
63
- return AstBuilder.#buildExec(exec);
64
- const bare = ctx.bareStatement();
65
- if (bare)
66
- return AstBuilder.#buildBare(bare);
67
- const work = ctx.workStatement();
68
- if (work)
69
- return AstBuilder.#buildWork(work);
70
- const fork = ctx.forkStatement();
71
- if (fork)
72
- return AstBuilder.#buildFork(fork);
73
- const kill = ctx.killStatement();
74
- if (kill)
75
- return AstBuilder.#buildKill(kill);
76
- // `midStatement` has no planStatement alternative (PLAN is never a mid-op); only the
77
- // full `statement` rule does.
78
- if ("planStatement" in ctx) {
79
- const plan = ctx.planStatement();
80
- if (plan)
81
- return AstBuilder.#buildPlan(plan);
82
- const send = ctx.sendStatement();
83
- if (send)
84
- return AstBuilder.#buildSend(send);
85
- }
86
- throw new Error("statement context has no recognized alternative");
87
- }
88
- static #buildFind(ctx) {
89
- const positionForBody = AstBuilder.#positionOf(ctx);
90
- const bodied = AstBuilder.#annotationBody("FIND", AstBuilder.#splitDelimiter(ctx.OPEN_FIND().getText(), "FIND"), AstBuilder.#annotationOf(ctx), AstBuilder.#bodyTextOf(ctx), positionForBody);
91
- return AstBuilder.#buildFindFrom(ctx, bodied.annotation, bodied.raw);
92
- }
93
- static #buildFindFrom(ctx, annotation, raw) {
94
- const position = AstBuilder.#positionOf(ctx);
95
- const slots = AstBuilder.#extractSlots(ctx.slotModifiers(), position);
96
- return {
97
- op: "FIND",
98
- delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_FIND().getText(), "FIND"),
99
- annotation,
100
- ...slots,
101
- body: raw !== null ? AstBuilder.#parseMatcherBody(raw, position) : null,
102
- position,
103
- };
104
- }
105
- // Client-tier dispatch (parseClient). A `clientStatement` is either a protocol `statement`
106
- // (delegated to build, returning a PlurnkStatement — which IS a ClientStatement) or one of
107
- // the two client-only ops. Kept separate from build() so the protocol return type stays the
108
- // closed PlurnkStatement and client ops never leak into it.
109
- static buildClient(ctx) {
110
- const statement = ctx.statement();
111
- if (statement)
112
- return AstBuilder.build(statement);
113
- const look = ctx.lookStatement();
114
- if (look)
115
- return AstBuilder.#buildLook(look);
116
- const buff = ctx.buffStatement();
117
- if (buff)
118
- return AstBuilder.#buildBuff(buff);
119
- throw new Error("clientStatement context has no recognized alternative");
120
- }
121
- // LOOK / BUFF are client-tier matcher observations. They share the tag slots
122
- // and parse matcher bodies directly for their client-owned lifecycles.
123
- static #buildLook(ctx) {
124
- const position = AstBuilder.#positionOf(ctx);
125
- const slots = AstBuilder.#extractTextSlots(ctx.slotModifiers(), position);
126
- const raw = AstBuilder.#bodyTextOf(ctx);
127
- return {
128
- op: "LOOK",
129
- delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_LOOK().getText(), "LOOK"),
130
- annotation: AstBuilder.#annotationOf(ctx),
131
- ...slots,
132
- body: raw !== null ? AstBuilder.#parseMatcherBody(raw, position) : null,
133
- position,
134
- };
135
- }
136
- static #buildBuff(ctx) {
137
- const position = AstBuilder.#positionOf(ctx);
138
- const slots = AstBuilder.#extractSlots(ctx.slotModifiers(), position);
139
- const raw = AstBuilder.#bodyTextOf(ctx);
140
- return {
141
- op: "BUFF",
142
- delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_BUFF().getText(), "BUFF"),
143
- annotation: AstBuilder.#annotationOf(ctx),
144
- ...slots,
145
- body: raw !== null ? AstBuilder.#parseMatcherBody(raw, position) : null,
146
- position,
147
- };
148
- }
149
- static #buildRead(ctx) {
150
- const position = AstBuilder.#positionOf(ctx);
151
- const slots = AstBuilder.#extractTextSlots(ctx.slotModifiers(), position);
152
- const delimiter = AstBuilder.#splitDelimiter(ctx.OPEN_READ().getText(), "READ");
153
- const bodied = AstBuilder.#annotationBody("READ", delimiter, AstBuilder.#annotationOf(ctx), AstBuilder.#bodyTextOf(ctx), position);
154
- const annotation = bodied.annotation;
155
- const raw = bodied.raw;
156
- const targetPath = slots.target?.kind === "url"
157
- ? slots.target.pathname
158
- : slots.target?.raw;
159
- const hasMatcher = raw !== null && raw.trim() !== "";
160
- if (hasMatcher || (targetPath !== undefined && PathSyntax.hasGlob(targetPath))) {
161
- if (slots.lineMarker?.marks.some((mark) => typeof mark === "string") === true) {
162
- throw new PlurnkParseError(position.line, position.column, "visitor", "line anchors require an exact READ target; FIND result positions are numeric");
163
- }
164
- const findSlots = slots;
165
- return {
166
- op: "FIND",
167
- delimiter,
168
- annotation,
169
- ...findSlots,
170
- body: hasMatcher ? AstBuilder.#parseMatcherBody(raw, position) : null,
171
- position,
172
- };
173
- }
174
- return {
175
- op: "READ",
176
- delimiter,
177
- annotation,
178
- ...slots,
179
- body: null,
180
- position,
181
- };
182
- }
183
- static #buildEdit(ctx) {
184
- const position = AstBuilder.#positionOf(ctx);
185
- const slots = AstBuilder.#extractTextSlots(ctx.slotModifiers(), position);
186
- return {
187
- op: "EDIT",
188
- delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_EDIT().getText(), "EDIT"),
189
- annotation: AstBuilder.#annotationOf(ctx),
190
- ...slots,
191
- body: AstBuilder.#bodyTextOf(ctx),
192
- position,
193
- };
194
- }
195
- static #buildCopy(ctx) {
196
- const position = AstBuilder.#positionOf(ctx);
197
- const modifier = ctx.transferModifiers();
198
- const selections = modifier.resourceSelection();
199
- if (selections.length !== 2)
200
- throw new Error("COPY grammar did not produce two resource selections");
201
- return {
202
- op: "COPY",
203
- delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_COPY().getText(), "COPY"),
204
- annotation: AstBuilder.#annotationOf(ctx),
205
- source: AstBuilder.#resourceSelectionFromCtx(selections[0], position),
206
- destination: AstBuilder.#resourceSelectionFromCtx(selections[1], position),
207
- position,
208
- };
209
- }
210
- static #buildMove(ctx) {
211
- const position = AstBuilder.#positionOf(ctx);
212
- const modifier = ctx.transferModifiers();
213
- const selections = modifier.resourceSelection();
214
- if (selections.length !== 2)
215
- throw new Error("MOVE grammar did not produce two resource selections");
216
- return {
217
- op: "MOVE",
218
- delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_MOVE().getText(), "MOVE"),
219
- annotation: AstBuilder.#annotationOf(ctx),
220
- source: AstBuilder.#resourceSelectionFromCtx(selections[0], position),
221
- destination: AstBuilder.#resourceSelectionFromCtx(selections[1], position),
222
- position,
223
- };
224
- }
225
- // {§send-label} — a disposition label makes the SEND terminal and names no recipient.
226
- static #SEND_LABELS = Object.freeze({ NEXT: 102, WAIT: 202, TERM: 200, FAIL: 499 });
227
- static #buildSend(ctx) {
228
- const position = AstBuilder.#positionOf(ctx);
229
- const label = ctx.SEND_LABEL().getText().slice(1, -1);
230
- const status = AstBuilder.#SEND_LABELS[label];
231
- if (status === undefined)
232
- throw new Error(`the lexer admitted an unknown SEND label: ${label}`);
233
- const raw = AstBuilder.#bodyTextOf(ctx);
234
- return {
235
- op: "SEND",
236
- delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_SEND().getText(), "SEND"),
237
- annotation: AstBuilder.#annotationOf(ctx),
238
- status,
239
- target: null,
240
- metadata: null,
241
- // A WAIT keeps its scope ({§send-wait-scope}); the dispatcher owns what it accepts.
242
- lineMarker: AstBuilder.#lineMarkerFromCtx(ctx.lineMarker()),
243
- body: raw !== null ? AstBuilder.#parseSendBody(raw) : null,
244
- position,
245
- };
246
- }
247
- // A mid-turn SEND is a message to its recipient path, or to the user when it names none.
248
- static #buildMidSend(ctx) {
249
- const position = AstBuilder.#positionOf(ctx);
250
- const slots = AstBuilder.#extractBranchSlots(ctx.targetWithMetadata(), position);
251
- const raw = AstBuilder.#bodyTextOf(ctx);
252
- return {
253
- op: "SEND",
254
- delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_SEND().getText(), "SEND"),
255
- annotation: AstBuilder.#annotationOf(ctx),
256
- status: null,
257
- target: slots.target,
258
- metadata: slots.metadata,
259
- lineMarker: null,
260
- body: raw !== null ? AstBuilder.#parseSendBody(raw) : null,
261
- position,
262
- };
263
- }
264
- static #buildExec(ctx) {
265
- const position = AstBuilder.#positionOf(ctx);
266
- const slots = AstBuilder.#extractExecSlots(ctx.execModifiers(), position);
267
- return {
268
- op: "EXEC",
269
- delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_EXEC().getText(), "EXEC"),
270
- annotation: AstBuilder.#annotationOf(ctx),
271
- executor: AstBuilder.#executorOf(ctx, position),
272
- ...slots,
273
- body: AstBuilder.#bodyTextOf(ctx),
274
- position,
275
- };
276
- }
277
- // {§exec-executor-slot} — the executor may lead the heading or trail the path (once, unambiguous:
278
- // no other slot after a path uses `[...]`); two executors are the one rejected shape.
279
- static #executorOf(ctx, pos) {
280
- const found = ctx.EXECUTOR();
281
- const list = Array.isArray(found) ? found : found === null || found === undefined ? [] : [found];
282
- if (list.length > 1) {
283
- throw new PlurnkParseError(pos.line, pos.column, "visitor", "`### EXEC0` accepts one `[executor]`");
284
- }
285
- return list[0]?.getText().slice(1, -1) ?? null;
286
- }
287
- static #buildBare(ctx) {
288
- const position = AstBuilder.#positionOf(ctx);
289
- return {
290
- op: "BARE",
291
- delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_BARE().getText(), "BARE"),
292
- annotation: AstBuilder.#annotationOf(ctx),
293
- target: null,
294
- metadata: null,
295
- lineMarker: null,
296
- body: AstBuilder.#requiredBodyTextOf(ctx),
297
- position,
298
- };
299
- }
300
- static #buildPlan(ctx) {
301
- const position = AstBuilder.#positionOf(ctx);
302
- const slots = AstBuilder.#extractSlots(ctx.slotModifiers(), position);
303
- const rejected = [
304
- slots.target !== null ? "(path)" : null,
305
- slots.metadata !== null ? "{metadata}" : null,
306
- slots.lineMarker !== null ? "<scope>" : null,
307
- ].filter((slot) => slot !== null);
308
- if (rejected.length > 0) {
309
- throw new PlurnkParseError(position.line, position.column, "visitor", `PLAN does not accept ${AstBuilder.#joinTerms(rejected)}.`);
310
- }
311
- return {
312
- op: "PLAN",
313
- delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_PLAN().getText(), "PLAN"),
314
- annotation: AstBuilder.#annotationOf(ctx),
315
- target: null,
316
- metadata: null,
317
- lineMarker: null,
318
- body: PlanValue.admit(AstBuilder.#requiredBodyTextOf(ctx)),
319
- position,
320
- };
321
- }
322
- static #joinTerms(terms) {
323
- if (terms.length < 2)
324
- return terms[0] ?? "";
325
- if (terms.length === 2)
326
- return `${terms[0]} and ${terms[1]}`;
327
- return `${terms.slice(0, -1).join(", ")}, and ${terms.at(-1)}`;
328
- }
329
- static #buildKill(ctx) {
330
- const position = AstBuilder.#positionOf(ctx);
331
- // {§kill-scope} — the scope names lines of a log body or of an entry; null kills the whole target.
332
- const slots = AstBuilder.#extractTextSlots(ctx.slotModifiers(), position);
333
- const raw = AstBuilder.#bodyTextOf(ctx);
334
- return {
335
- op: "KILL",
336
- delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_KILL().getText(), "KILL"),
337
- annotation: AstBuilder.#annotationOf(ctx),
338
- ...slots,
339
- body: raw !== null ? AstBuilder.#parseMatcherBody(raw, position) : null,
340
- position,
341
- };
342
- }
343
- static #buildWork(ctx) {
344
- const position = AstBuilder.#positionOf(ctx);
345
- const slots = AstBuilder.#extractBranchSlots(ctx.targetWithMetadata(), position);
346
- return {
347
- op: "WORK",
348
- delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_WORK().getText(), "WORK"),
349
- annotation: AstBuilder.#annotationOf(ctx),
350
- ...slots,
351
- lineMarker: null,
352
- body: AstBuilder.#requiredBodyTextOf(ctx),
353
- position,
354
- };
355
- }
356
- static #buildFork(ctx) {
357
- const position = AstBuilder.#positionOf(ctx);
358
- const slots = AstBuilder.#extractBranchSlots(ctx.targetWithMetadata(), position);
359
- return {
360
- op: "FORK",
361
- delimiter: AstBuilder.#splitDelimiter(ctx.OPEN_FORK().getText(), "FORK"),
362
- annotation: AstBuilder.#annotationOf(ctx),
363
- ...slots,
364
- lineMarker: null,
365
- body: AstBuilder.#requiredBodyTextOf(ctx),
366
- position,
367
- };
368
- }
369
- static #extractBranchSlots(ctx, pos) {
370
- return {
371
- target: AstBuilder.#targetFromCtx(AstBuilder.#findFirst(ctx, TargetContext), pos),
372
- metadata: AstBuilder.#metadataFromCtx(ctx),
373
- };
374
- }
375
- static #extractSlots(modCtx, pos) {
376
- return {
377
- target: AstBuilder.#targetFromCtx(AstBuilder.#findFirst(modCtx, TargetContext), pos),
378
- metadata: AstBuilder.#metadataFromCtx(modCtx),
379
- lineMarker: AstBuilder.#lineMarkerFromCtx(AstBuilder.#findFirst(modCtx, LineMarkerContext)),
380
- };
381
- }
382
- static #extractTextSlots(modCtx, pos) {
383
- return {
384
- target: AstBuilder.#targetFromCtx(AstBuilder.#findFirst(modCtx, TargetContext), pos),
385
- metadata: AstBuilder.#metadataFromCtx(modCtx),
386
- lineMarker: AstBuilder.#textLineMarkerFromCtx(AstBuilder.#findFirst(modCtx, LineMarkerContext)),
387
- };
388
- }
389
- // Depth-first search for the first terminal of `tokenType`; returns its text or null.
390
- // Lets the SEND/KILL signal read work regardless of which signal rule wrapped it.
391
- static #findToken(root, tokenType) {
392
- if (root === null)
393
- return null;
394
- for (const child of root.children ?? []) {
395
- if (child instanceof TerminalNode && child.symbol.type === tokenType)
396
- return child.getText();
397
- if (child instanceof ParserRuleContext) {
398
- const found = AstBuilder.#findToken(child, tokenType);
399
- if (found !== null)
400
- return found;
401
- }
402
- }
403
- return null;
404
- }
405
- static #extractExecSlots(modCtx, pos) {
406
- // {§exec-executor-slot} — every slot at most once; the grammar admits any order.
407
- const once = (type, slot) => {
408
- const found = AstBuilder.#findAll(modCtx, type);
409
- if (found.length > 1) {
410
- throw new PlurnkParseError(pos.line, pos.column, "visitor", `\`### EXEC0\` accepts ${slot} at most once`);
411
- }
412
- return found[0] ?? null;
413
- };
414
- return {
415
- target: AstBuilder.#targetFromCtx(once(TargetContext, "one `(program)` path"), pos),
416
- metadata: AstBuilder.#metadataFromCtx(modCtx),
417
- lineMarker: AstBuilder.#lineMarkerFromCtx(once(LineMarkerContext, "one `<scope>`")),
418
- };
419
- }
420
- static #findAll(root, type) {
421
- if (root === null)
422
- return [];
423
- if (root instanceof type)
424
- return [root];
425
- return (root.children ?? []).flatMap((child) => child instanceof ParserRuleContext ? AstBuilder.#findAll(child, type) : []);
426
- }
427
- static #findFirst(root, type) {
428
- if (root === null)
429
- return null;
430
- if (root instanceof type)
431
- return root;
432
- const children = root.children;
433
- if (!children)
434
- return null;
435
- for (const child of children) {
436
- if (child instanceof ParserRuleContext) {
437
- const found = AstBuilder.#findFirst(child, type);
438
- if (found !== null)
439
- return found;
440
- }
441
- }
442
- return null;
443
- }
444
- static #targetFromCtx(ctx, pos) {
445
- if (ctx === null)
446
- return null;
447
- const text = ctx.TARGET_TEXT().map((token) => token.getText()).join("");
448
- return AstBuilder.parsePath(text, pos);
449
- }
450
- static #metadataFromCtx(ctx) {
451
- const blocks = AstBuilder.#findAll(ctx, MetadataContext);
452
- return blocks.length === 0
453
- ? null
454
- : blocks.map((block) => block.METADATA_TEXT().map((token) => token.getText()).join(""));
455
- }
456
- static #resourceSelectionFromCtx(ctx, pos) {
457
- const target = AstBuilder.#targetFromCtx(AstBuilder.#findFirst(ctx, TargetContext), pos);
458
- if (target === null)
459
- throw new Error("resource selection grammar did not produce a target");
460
- return {
461
- target,
462
- metadata: AstBuilder.#metadataFromCtx(ctx),
463
- lineMarker: AstBuilder.#textLineMarkerFromCtx(AstBuilder.#findFirst(ctx, LineMarkerContext)),
464
- };
465
- }
466
- static #lineMarkerFromCtx(ctx) {
467
- if (ctx === null)
468
- return null;
469
- const text = ctx.L_MARKER()?.getText() ?? "";
470
- return AstBuilder.#parseLineMarker(text);
471
- }
472
- static #textLineMarkerFromCtx(ctx) {
473
- if (ctx === null)
474
- return null;
475
- const text = ctx.L_MARKER()?.getText() ?? "";
476
- return AstBuilder.#parseTextLineMarker(text);
477
- }
478
- static #parseTextLineMarker(text) {
479
- if (!text.includes("@"))
480
- return AstBuilder.#parseLineMarker(text);
481
- const marks = text.slice(1, -1).split(/, ?/).map((component) => component.startsWith("@") ? component : Number.parseFloat(component));
482
- return { marks: marks };
483
- }
484
- static #positionOf(ctx) {
485
- const start = ctx.start;
486
- return { line: start?.line ?? 0, column: start?.column ?? 0 };
487
- }
488
- static #annotationOf(ctx) {
489
- const token = AstBuilder.#findToken(ctx, plurnkLexer.ANNOTATION);
490
- return token === null ? null : token.slice("<!--".length, -"-->".length).trim();
491
- }
492
- static #bodyTextOf(ctx) {
493
- return AstBuilder.#findFirst(ctx, BodyContext)?.getText() ?? null;
494
- }
495
- static #requiredBodyTextOf(ctx) {
496
- return AstBuilder.#bodyTextOf(ctx) ?? "";
497
- }
498
- static #splitDelimiter(headingText, op) {
499
- const marker = op === "PLAN" ? "## " : "### ";
500
- return headingText.slice(marker.length + op.length);
501
- }
502
- static #isDigit(c) {
503
- return c !== undefined && c >= "0" && c <= "9";
504
- }
505
- // Scans `<scope>` into ordered numeric components. Separators are `,`
506
- // (with an optional space) or `-`; a `-` immediately starting a component is its
507
- // sign, not a separator. The operation owner assigns roles. {§scope-marker-forms}
508
- static #parseLineMarker(text) {
509
- const inner = text.slice(1, -1);
510
- const marks = [];
511
- let i = 0;
512
- while (i < inner.length) {
513
- let j = i;
514
- if (inner[j] === "-")
515
- j++;
516
- while (AstBuilder.#isDigit(inner[j]))
517
- j++;
518
- if (inner[j] === "." && AstBuilder.#isDigit(inner[j + 1])) {
519
- j++;
520
- while (AstBuilder.#isDigit(inner[j]))
521
- j++;
522
- }
523
- marks.push(Number.parseFloat(inner.slice(i, j)));
524
- i = j;
525
- if (inner[i] === ",") {
526
- i++;
527
- if (inner[i] === " ")
528
- i++;
529
- }
530
- else if (inner[i] === "-") {
531
- i++;
532
- }
533
- else {
534
- break;
535
- }
536
- }
537
- // L_MARKER always matches at least one number, so marks is non-empty.
538
- return { marks: marks };
539
- }
540
- /**
541
- * Apply target-slot decomposition without round-tripping through a statement.
542
- * Returns null for empty input and throws PlurnkParseError when a scheme URL
543
- * fails WHATWG admission. {§path-syntax}
544
- */
545
- static parsePath(raw, pos = { line: 0, column: 0 }) {
546
- if (raw.length === 0)
547
- return null;
548
- const target = PathSyntax.unescapeTarget(raw);
549
- if (!AstBuilder.#SCHEME_PATTERN.test(target)) {
550
- return { kind: "local", raw: target };
551
- }
552
- const protectedTarget = AstBuilder.#protectPathBraces(target);
553
- let url;
554
- try {
555
- url = new URL(protectedTarget.value);
556
- }
557
- catch {
558
- throw new PlurnkParseError(pos.line, pos.column, "visitor", "invalid URI in path");
559
- }
560
- // Uniform WHATWG decomposition — no per-scheme authority allowlist. `://`
561
- // introduces an authority for every scheme; an authority-less reference
562
- // writes the empty-authority form `scheme:///path` (host parses empty).
563
- // Whether a given scheme should carry an authority is a runtime concern,
564
- // not the grammar's; the parser just reports what the standard parsed.
565
- const parsed = {
566
- kind: "url",
567
- raw: target,
568
- scheme: url.protocol.replace(/:$/, ""),
569
- username: url.username || null,
570
- password: url.password || null,
571
- hostname: url.hostname || null,
572
- port: url.port ? Number.parseInt(url.port, 10) : null,
573
- pathname: protectedTarget.restore(url.pathname),
574
- query: AstBuilder.#queryFrom(url),
575
- fragment: url.hash ? url.hash.slice(1) : null,
576
- };
577
- return parsed;
578
- }
579
- // WHATWG percent-encodes raw braces. Protect only authored path braces while
580
- // decomposing so brace globs remain distinguishable from authored `%7B`/`%7D`
581
- // literal path data. Query and fragment spelling remain untouched.
582
- static #protectPathBraces(target) {
583
- const authorityStart = target.indexOf("://") + 3;
584
- const pathStart = target.indexOf("/", authorityStart);
585
- if (pathStart < 0)
586
- return { value: target, restore: (pathname) => pathname };
587
- const queryStart = target.indexOf("?", pathStart);
588
- const fragmentStart = target.indexOf("#", pathStart);
589
- const endings = [queryStart, fragmentStart].filter((index) => index >= 0);
590
- const pathEnd = endings.length === 0 ? target.length : Math.min(...endings);
591
- const rawPath = target.slice(pathStart, pathEnd);
592
- if (!/[{}]/u.test(rawPath))
593
- return { value: target, restore: (pathname) => pathname };
594
- const sentinels = [];
595
- for (let codePoint = 0xF0000; sentinels.length < 2; codePoint += 1) {
596
- const character = String.fromCodePoint(codePoint);
597
- const encoded = encodeURIComponent(character);
598
- if (!target.includes(character) && !target.toUpperCase().includes(encoded))
599
- sentinels.push(encoded);
600
- }
601
- const [open, close] = sentinels;
602
- const protectedPath = rawPath.replaceAll("{", open).replaceAll("}", close);
603
- return {
604
- value: target.slice(0, pathStart) + protectedPath + target.slice(pathEnd),
605
- restore: (pathname) => pathname.replaceAll(open, "{").replaceAll(close, "}"),
606
- };
607
- }
608
- static #queryFrom(url) {
609
- const queryStart = url.href.indexOf("?");
610
- if (queryStart === -1)
611
- return null;
612
- const fragmentStart = url.href.indexOf("#", queryStart);
613
- return url.href.slice(queryStart + 1, fragmentStart === -1 ? undefined : fragmentStart);
614
- }
615
- // The leading prefix claims its dialect; failed claimed syntax never falls back
616
- // to glob. XPath's `//` is classified before regex `/`. {§matcher-prefix-claims}
617
- static #parseMatcherBody(body, pos) {
618
- // At statement EOF ANTLR retains one ordinary terminating line ending in
619
- // BODY_TEXT; before a following heading the lexer consumes that same EOL as
620
- // SECTION_END. Normalize the equivalent surfaces before enforcing one line.
621
- const raw = body.replace(/(?:\r\n|\r|\n)$/u, "");
622
- const lineCount = raw.split(/\r\n|\r|\n/u).length;
623
- if (lineCount !== 1) {
624
- throw new PlurnkParseError(pos.line, pos.column, "visitor", `Matcher body has ${lineCount} lines; expected 1.`);
625
- }
626
- if (raw.startsWith("//")) {
627
- try {
628
- xpath.parse(raw);
629
- }
630
- catch (e) {
631
- throw new PlurnkParseError(pos.line, pos.column, "visitor", `pattern leads with \`//\` but is not a valid xpath selector - ${AstBuilder.#detail(e)}`);
632
- }
633
- return { dialect: "xpath", raw };
634
- }
635
- if (raw.startsWith("/")) {
636
- const regex = AstBuilder.#tryParseSlashRegex(raw);
637
- if (regex.ok)
638
- return { dialect: "regex", raw, pattern: regex.pattern, flags: regex.flags };
639
- if (regex.reason === "invalid" && /^[\t ]/u.test(regex.flags)) {
640
- throw new PlurnkParseError(pos.line, pos.column, "visitor", "Regex matcher has trailing text after its closing `/`; operation modifiers precede the line ending, and the matcher occupies the next line: `### FIND0 (path) <scope>` above, `/pattern/` below.");
641
- }
642
- const slashRecovery = regex.reason === "invalid"
643
- && regex.detail.includes("Invalid flags supplied")
644
- ? " - use only ECMAScript flags after the closing `/`; escape a literal `/` inside the pattern as `\\/`"
645
- : "";
646
- // Quote the offending matcher so a multi-op emission's failure is
647
- // unambiguous about WHICH body failed (a correct sibling regex must
648
- // not take the blame for a broken one).
649
- const excerpt = raw.length > 80 ? `${raw.slice(0, 80)}…` : raw;
650
- throw new PlurnkParseError(pos.line, pos.column, "visitor", regex.reason === "unclosed"
651
- ? `regex matcher must use \`/pattern/flags\`; this matcher has no closing \`/\`: \`${excerpt}\``
652
- : `pattern leads with \`/\` but is not a valid \`/pattern/flags\` regex - ${regex.detail}${slashRecovery}: \`${excerpt}\``);
653
- }
654
- if (raw.startsWith("$")) {
655
- // Compile-only RFC 9535 admission through the shared json-p3 engine.
656
- try {
657
- AstBuilder.#JSONPATH.compile(raw);
658
- }
659
- catch (e) {
660
- throw new PlurnkParseError(pos.line, pos.column, "visitor", `pattern leads with \`$\` but is not a valid jsonpath - ${AstBuilder.#detail(e)}`);
661
- }
662
- return { dialect: "jsonpath", raw };
663
- }
664
- if (raw.startsWith("~"))
665
- return { dialect: "semantic", raw };
666
- if (raw.startsWith("&")) {
667
- if (!AstBuilder.#GRAPH_MATCHER.test(raw)) {
668
- throw new PlurnkParseError(pos.line, pos.column, "visitor", "Malformed graph matcher; expected `&symbol`, `&<symbol`, or `&>symbol`.");
669
- }
670
- return { dialect: "graph", raw };
671
- }
672
- return { dialect: "glob", raw };
673
- }
674
- static #detail(e) {
675
- return e instanceof Error ? e.message : String(e);
676
- }
677
- // Splits an ECMAScript `/pattern/flags` literal. Backslash escapes and character
678
- // classes keep a slash inside the pattern; the first unescaped slash outside a
679
- // class closes it. The native constructor owns pattern and flag validity.
680
- static #tryParseSlashRegex(raw) {
681
- let i = 1;
682
- let inClass = false;
683
- while (i < raw.length) {
684
- if (raw[i] === "\\") {
685
- i += 2;
686
- continue;
687
- }
688
- if (raw[i] === "[") {
689
- inClass = true;
690
- i++;
691
- continue;
692
- }
693
- if (raw[i] === "]" && inClass) {
694
- inClass = false;
695
- i++;
696
- continue;
697
- }
698
- if (raw[i] === "/" && !inClass)
699
- break;
700
- i++;
701
- }
702
- if (i >= raw.length)
703
- return { ok: false, reason: "unclosed" };
704
- const pattern = raw.slice(1, i);
705
- const flags = raw.slice(i + 1);
706
- try {
707
- new RegExp(pattern, flags);
708
- }
709
- catch (e) {
710
- return { ok: false, reason: "invalid", detail: AstBuilder.#detail(e), flags };
711
- }
712
- return { ok: true, pattern, flags };
713
- }
714
- static #parseSendBody(raw) {
715
- let json = null;
716
- try {
717
- json = JSON.parse(raw);
718
- }
719
- catch { /* best-effort */ }
720
- return { raw, json };
721
- }
722
- }
723
- //# sourceMappingURL=AstBuilder.js.map