@nhtio/adk 1.20260609.0 → 1.20260610.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 (134) hide show
  1. package/CHANGELOG.md +132 -9
  2. package/batteries/llm/ollama/helpers.cjs +9 -0
  3. package/batteries/llm/ollama/helpers.cjs.map +1 -1
  4. package/batteries/llm/ollama/helpers.mjs +9 -0
  5. package/batteries/llm/ollama/helpers.mjs.map +1 -1
  6. package/batteries/llm/openai_chat_completions/helpers.cjs +19 -0
  7. package/batteries/llm/openai_chat_completions/helpers.cjs.map +1 -1
  8. package/batteries/llm/openai_chat_completions/helpers.mjs +19 -0
  9. package/batteries/llm/openai_chat_completions/helpers.mjs.map +1 -1
  10. package/batteries/media/builder.d.ts +245 -0
  11. package/batteries/media/contracts.cjs +119 -0
  12. package/batteries/media/contracts.cjs.map +1 -0
  13. package/batteries/media/contracts.d.ts +321 -0
  14. package/batteries/media/contracts.mjs +110 -0
  15. package/batteries/media/contracts.mjs.map +1 -0
  16. package/batteries/media/engines/audio_decode.cjs +92 -0
  17. package/batteries/media/engines/audio_decode.cjs.map +1 -0
  18. package/batteries/media/engines/audio_decode.d.ts +46 -0
  19. package/batteries/media/engines/audio_decode.mjs +90 -0
  20. package/batteries/media/engines/audio_decode.mjs.map +1 -0
  21. package/batteries/media/engines/execa_executor.cjs +64 -0
  22. package/batteries/media/engines/execa_executor.cjs.map +1 -0
  23. package/batteries/media/engines/execa_executor.d.ts +54 -0
  24. package/batteries/media/engines/execa_executor.mjs +62 -0
  25. package/batteries/media/engines/execa_executor.mjs.map +1 -0
  26. package/batteries/media/engines/fs_workspace.cjs +84 -0
  27. package/batteries/media/engines/fs_workspace.cjs.map +1 -0
  28. package/batteries/media/engines/fs_workspace.d.ts +51 -0
  29. package/batteries/media/engines/fs_workspace.mjs +82 -0
  30. package/batteries/media/engines/fs_workspace.mjs.map +1 -0
  31. package/batteries/media/engines/jimp.cjs +116 -0
  32. package/batteries/media/engines/jimp.cjs.map +1 -0
  33. package/batteries/media/engines/jimp.d.ts +32 -0
  34. package/batteries/media/engines/jimp.mjs +114 -0
  35. package/batteries/media/engines/jimp.mjs.map +1 -0
  36. package/batteries/media/engines/sharp.cjs +120 -0
  37. package/batteries/media/engines/sharp.cjs.map +1 -0
  38. package/batteries/media/engines/sharp.d.ts +42 -0
  39. package/batteries/media/engines/sharp.mjs +117 -0
  40. package/batteries/media/engines/sharp.mjs.map +1 -0
  41. package/batteries/media/engines/soffice.cjs +246 -0
  42. package/batteries/media/engines/soffice.cjs.map +1 -0
  43. package/batteries/media/engines/soffice.d.ts +39 -0
  44. package/batteries/media/engines/soffice.mjs +244 -0
  45. package/batteries/media/engines/soffice.mjs.map +1 -0
  46. package/batteries/media/engines/tesseract_js.cjs +87 -0
  47. package/batteries/media/engines/tesseract_js.cjs.map +1 -0
  48. package/batteries/media/engines/tesseract_js.d.ts +41 -0
  49. package/batteries/media/engines/tesseract_js.mjs +85 -0
  50. package/batteries/media/engines/tesseract_js.mjs.map +1 -0
  51. package/batteries/media/engines/transformers_asr.cjs +111 -0
  52. package/batteries/media/engines/transformers_asr.cjs.map +1 -0
  53. package/batteries/media/engines/transformers_asr.d.ts +41 -0
  54. package/batteries/media/engines/transformers_asr.mjs +109 -0
  55. package/batteries/media/engines/transformers_asr.mjs.map +1 -0
  56. package/batteries/media/exceptions.d.ts +103 -0
  57. package/batteries/media/forge.cjs +403 -0
  58. package/batteries/media/forge.cjs.map +1 -0
  59. package/batteries/media/forge.d.ts +90 -0
  60. package/batteries/media/forge.mjs +399 -0
  61. package/batteries/media/forge.mjs.map +1 -0
  62. package/batteries/media/formats.d.ts +72 -0
  63. package/batteries/media/index.d.ts +136 -0
  64. package/batteries/media/lint.cjs +339 -0
  65. package/batteries/media/lint.cjs.map +1 -0
  66. package/batteries/media/lint.d.ts +117 -0
  67. package/batteries/media/lint.mjs +331 -0
  68. package/batteries/media/lint.mjs.map +1 -0
  69. package/batteries/media/pipe.d.ts +66 -0
  70. package/batteries/media/plan.d.ts +133 -0
  71. package/batteries/media/registry.d.ts +92 -0
  72. package/batteries/media/runtime.d.ts +105 -0
  73. package/batteries/media/steps/doc.d.ts +33 -0
  74. package/batteries/media/steps/image_audio.d.ts +24 -0
  75. package/batteries/media/steps/ingest.d.ts +25 -0
  76. package/batteries/media/steps/pages.d.ts +18 -0
  77. package/batteries/media/steps/sheet.d.ts +36 -0
  78. package/batteries/media/steps/slides.d.ts +35 -0
  79. package/batteries/media/steps/text.d.ts +43 -0
  80. package/batteries/media/validate.d.ts +49 -0
  81. package/batteries/media/verbs.d.ts +126 -0
  82. package/batteries/media.cjs +3049 -0
  83. package/batteries/media.cjs.map +1 -0
  84. package/batteries/media.mjs +3009 -0
  85. package/batteries/media.mjs.map +1 -0
  86. package/batteries/tools/_shared/index.d.ts +142 -0
  87. package/batteries/tools/_shared.cjs +173 -0
  88. package/batteries/tools/_shared.cjs.map +1 -0
  89. package/batteries/tools/_shared.mjs +164 -0
  90. package/batteries/tools/_shared.mjs.map +1 -0
  91. package/batteries/tools/index.d.ts +2 -0
  92. package/batteries/tools/scrapper/exceptions.d.ts +21 -0
  93. package/batteries/tools/scrapper/index.d.ts +172 -0
  94. package/batteries/tools/scrapper/shared.d.ts +146 -0
  95. package/batteries/tools/scrapper.cjs +8 -0
  96. package/batteries/tools/scrapper.mjs +2 -0
  97. package/batteries/tools/searxng/index.d.ts +54 -20
  98. package/batteries/tools/searxng.cjs +2 -1
  99. package/batteries/tools/searxng.mjs +2 -2
  100. package/batteries/tools/web_retrieval/index.d.ts +186 -0
  101. package/batteries/tools/web_retrieval.cjs +206 -0
  102. package/batteries/tools/web_retrieval.cjs.map +1 -0
  103. package/batteries/tools/web_retrieval.mjs +201 -0
  104. package/batteries/tools/web_retrieval.mjs.map +1 -0
  105. package/batteries/tools.cjs +13 -1
  106. package/batteries/tools.mjs +4 -2
  107. package/batteries.cjs +13 -1
  108. package/batteries.mjs +4 -2
  109. package/common.d.ts +1 -1
  110. package/eslint.cjs +1 -1
  111. package/eslint.mjs +1 -1
  112. package/exceptions-C7FSHEnV.mjs +87 -0
  113. package/exceptions-C7FSHEnV.mjs.map +1 -0
  114. package/exceptions-CQi_lNs1.js +152 -0
  115. package/exceptions-CQi_lNs1.js.map +1 -0
  116. package/index.cjs +2 -2
  117. package/index.mjs +2 -2
  118. package/mcp/adk-docs-corpus.json +1 -1
  119. package/package.json +301 -178
  120. package/scrapper-BOLWYGbD.js +463 -0
  121. package/scrapper-BOLWYGbD.js.map +1 -0
  122. package/scrapper-hDKlNuCT.mjs +433 -0
  123. package/scrapper-hDKlNuCT.mjs.map +1 -0
  124. package/{searxng-Bkrwhwhw.js → searxng-CJtEpa8p.js} +82 -85
  125. package/searxng-CJtEpa8p.js.map +1 -0
  126. package/{searxng-CyA-nEu5.mjs → searxng-riarj_0u.mjs} +76 -85
  127. package/searxng-riarj_0u.mjs.map +1 -0
  128. package/skills/adk-assembly/SKILL.md +2 -2
  129. package/validate-BFaUYHDN.js +1298 -0
  130. package/validate-BFaUYHDN.js.map +1 -0
  131. package/validate-DSZ3wicB.mjs +1215 -0
  132. package/validate-DSZ3wicB.mjs.map +1 -0
  133. package/searxng-Bkrwhwhw.js.map +0 -1
  134. package/searxng-CyA-nEu5.mjs.map +0 -1
@@ -0,0 +1,1215 @@
1
+ import { c as isObject, o as isError } from "./tool_registry-791Vrjtf.mjs";
2
+ import "./guards.mjs";
3
+ import { PCM_MIME } from "./batteries/media/contracts.mjs";
4
+ import { a as E_MEDIA_NOT_PIPE_EXPRESSIBLE, i as E_MEDIA_MISSING_ARG, l as E_MEDIA_UNKNOWN_ARG, n as E_MEDIA_BAD_ARG, r as E_MEDIA_ENGINE_REQUIRED, u as E_MEDIA_UNKNOWN_VERB } from "./exceptions-C7FSHEnV.mjs";
5
+ //#region src/batteries/media/plan.ts
6
+ /**
7
+ * The neutral `MediaPlan` intermediate representation and its canonical serializers.
8
+ *
9
+ * @remarks
10
+ * Internal sibling of the `@nhtio/adk/batteries/media` entry. All three front-ends — the
11
+ * chainable builder, the pipe-string parser, and the JSON ops array — compile to this one IR,
12
+ * and the step runtime consumes only this IR. Invariants (frozen design, section 0.7):
13
+ *
14
+ * - **Serializable.** No live `RegExp`, `Media`, or function values — regexes are
15
+ * `{ source, flags }`, media refs are typed handles. A plan can be logged, content-hashed,
16
+ * and embedded in a tool result.
17
+ * - **Round-trip is fixed-point.** `parsePipe(toPipe(plan))` produces an equal plan and
18
+ * `toPipe` is idempotent. `toPipe(parsePipe(s)) === s` is NOT promised (e.g. `2,3,4,5`
19
+ * renders as `2-5`).
20
+ * - **Engine-agnostic.** Steps name verbs, never engines.
21
+ */
22
+ /** `true` when `value` is a {@link RegExpRef}. */
23
+ var isRegExpRef = (value) => {
24
+ if (!isObject(value)) return false;
25
+ const v = value;
26
+ return typeof v.source === "string" && typeof v.flags === "string" && Object.keys(v).length === 2;
27
+ };
28
+ /** `true` when `value` is a {@link MediaRef}. */
29
+ var isMediaRef = (value) => {
30
+ if (!isObject(value)) return false;
31
+ const v = value;
32
+ if (v.kind === "id") return typeof v.id === "string";
33
+ if (v.kind === "builder") return isObject(v.plan);
34
+ return false;
35
+ };
36
+ /** Characters legal in an unquoted bareword value. */
37
+ var BAREWORD = /^[A-Za-z0-9_]+$/;
38
+ /** Strings that would lex as a non-ident token and therefore must be quoted. */
39
+ var LEXES_AS_OTHER = /^(?:\d+(?:-\d+)?|\d+\.\d+|true|false|@.*)$/;
40
+ /**
41
+ * The frozen quoting predicate: quote any string that is empty, would lex as a number, range,
42
+ * boolean, or `@id` token, starts with `/`, or contains a character outside `[A-Za-z0-9_]`.
43
+ */
44
+ var renderString = (s) => {
45
+ if (s.length === 0 || !BAREWORD.test(s) || LEXES_AS_OTHER.test(s) || s.startsWith("/")) return `"${s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n")}"`;
46
+ return s;
47
+ };
48
+ /** Canonicalize regex flags: validate against JS flags and sort. */
49
+ var canonicalFlags = (flags) => Array.from(flags).sort().join("");
50
+ /** Render a {@link RegExpRef} back to a pipe regex literal, re-escaping bare slashes. */
51
+ var renderRegExp = (ref) => {
52
+ const source = ref.source.length === 0 ? "(?:)" : ref.source;
53
+ let out = "";
54
+ let inClass = false;
55
+ for (let i = 0; i < source.length; i++) {
56
+ const ch = source[i];
57
+ if (ch === "\\") {
58
+ out += ch + (source[i + 1] ?? "");
59
+ i++;
60
+ continue;
61
+ }
62
+ if (ch === "[") inClass = true;
63
+ else if (ch === "]") inClass = false;
64
+ if (ch === "/" && !inClass) {
65
+ out += "\\/";
66
+ continue;
67
+ }
68
+ out += ch;
69
+ }
70
+ return `/${out}/${canonicalFlags(ref.flags)}`;
71
+ };
72
+ /**
73
+ * Compress maximal ascending consecutive runs in a number list to `a-b` range tokens.
74
+ * Order-preserving and lossless on the array; never sorts or dedupes.
75
+ */
76
+ var renderNumberList = (nums) => {
77
+ const parts = [];
78
+ let i = 0;
79
+ while (i < nums.length) {
80
+ let j = i;
81
+ while (j + 1 < nums.length && Number.isInteger(nums[j]) && nums[j + 1] === nums[j] + 1) j++;
82
+ if (j - i >= 1 && Number.isInteger(nums[i])) parts.push(`${nums[i]}-${nums[j]}`);
83
+ else {
84
+ parts.push(String(nums[i]));
85
+ j = i;
86
+ }
87
+ i = j + 1;
88
+ }
89
+ return parts.join(",");
90
+ };
91
+ var renderScalar = (value) => {
92
+ if (typeof value === "number") return String(value);
93
+ if (typeof value === "boolean") return String(value);
94
+ if (typeof value === "string") return renderString(value);
95
+ if (isRegExpRef(value)) return renderRegExp(value);
96
+ if (value.kind === "id") return `@${value.id}`;
97
+ throw new E_MEDIA_NOT_PIPE_EXPRESSIBLE(["nested builder refs have no pipe form; use toOps()"]);
98
+ };
99
+ var isScalar = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean" || isRegExpRef(value) || isMediaRef(value);
100
+ var renderValue = (value) => {
101
+ if (value === null) throw new E_MEDIA_NOT_PIPE_EXPRESSIBLE(["null is not a flat pipe value; use toOps()"]);
102
+ if (Array.isArray(value)) {
103
+ const arr = value;
104
+ if (arr.length > 0 && arr.every((v) => typeof v === "number")) return renderNumberList(arr);
105
+ if (arr.every(isScalar)) return arr.map(renderScalar).join(",");
106
+ return `'${JSON.stringify(value)}'`;
107
+ }
108
+ if (isScalar(value)) return renderScalar(value);
109
+ return `'${JSON.stringify(value)}'`;
110
+ };
111
+ /**
112
+ * Render a {@link MediaPlan} to its canonical pipe string.
113
+ *
114
+ * @remarks
115
+ * Total for every plan except those containing builder-variant media refs, which throw
116
+ * {@link E_MEDIA_NOT_PIPE_EXPRESSIBLE}. Structured args render as quoted JSON. The output is
117
+ * canonical: dot-namespaced verbs render as space-separated words, number runs compress to
118
+ * ranges, strings quote per the frozen predicate.
119
+ *
120
+ * @param plan - The plan to render.
121
+ * @returns The canonical pipe expression.
122
+ */
123
+ var toPipe = (plan) => plan.steps.map((step) => {
124
+ const verb = step.verb.replace(/\./g, " ");
125
+ const args = Object.entries(step.args).map(([k, v]) => `${k}=${renderValue(v)}`).join(" ");
126
+ return args.length > 0 ? `${verb} ${args}` : verb;
127
+ }).join(" | ");
128
+ /**
129
+ * Render a {@link MediaPlan} to its JSON ops array. Total — every plan has an ops form.
130
+ *
131
+ * @param plan - The plan to render.
132
+ * @returns The ops array (spans stripped).
133
+ */
134
+ var toOps = (plan) => plan.steps.map((step) => ({
135
+ verb: step.verb,
136
+ args: step.args
137
+ }));
138
+ /**
139
+ * Build a {@link MediaPlan} from a JSON ops array. The inverse of {@link toOps}.
140
+ *
141
+ * @remarks
142
+ * Performs structural normalization only (verb-id folding via the caller's verb table happens
143
+ * in validation, not here). Steps carry no spans.
144
+ *
145
+ * @param ops - The ops array.
146
+ * @returns The equivalent plan.
147
+ */
148
+ var fromOps = (ops) => ({ steps: ops.map((op) => ({
149
+ verb: op.verb,
150
+ args: op.args
151
+ })) });
152
+ //#endregion
153
+ //#region src/batteries/media/verbs.ts
154
+ /**
155
+ * The canonical verb table: every verb the media DSL knows, with arg schemas, engine
156
+ * requirements, format-family applicability, and output kinds.
157
+ *
158
+ * @remarks
159
+ * Internal sibling of the `@nhtio/adk/batteries/media` entry. This table is the single source
160
+ * of truth consumed by:
161
+ *
162
+ * - the pipe parser's semantic validator (unknown-verb/arg detection, did-you-mean),
163
+ * - the plan compiler (arg coercion + constraint checks),
164
+ * - the engine-narrowing pass (which verbs a deployment advertises),
165
+ * - the forge (generating the `media_query` tool description and few-shot examples),
166
+ * - the builder (typed front-end methods map 1:1 onto entries here).
167
+ *
168
+ * Frozen design decisions (design doc section 0): canonical verb ids are dot-namespaced
169
+ * snake_case; verb matching is separator-insensitive (space/`_`/`.` fold); args are named-only;
170
+ * indices are 1-based everywhere; arg names follow one-meaning-one-name (`to`, `with`, `match`,
171
+ * `replace`, `order`, `pages`, `at`).
172
+ */
173
+ /** Conversion targets supported by `convert` (matches the server's enum exactly). */
174
+ var CONVERT_TARGETS = [
175
+ "pdf",
176
+ "html",
177
+ "txt",
178
+ "md",
179
+ "csv",
180
+ "json",
181
+ "docx",
182
+ "doc",
183
+ "rtf",
184
+ "odt",
185
+ "xlsx",
186
+ "xls",
187
+ "ods",
188
+ "pptx",
189
+ "ppt",
190
+ "odp"
191
+ ];
192
+ /** Image output formats supported by `image.format`. */
193
+ var IMAGE_FORMATS = [
194
+ "png",
195
+ "jpg",
196
+ "jpeg",
197
+ "webp",
198
+ "tiff",
199
+ "avif"
200
+ ];
201
+ /** The canonical verb table. Order is presentation order in generated grammar text. */
202
+ var VERBS = [
203
+ {
204
+ id: "convert",
205
+ description: "Convert the media to another format.",
206
+ args: { to: {
207
+ type: "enum",
208
+ required: true,
209
+ values: CONVERT_TARGETS,
210
+ description: "Target format."
211
+ } },
212
+ requires: { capability: "convert" },
213
+ appliesTo: [
214
+ "document",
215
+ "spreadsheet",
216
+ "presentation"
217
+ ],
218
+ output: "media"
219
+ },
220
+ {
221
+ id: "select",
222
+ description: "Keep only the listed pages/slides/sections (1-based), producing one file.",
223
+ args: { pages: {
224
+ type: "number-list",
225
+ required: true,
226
+ min: 1,
227
+ description: "Pages (or slides/sections) to keep, 1-based. Ranges allowed: 2-5,8."
228
+ } },
229
+ appliesTo: ["document", "presentation"],
230
+ output: "media"
231
+ },
232
+ {
233
+ id: "split",
234
+ description: "Split the media into multiple files by page or section.",
235
+ args: {
236
+ by: {
237
+ type: "enum",
238
+ values: ["page", "section"],
239
+ description: "Split unit. Default: page."
240
+ },
241
+ ranges: {
242
+ type: "json",
243
+ description: "Explicit grouping as a JSON array of [start,end] pairs (1-based), e.g. '[[1,3],[5,7]]'."
244
+ }
245
+ },
246
+ appliesTo: ["document", "presentation"],
247
+ output: "media-list"
248
+ },
249
+ {
250
+ id: "merge",
251
+ description: "Merge other media into this one, in order.",
252
+ args: { with: {
253
+ type: "media-ref-list",
254
+ required: true,
255
+ description: "Other media to append, as @id refs: with=@<media id>,@<media id>."
256
+ } },
257
+ appliesTo: ["document", "presentation"],
258
+ output: "media"
259
+ },
260
+ {
261
+ id: "reorder",
262
+ description: "Reorder pages/slides/sections by the given 1-based index order.",
263
+ args: { order: {
264
+ type: "number-list",
265
+ required: true,
266
+ min: 1,
267
+ description: "New order of existing 1-based indices."
268
+ } },
269
+ appliesTo: ["document", "presentation"],
270
+ output: "media"
271
+ },
272
+ {
273
+ id: "redact",
274
+ description: "Redact matching text. Prefer literal strings; /regex/ is supported.",
275
+ args: {
276
+ match: {
277
+ type: "regex-or-string-list",
278
+ required: true,
279
+ description: "Literal string(s) or a /regex/ to redact."
280
+ },
281
+ replace: {
282
+ type: "string",
283
+ description: "Replacement text. Default: blackout/removal."
284
+ }
285
+ },
286
+ appliesTo: [
287
+ "document",
288
+ "spreadsheet",
289
+ "presentation"
290
+ ],
291
+ output: "media"
292
+ },
293
+ {
294
+ id: "sanitize",
295
+ description: "Remove potentially unsafe embedded content from the media.",
296
+ args: {},
297
+ appliesTo: [
298
+ "document",
299
+ "spreadsheet",
300
+ "presentation"
301
+ ],
302
+ output: "media"
303
+ },
304
+ {
305
+ id: "normalize",
306
+ description: "Normalize the media structure/encoding for downstream consistency.",
307
+ args: {},
308
+ appliesTo: [
309
+ "document",
310
+ "spreadsheet",
311
+ "presentation"
312
+ ],
313
+ output: "media"
314
+ },
315
+ {
316
+ id: "update_text",
317
+ description: "Replace the first occurrence of an anchor text.",
318
+ args: {
319
+ anchor: {
320
+ type: "string",
321
+ required: true,
322
+ description: "Existing text to find."
323
+ },
324
+ replace: {
325
+ type: "string",
326
+ required: true,
327
+ description: "Replacement text (empty string deletes)."
328
+ }
329
+ },
330
+ appliesTo: ["document", "presentation"],
331
+ output: "media"
332
+ },
333
+ {
334
+ id: "diff",
335
+ description: "Compare this media against another; returns a structured diff.",
336
+ args: { with: {
337
+ type: "media-ref",
338
+ required: true,
339
+ description: "The media to compare against, as an @id ref."
340
+ } },
341
+ appliesTo: [
342
+ "document",
343
+ "spreadsheet",
344
+ "presentation"
345
+ ],
346
+ output: "json"
347
+ },
348
+ {
349
+ id: "apply_patch",
350
+ description: "Apply a unified-diff patch to the media text.",
351
+ args: {
352
+ patch: {
353
+ type: "string",
354
+ required: true,
355
+ description: "The unified diff content."
356
+ },
357
+ with: {
358
+ type: "media-ref-list",
359
+ description: "Optional additional context media, as @id refs."
360
+ }
361
+ },
362
+ appliesTo: ["document"],
363
+ output: "media"
364
+ },
365
+ {
366
+ id: "extract.text",
367
+ description: "Extract text from any supported media (document, spreadsheet, image). To limit pages: select pages=… | extract text.",
368
+ args: {
369
+ ocr: {
370
+ type: "enum",
371
+ values: [
372
+ "off",
373
+ "auto",
374
+ "force"
375
+ ],
376
+ description: "OCR behavior. Default auto (OCR only when there is no text layer)."
377
+ },
378
+ ocr_out: {
379
+ type: "enum",
380
+ values: [
381
+ "txt",
382
+ "hocr",
383
+ "json"
384
+ ],
385
+ description: "OCR output structure when OCR runs. Default txt."
386
+ },
387
+ lang: {
388
+ type: "string-list",
389
+ description: "OCR language hint(s), e.g. lang=eng,deu. Quote tags with dashes."
390
+ }
391
+ },
392
+ appliesTo: [
393
+ "document",
394
+ "spreadsheet",
395
+ "presentation",
396
+ "image"
397
+ ],
398
+ output: "text"
399
+ },
400
+ {
401
+ id: "extract.metadata",
402
+ description: "Extract document metadata (author, dates, page count) as JSON.",
403
+ args: {},
404
+ appliesTo: [
405
+ "document",
406
+ "spreadsheet",
407
+ "presentation",
408
+ "image",
409
+ "audio"
410
+ ],
411
+ output: "json"
412
+ },
413
+ {
414
+ id: "extract.assets",
415
+ description: "Extract embedded assets (images, fonts, attachments) as separate media.",
416
+ args: {
417
+ types: {
418
+ type: "string-list",
419
+ values: [
420
+ "image",
421
+ "font",
422
+ "attachment",
423
+ "all"
424
+ ],
425
+ description: "Asset kinds to extract. Default all."
426
+ },
427
+ format: {
428
+ type: "enum",
429
+ values: IMAGE_FORMATS,
430
+ description: "Re-encode extracted images to this format. Default: native encoding as stored."
431
+ }
432
+ },
433
+ appliesTo: [
434
+ "document",
435
+ "presentation",
436
+ "spreadsheet"
437
+ ],
438
+ output: "media-list"
439
+ },
440
+ {
441
+ id: "chunk",
442
+ description: "Split extracted text into chunks for retrieval/indexing.",
443
+ args: {
444
+ by: {
445
+ type: "enum",
446
+ values: [
447
+ "sentence",
448
+ "paragraph",
449
+ "fixed"
450
+ ],
451
+ description: "Chunking strategy. Default paragraph."
452
+ },
453
+ size: {
454
+ type: "number",
455
+ min: 1,
456
+ description: "Chunk size for fixed strategy / max size otherwise."
457
+ },
458
+ overlap: {
459
+ type: "number",
460
+ min: 0,
461
+ description: "Overlap between consecutive chunks."
462
+ }
463
+ },
464
+ appliesTo: ["document", "any"],
465
+ output: "json"
466
+ },
467
+ {
468
+ id: "sheet.add_rows",
469
+ description: "Insert rows into a worksheet.",
470
+ args: {
471
+ sheet: {
472
+ type: "name-or-index",
473
+ description: "Target worksheet: bare number = 1-based index, quoted string = name."
474
+ },
475
+ rows: {
476
+ type: "json",
477
+ required: true,
478
+ description: "Rows as a JSON array of arrays of cell values, e.g. '[[\"a\",1],[\"b\",2]]'."
479
+ },
480
+ before: {
481
+ type: "number",
482
+ min: 1,
483
+ description: "Insert before this 1-based row."
484
+ },
485
+ after: {
486
+ type: "number",
487
+ min: 1,
488
+ description: "Insert after this 1-based row."
489
+ }
490
+ },
491
+ appliesTo: ["spreadsheet"],
492
+ output: "media"
493
+ },
494
+ {
495
+ id: "sheet.add_columns",
496
+ description: "Insert columns into a worksheet.",
497
+ args: {
498
+ sheet: {
499
+ type: "name-or-index",
500
+ description: "Target worksheet (index or quoted name)."
501
+ },
502
+ headers: {
503
+ type: "string-list",
504
+ description: "Header names for the new columns (values empty)."
505
+ },
506
+ columns: {
507
+ type: "json",
508
+ description: "Full column descriptors as JSON: '[{\"header\":\"X\",\"values\":[1,2]}]'."
509
+ },
510
+ before: {
511
+ type: "number",
512
+ min: 1,
513
+ description: "Insert before this 1-based column."
514
+ },
515
+ after: {
516
+ type: "number",
517
+ min: 1,
518
+ description: "Insert after this 1-based column."
519
+ }
520
+ },
521
+ appliesTo: ["spreadsheet"],
522
+ output: "media"
523
+ },
524
+ {
525
+ id: "sheet.update_cells",
526
+ description: "Update specific cells.",
527
+ args: {
528
+ sheet: {
529
+ type: "name-or-index",
530
+ description: "Target worksheet (index or quoted name)."
531
+ },
532
+ updates: {
533
+ type: "json",
534
+ required: true,
535
+ description: "JSON array of updates: '[{\"address\":\"B2\",\"value\":42}]' or '[{\"row\":2,\"col\":3,\"value\":\"x\"}]'."
536
+ }
537
+ },
538
+ appliesTo: ["spreadsheet"],
539
+ output: "media"
540
+ },
541
+ {
542
+ id: "sheet.delete_rows",
543
+ description: "Delete rows by 1-based index.",
544
+ args: {
545
+ sheet: {
546
+ type: "name-or-index",
547
+ description: "Target worksheet (index or quoted name)."
548
+ },
549
+ rows: {
550
+ type: "number-list",
551
+ required: true,
552
+ min: 1,
553
+ description: "1-based row indices to delete."
554
+ }
555
+ },
556
+ appliesTo: ["spreadsheet"],
557
+ output: "media"
558
+ },
559
+ {
560
+ id: "sheet.delete_columns",
561
+ description: "Delete columns by 1-based index.",
562
+ args: {
563
+ sheet: {
564
+ type: "name-or-index",
565
+ description: "Target worksheet (index or quoted name)."
566
+ },
567
+ columns: {
568
+ type: "number-list",
569
+ required: true,
570
+ min: 1,
571
+ description: "1-based column indices to delete."
572
+ }
573
+ },
574
+ appliesTo: ["spreadsheet"],
575
+ output: "media"
576
+ },
577
+ {
578
+ id: "sheet.rename_sheet",
579
+ description: "Rename a worksheet. The target must be a sheet NAME (quote it).",
580
+ args: {
581
+ sheet: {
582
+ type: "string",
583
+ required: true,
584
+ description: "Current sheet name (names only for rename; quote it)."
585
+ },
586
+ to: {
587
+ type: "string",
588
+ required: true,
589
+ description: "New sheet name."
590
+ }
591
+ },
592
+ appliesTo: ["spreadsheet"],
593
+ output: "media"
594
+ },
595
+ {
596
+ id: "sheet.add_sheet",
597
+ description: "Add a new worksheet.",
598
+ args: {
599
+ name: {
600
+ type: "string",
601
+ required: true,
602
+ description: "Name for the new worksheet."
603
+ },
604
+ at: {
605
+ type: "number",
606
+ min: 1,
607
+ description: "Insert at this 1-based position."
608
+ }
609
+ },
610
+ appliesTo: ["spreadsheet"],
611
+ output: "media"
612
+ },
613
+ {
614
+ id: "sheet.remove_sheet",
615
+ description: "Remove a worksheet. The target must be a sheet NAME (quote it).",
616
+ args: { sheet: {
617
+ type: "string",
618
+ required: true,
619
+ description: "Sheet name to remove (names only; quote it)."
620
+ } },
621
+ appliesTo: ["spreadsheet"],
622
+ output: "media"
623
+ },
624
+ {
625
+ id: "sheet.reorder_sheets",
626
+ description: "Reorder worksheets.",
627
+ args: { order: {
628
+ type: "json",
629
+ required: true,
630
+ description: "JSON array of sheet names and/or 1-based indices in the new order: '[\"Summary\",2,3]'."
631
+ } },
632
+ appliesTo: ["spreadsheet"],
633
+ output: "media"
634
+ },
635
+ {
636
+ id: "sheet.transform_table",
637
+ description: "Rename/select/drop table columns by header name.",
638
+ args: {
639
+ sheet: {
640
+ type: "name-or-index",
641
+ description: "Target worksheet (index or quoted name)."
642
+ },
643
+ header_row: {
644
+ type: "number",
645
+ min: 1,
646
+ description: "1-based header row. Default 1."
647
+ },
648
+ select: {
649
+ type: "string-list",
650
+ description: "Column headers to keep."
651
+ },
652
+ drop: {
653
+ type: "string-list",
654
+ description: "Column headers to drop."
655
+ },
656
+ rename: {
657
+ type: "json",
658
+ description: "JSON array of renames: '[{\"from\":\"Old\",\"to\":\"New\"}]'."
659
+ }
660
+ },
661
+ appliesTo: ["spreadsheet"],
662
+ output: "media"
663
+ },
664
+ {
665
+ id: "slides.add",
666
+ description: "Add a new slide.",
667
+ args: {
668
+ at: {
669
+ type: "number",
670
+ min: 1,
671
+ description: "Insert at this 1-based position."
672
+ },
673
+ title: {
674
+ type: "string",
675
+ description: "Title text for the new slide."
676
+ },
677
+ layout: {
678
+ type: "string",
679
+ description: "Layout name (template-dependent)."
680
+ }
681
+ },
682
+ appliesTo: ["presentation"],
683
+ output: "media"
684
+ },
685
+ {
686
+ id: "slides.update_text",
687
+ description: "Update text on a slide.",
688
+ args: {
689
+ slide: {
690
+ type: "name-or-index",
691
+ description: "Target slide: bare number = 1-based index, quoted string = title."
692
+ },
693
+ placeholder: {
694
+ type: "string",
695
+ description: "Placeholder/shape to target."
696
+ },
697
+ text: {
698
+ type: "string",
699
+ required: true,
700
+ description: "Replacement text."
701
+ }
702
+ },
703
+ appliesTo: ["presentation"],
704
+ output: "media"
705
+ },
706
+ {
707
+ id: "slides.update_table",
708
+ description: "Update table cells on a slide.",
709
+ args: {
710
+ slide: {
711
+ type: "name-or-index",
712
+ description: "Target slide (index or quoted title)."
713
+ },
714
+ updates: {
715
+ type: "json",
716
+ required: true,
717
+ description: "JSON array: '[{\"row\":1,\"col\":2,\"value\":\"x\"}]' (1-based)."
718
+ }
719
+ },
720
+ appliesTo: ["presentation"],
721
+ output: "media"
722
+ },
723
+ {
724
+ id: "slides.update_image",
725
+ description: "Replace an image on a slide with another media.",
726
+ args: {
727
+ slide: {
728
+ type: "name-or-index",
729
+ description: "Target slide (index or quoted title)."
730
+ },
731
+ placeholder: {
732
+ type: "string",
733
+ description: "Placeholder/shape to target."
734
+ },
735
+ with: {
736
+ type: "media-ref",
737
+ required: true,
738
+ description: "The replacement image, as an @id ref."
739
+ }
740
+ },
741
+ appliesTo: ["presentation"],
742
+ output: "media"
743
+ },
744
+ {
745
+ id: "slides.update_chart",
746
+ description: "Update chart data on a slide.",
747
+ args: {
748
+ slide: {
749
+ type: "name-or-index",
750
+ description: "Target slide (index or quoted title)."
751
+ },
752
+ data: {
753
+ type: "json",
754
+ description: "JSON array-of-arrays of chart data: '[[\"Q1\",10],[\"Q2\",20]]'."
755
+ }
756
+ },
757
+ appliesTo: ["presentation"],
758
+ output: "media"
759
+ },
760
+ {
761
+ id: "slides.delete",
762
+ description: "Delete slides by 1-based index.",
763
+ args: { slides: {
764
+ type: "number-list",
765
+ required: true,
766
+ min: 1,
767
+ description: "1-based slide indices to delete."
768
+ } },
769
+ appliesTo: ["presentation"],
770
+ output: "media"
771
+ },
772
+ {
773
+ id: "slides.reorder",
774
+ description: "Reorder slides.",
775
+ args: { order: {
776
+ type: "number-list",
777
+ required: true,
778
+ min: 1,
779
+ description: "New order of existing 1-based slide indices."
780
+ } },
781
+ appliesTo: ["presentation"],
782
+ output: "media"
783
+ },
784
+ {
785
+ id: "slides.duplicate",
786
+ description: "Duplicate a slide.",
787
+ args: {
788
+ slide: {
789
+ type: "number",
790
+ required: true,
791
+ min: 1,
792
+ description: "1-based slide to copy."
793
+ },
794
+ at: {
795
+ type: "number",
796
+ min: 1,
797
+ description: "Insert the copy at this position."
798
+ }
799
+ },
800
+ appliesTo: ["presentation"],
801
+ output: "media"
802
+ },
803
+ {
804
+ id: "image.resize",
805
+ description: "Resize the image.",
806
+ args: {
807
+ width: {
808
+ type: "number",
809
+ min: 1,
810
+ max: 16384,
811
+ description: "Target width in px."
812
+ },
813
+ height: {
814
+ type: "number",
815
+ min: 1,
816
+ max: 16384,
817
+ description: "Target height in px."
818
+ },
819
+ fit: {
820
+ type: "enum",
821
+ values: [
822
+ "cover",
823
+ "contain",
824
+ "fill",
825
+ "inside",
826
+ "outside"
827
+ ],
828
+ description: "Resize fit mode. Default cover."
829
+ }
830
+ },
831
+ requires: { capability: "mutate" },
832
+ appliesTo: ["image"],
833
+ output: "media"
834
+ },
835
+ {
836
+ id: "image.format",
837
+ description: "Re-encode the image to another format.",
838
+ args: {
839
+ to: {
840
+ type: "enum",
841
+ required: true,
842
+ values: IMAGE_FORMATS,
843
+ description: "Target image format."
844
+ },
845
+ quality: {
846
+ type: "number",
847
+ min: 1,
848
+ max: 100,
849
+ description: "Quality for lossy formats."
850
+ },
851
+ strip_metadata: {
852
+ type: "boolean",
853
+ description: "Remove EXIF/ICC metadata from the output."
854
+ }
855
+ },
856
+ requires: { capability: "mutate" },
857
+ appliesTo: ["image"],
858
+ output: "media"
859
+ },
860
+ {
861
+ id: "image.rotate",
862
+ description: "Rotate the image.",
863
+ args: { deg: {
864
+ type: "enum",
865
+ required: true,
866
+ values: [
867
+ "90",
868
+ "180",
869
+ "270"
870
+ ],
871
+ description: "Rotation in degrees (clockwise)."
872
+ } },
873
+ requires: { capability: "mutate" },
874
+ appliesTo: ["image"],
875
+ output: "media"
876
+ },
877
+ {
878
+ id: "image.flip",
879
+ description: "Flip the image.",
880
+ args: { axis: {
881
+ type: "enum",
882
+ required: true,
883
+ values: [
884
+ "horizontal",
885
+ "vertical",
886
+ "both"
887
+ ],
888
+ description: "Flip axis."
889
+ } },
890
+ requires: { capability: "mutate" },
891
+ appliesTo: ["image"],
892
+ output: "media"
893
+ },
894
+ {
895
+ id: "image.strip_metadata",
896
+ description: "Remove EXIF/ICC metadata from the image without other changes.",
897
+ args: {},
898
+ requires: { capability: "mutate" },
899
+ appliesTo: ["image"],
900
+ output: "media"
901
+ },
902
+ {
903
+ id: "audio.transcribe",
904
+ description: "Transcribe speech to text.",
905
+ args: {
906
+ lang: {
907
+ type: "string",
908
+ description: "Language hint, e.g. lang=en. Quote tags with dashes: lang=\"en-US\"."
909
+ },
910
+ out: {
911
+ type: "enum",
912
+ values: [
913
+ "txt",
914
+ "srt",
915
+ "vtt",
916
+ "json"
917
+ ],
918
+ description: "Output format. Default txt. srt/vtt produce subtitles."
919
+ },
920
+ translate: {
921
+ type: "boolean",
922
+ description: "Translate the transcription to English."
923
+ }
924
+ },
925
+ requires: {
926
+ capability: "convert",
927
+ from: PCM_MIME
928
+ },
929
+ appliesTo: ["audio"],
930
+ output: "text"
931
+ }
932
+ ];
933
+ /** Map of canonical verb id → spec, for direct lookup. */
934
+ var VERB_INDEX = new Map(VERBS.map((v) => [v.id, v]));
935
+ /**
936
+ * Fold a verb token sequence to canonical form: lowercase, separators (space/`_`/`.`)
937
+ * normalized so `extract_text` ≡ `extract text` ≡ `extract.text` all match `extract.text`.
938
+ *
939
+ * @param words - The verb word tokens as written (1 or 2 words, possibly containing `_`/`.`).
940
+ * @returns The canonical verb id when a fold-match exists, otherwise `undefined`.
941
+ */
942
+ var foldVerb = (words) => {
943
+ const flat = words.join(" ").toLowerCase().replace(/[._\s]+/g, " ").trim();
944
+ return FOLDED_INDEX.get(flat);
945
+ };
946
+ /** Internal: folded "word word" form → canonical id. */
947
+ var FOLDED_INDEX = (() => {
948
+ const map = /* @__PURE__ */ new Map();
949
+ for (const v of VERBS) {
950
+ const folded = v.id.replace(/[._]+/g, " ");
951
+ if (map.has(folded)) throw new Error(`verb table invariant violated: "${folded}" folds to multiple verbs`);
952
+ map.set(folded, v.id);
953
+ }
954
+ return map;
955
+ })();
956
+ /**
957
+ * All folded verb word-sequences, for the parser's longest-match verb recognition and for
958
+ * generated grammar text.
959
+ */
960
+ var FOLDED_VERBS = Array.from(FOLDED_INDEX.keys());
961
+ /**
962
+ * Suggest the nearest verbs to an unknown input, for did-you-mean errors. Matches whole folded
963
+ * forms AND suffix words (`resize` suggests `image resize`), per the frozen error model.
964
+ *
965
+ * @param input - The unknown verb text as written.
966
+ * @param candidates - The folded verb forms to search (pass the narrowed set to avoid
967
+ * suggesting unconfigured verbs).
968
+ * @returns Up to three suggestions, best first.
969
+ */
970
+ var suggestVerbs = (input, candidates) => {
971
+ const folded = input.toLowerCase().replace(/[._\s]+/g, " ").trim();
972
+ const scored = [];
973
+ for (const cand of candidates) {
974
+ const direct = levenshtein(folded, cand);
975
+ const words = cand.split(" ");
976
+ const suffix = words.length > 1 ? levenshtein(folded, words[words.length - 1]) : Infinity;
977
+ const score = Math.min(direct, suffix);
978
+ if (score <= Math.max(2, Math.floor(folded.length / 3))) scored.push({
979
+ name: cand,
980
+ score
981
+ });
982
+ }
983
+ scored.sort((a, b) => a.score - b.score);
984
+ return scored.slice(0, 3).map((s) => s.name);
985
+ };
986
+ /** Classic two-row Levenshtein distance. */
987
+ var levenshtein = (a, b) => {
988
+ if (a === b) return 0;
989
+ if (a.length === 0) return b.length;
990
+ if (b.length === 0) return a.length;
991
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
992
+ let curr = new Array(b.length + 1);
993
+ for (let i = 1; i <= a.length; i++) {
994
+ curr[0] = i;
995
+ for (let j = 1; j <= b.length; j++) {
996
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
997
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
998
+ }
999
+ [prev, curr] = [curr, prev];
1000
+ }
1001
+ return prev[b.length];
1002
+ };
1003
+ //#endregion
1004
+ //#region src/batteries/media/validate.ts
1005
+ /**
1006
+ * The semantic validator: checks raw parsed segments (or ops) against the verb table and the
1007
+ * deployment's configured engines, producing the validated {@link MediaPlan} or a
1008
+ * model-actionable error.
1009
+ *
1010
+ * @remarks
1011
+ * Internal sibling of the `@nhtio/adk/batteries/media` entry. This is the layer no parser
1012
+ * toolkit could supply — every error names what was wrong, suggests the nearest valid form,
1013
+ * and shows a corrective exemplar (frozen design 0.14). Engine narrowing happens HERE, never
1014
+ * at parse time (frozen 0.3): the same string parses identically in every deployment; only
1015
+ * validation differs.
1016
+ */
1017
+ /** `true` when the deployment satisfies a verb's capability requirement. */
1018
+ var satisfies = (capabilities, requires) => requires.capability === "mutate" ? capabilities.hasMutate() : capabilities.hasConvert(requires.from, requires.to);
1019
+ /** A human phrase for an unmet requirement, for the do-not-retry error. */
1020
+ var requirementText = (requires) => {
1021
+ if (requires.capability === "mutate") return "an engine that can mutate this media";
1022
+ return `an engine that can convert${requires.from ? ` from ${requires.from}` : ""}${requires.to ? ` to ${requires.to}` : ""}`;
1023
+ };
1024
+ /** The folded verb forms available under a given capability configuration. */
1025
+ var availableVerbs = (capabilities) => {
1026
+ const out = [];
1027
+ for (const [id, spec] of VERB_INDEX) {
1028
+ if (spec.requires && !satisfies(capabilities, spec.requires)) continue;
1029
+ out.push(id.replace(/[._]+/g, " "));
1030
+ }
1031
+ return out;
1032
+ };
1033
+ var levSuggest = (verbText, capabilities) => {
1034
+ const suggestions = suggestVerbs(verbText, availableVerbs(capabilities));
1035
+ return suggestions.length > 0 ? ` did you mean "${suggestions[0]}"?` : "";
1036
+ };
1037
+ var verbList = (capabilities) => availableVerbs(capabilities).join(", ");
1038
+ var resolveVerb = (verbText, capabilities, position) => {
1039
+ const canonical = foldVerb(verbText.split(" ")) ?? verbText;
1040
+ const spec = VERB_INDEX.get(canonical);
1041
+ if (!spec) throw new E_MEDIA_UNKNOWN_VERB([`unknown verb "${verbText}" at segment ${position}.${levSuggest(verbText, capabilities)} Available verbs: ${verbList(capabilities)}`]);
1042
+ if (spec.requires && !satisfies(capabilities, spec.requires)) throw new E_MEDIA_ENGINE_REQUIRED([`verb "${verbText}" requires ${requirementText(spec.requires)}, and none is configured in this deployment. Do not retry this verb here. Available verbs: ${verbList(capabilities)}`]);
1043
+ return spec;
1044
+ };
1045
+ var suggestArg = (name, spec) => {
1046
+ const ranked = suggestVerbs(name, Object.keys(spec.args));
1047
+ return ranked.length > 0 ? ` did you mean "${ranked[0]}"?` : "";
1048
+ };
1049
+ var argList = (spec) => {
1050
+ const names = Object.keys(spec.args);
1051
+ return names.length > 0 ? names.join(", ") : "(none)";
1052
+ };
1053
+ var exemplar = (spec) => {
1054
+ const parts = [spec.id.replace(/\./g, " ")];
1055
+ for (const [name, arg] of Object.entries(spec.args)) {
1056
+ if (!arg.required) continue;
1057
+ parts.push(`${name}=${exampleValue(arg)}`);
1058
+ }
1059
+ return parts.join(" ");
1060
+ };
1061
+ var exampleValue = (arg) => {
1062
+ switch (arg.type) {
1063
+ case "enum": return arg.values?.[0] ?? "value";
1064
+ case "number": return String(arg.min ?? 1);
1065
+ case "number-list": return "1-3,5";
1066
+ case "string-list": return "a,b";
1067
+ case "boolean": return "true";
1068
+ case "regex-or-string-list": return "\"literal text\"";
1069
+ case "media-ref": return "@<media id>";
1070
+ case "media-ref-list": return "@<media id>";
1071
+ case "json": return `'[…]'`;
1072
+ default: return "\"value\"";
1073
+ }
1074
+ };
1075
+ /** Coerce + check one arg value against its spec. Returns the IR-final value. */
1076
+ var checkArg = (verb, name, raw) => {
1077
+ const spec = verb.args[name];
1078
+ const where = `arg "${name}" on "${verb.id.replace(/\./g, " ")}"`;
1079
+ const bad = (msg) => {
1080
+ throw new E_MEDIA_BAD_ARG([`${where}: ${msg}. Write it like: ${exemplar(verb)}`]);
1081
+ };
1082
+ const v = raw.value;
1083
+ switch (spec.type) {
1084
+ case "name-or-index":
1085
+ if (typeof v === "number") {
1086
+ if (!Number.isInteger(v) || v < 1) bad("indices are 1-based integers");
1087
+ return v;
1088
+ }
1089
+ if (typeof v !== "string") bad("expected a 1-based index or a quoted name");
1090
+ return v;
1091
+ case "string":
1092
+ if (typeof v === "number" && !raw.quoted) return String(v);
1093
+ if (typeof v !== "string") bad(`expected text${typeof v === "object" ? ", got a structured value" : ""}`);
1094
+ return v;
1095
+ case "number": {
1096
+ if (typeof v !== "number") bad("expected a number");
1097
+ const n = v;
1098
+ if (spec.min !== void 0 && n < spec.min) bad(`must be ≥ ${spec.min} (indices are 1-based)`);
1099
+ if (spec.max !== void 0 && n > spec.max) bad(`must be ≤ ${spec.max}`);
1100
+ return n;
1101
+ }
1102
+ case "boolean":
1103
+ if (typeof v !== "boolean") bad("expected true or false");
1104
+ return v;
1105
+ case "enum": {
1106
+ const s = typeof v === "number" ? String(v) : v;
1107
+ if (typeof s !== "string" || !spec.values?.includes(s)) bad(`"${String(v)}" is not valid; valid values: ${spec.values?.join(", ")}`);
1108
+ return s;
1109
+ }
1110
+ case "number-list": {
1111
+ const arr = Array.isArray(v) ? v : [v];
1112
+ if (!arr.every((x) => typeof x === "number")) bad("expected numbers (e.g. 1-3,5)");
1113
+ const nums = arr;
1114
+ if (spec.min !== void 0 && nums.some((n) => n < spec.min)) bad(`values must be ≥ ${spec.min} (indices are 1-based)`);
1115
+ return nums;
1116
+ }
1117
+ case "string-list": {
1118
+ const arr = Array.isArray(v) ? v : [v];
1119
+ if (!arr.every((x) => typeof x === "string")) bad("expected names (quote values with special characters)");
1120
+ const strs = arr;
1121
+ if (spec.values && !strs.every((s) => spec.values?.includes(s))) bad(`valid values: ${spec.values.join(", ")}`);
1122
+ return strs;
1123
+ }
1124
+ case "regex-or-string-list": {
1125
+ if (isRegExpRef(v)) return v;
1126
+ const arr = Array.isArray(v) ? v : [v];
1127
+ if (arr.every((x) => typeof x === "string" || isRegExpRef(x))) return arr;
1128
+ bad("expected literal text, a list of literals, or a /regex/");
1129
+ break;
1130
+ }
1131
+ case "media-ref":
1132
+ if (isMediaRef(v)) return v;
1133
+ bad("expected a media reference: with=@<media id> (get ids from list_media)");
1134
+ break;
1135
+ case "media-ref-list": {
1136
+ const arr = Array.isArray(v) ? v : [v];
1137
+ if (arr.every(isMediaRef)) return arr;
1138
+ bad("expected media references: with=@<id>,@<id> (get ids from list_media)");
1139
+ break;
1140
+ }
1141
+ case "json":
1142
+ if (typeof v === "string") {
1143
+ if (!raw.quoted) bad(`expected a quoted JSON value, e.g. ${name}='[…]'`);
1144
+ try {
1145
+ return JSON.parse(v);
1146
+ } catch (err) {
1147
+ bad(`the quoted value is not valid JSON (${isError(err) ? err.message : String(err)})`);
1148
+ }
1149
+ }
1150
+ if (isObject(v) || Array.isArray(v)) return v;
1151
+ bad(`expected a JSON value, e.g. ${name}='[…]'`);
1152
+ break;
1153
+ }
1154
+ throw new E_MEDIA_BAD_ARG([`${where}: invalid value`]);
1155
+ };
1156
+ var checkStep = (spec, args) => {
1157
+ const verbText = spec.id.replace(/\./g, " ");
1158
+ const finalArgs = {};
1159
+ for (const [name, raw] of args) {
1160
+ if (!(name in spec.args)) throw new E_MEDIA_UNKNOWN_ARG([`verb "${verbText}" has no arg "${name}".${suggestArg(name, spec)} Args: ${argList(spec)}. Write it like: ${exemplar(spec)}`]);
1161
+ finalArgs[name] = checkArg(spec, name, raw);
1162
+ }
1163
+ for (const [name, argSpec] of Object.entries(spec.args)) if (argSpec.required && !(name in finalArgs)) throw new E_MEDIA_MISSING_ARG([`verb "${verbText}" requires arg "${name}" (${argSpec.description}). Write it like: ${exemplar(spec)}`]);
1164
+ return {
1165
+ verb: spec.id,
1166
+ args: finalArgs
1167
+ };
1168
+ };
1169
+ /**
1170
+ * Validate raw pipe segments into a {@link MediaPlan}.
1171
+ *
1172
+ * @param segments - Output of `parsePipeRaw`.
1173
+ * @param options - The deployment's capability probe.
1174
+ * @returns The validated plan, spans preserved.
1175
+ */
1176
+ var validateSegments = (segments, options) => {
1177
+ const steps = [];
1178
+ segments.forEach((seg, i) => {
1179
+ const step = checkStep(resolveVerb(seg.verb.replace(/\./g, " "), options.capabilities, i + 1), seg.args);
1180
+ step.span = seg.span;
1181
+ steps.push(step);
1182
+ });
1183
+ return { steps };
1184
+ };
1185
+ /**
1186
+ * Validate a JSON ops array into a {@link MediaPlan}. The same checks as the pipe path —
1187
+ * verbs fold the same way, args validate against the same specs — so the two front-ends
1188
+ * produce identical plans for equivalent statements.
1189
+ *
1190
+ * @param ops - The ops array.
1191
+ * @param options - The deployment's capability probe.
1192
+ * @returns The validated plan (no spans).
1193
+ */
1194
+ var validateOps = (ops, options) => {
1195
+ const steps = [];
1196
+ ops.forEach((op, i) => {
1197
+ const spec = resolveVerb(op.verb.replace(/[._]+/g, " "), options.capabilities, i + 1);
1198
+ const args = new Map(Object.entries(op.args).map(([k, v]) => [k, {
1199
+ value: v,
1200
+ quoted: typeof v === "string",
1201
+ span: {
1202
+ offset: 0,
1203
+ line: 1,
1204
+ col: 1,
1205
+ length: 0
1206
+ }
1207
+ }]));
1208
+ steps.push(checkStep(spec, args));
1209
+ });
1210
+ return { steps };
1211
+ };
1212
+ //#endregion
1213
+ export { VERBS as a, suggestVerbs as c, isMediaRef as d, isRegExpRef as f, FOLDED_VERBS as i, canonicalFlags as l, toPipe as m, validateOps as n, VERB_INDEX as o, toOps as p, validateSegments as r, foldVerb as s, availableVerbs as t, fromOps as u };
1214
+
1215
+ //# sourceMappingURL=validate-DSZ3wicB.mjs.map