scoutline 0.16.0 → 0.17.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 (58) hide show
  1. package/README.md +47 -0
  2. package/dist/command-invocation.d.ts +2 -2
  3. package/dist/command-invocation.d.ts.map +1 -1
  4. package/dist/command-invocation.js +1 -1
  5. package/dist/command-invocation.js.map +1 -1
  6. package/dist/commands/research.d.ts +45 -0
  7. package/dist/commands/research.d.ts.map +1 -1
  8. package/dist/commands/research.js +152 -4
  9. package/dist/commands/research.js.map +1 -1
  10. package/dist/commands/search.d.ts +11 -0
  11. package/dist/commands/search.d.ts.map +1 -1
  12. package/dist/commands/search.js +31 -2
  13. package/dist/commands/search.js.map +1 -1
  14. package/dist/commands/usage.d.ts +101 -0
  15. package/dist/commands/usage.d.ts.map +1 -0
  16. package/dist/commands/usage.js +228 -0
  17. package/dist/commands/usage.js.map +1 -0
  18. package/dist/commands/vision.d.ts +28 -0
  19. package/dist/commands/vision.d.ts.map +1 -1
  20. package/dist/commands/vision.js +452 -0
  21. package/dist/commands/vision.js.map +1 -1
  22. package/dist/index.d.ts +25 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +572 -10
  25. package/dist/index.js.map +1 -1
  26. package/dist/lib/batch-assign.d.ts +61 -0
  27. package/dist/lib/batch-assign.d.ts.map +1 -0
  28. package/dist/lib/batch-assign.js +118 -0
  29. package/dist/lib/batch-assign.js.map +1 -0
  30. package/dist/lib/batch-manifest.d.ts +183 -0
  31. package/dist/lib/batch-manifest.d.ts.map +1 -0
  32. package/dist/lib/batch-manifest.js +644 -0
  33. package/dist/lib/batch-manifest.js.map +1 -0
  34. package/dist/lib/batch-runner.d.ts +169 -0
  35. package/dist/lib/batch-runner.d.ts.map +1 -0
  36. package/dist/lib/batch-runner.js +357 -0
  37. package/dist/lib/batch-runner.js.map +1 -0
  38. package/dist/lib/consumption.d.ts +17 -0
  39. package/dist/lib/consumption.d.ts.map +1 -1
  40. package/dist/lib/consumption.js +38 -0
  41. package/dist/lib/consumption.js.map +1 -1
  42. package/dist/lib/context-file.d.ts +90 -0
  43. package/dist/lib/context-file.d.ts.map +1 -0
  44. package/dist/lib/context-file.js +244 -0
  45. package/dist/lib/context-file.js.map +1 -0
  46. package/dist/lib/trigger-detection.d.ts +21 -0
  47. package/dist/lib/trigger-detection.d.ts.map +1 -1
  48. package/dist/lib/trigger-detection.js +63 -0
  49. package/dist/lib/trigger-detection.js.map +1 -1
  50. package/dist/lib/usage-ledger.d.ts +221 -0
  51. package/dist/lib/usage-ledger.d.ts.map +1 -0
  52. package/dist/lib/usage-ledger.js +408 -0
  53. package/dist/lib/usage-ledger.js.map +1 -0
  54. package/dist/node-command-invocation-adapter.d.ts +25 -0
  55. package/dist/node-command-invocation-adapter.d.ts.map +1 -1
  56. package/dist/node-command-invocation-adapter.js +73 -38
  57. package/dist/node-command-invocation-adapter.js.map +1 -1
  58. package/package.json +1 -1
@@ -0,0 +1,644 @@
1
+ /**
2
+ * Batch Manifest module (batch-runner DESIGN D2, D2.8, D3).
3
+ *
4
+ * Strict parse of the manifest schema v1 plus the deterministic
5
+ * `input → argv` compiler. The manifest carries STRUCTURED, typed values
6
+ * (numbers as numbers, booleans as booleans, enums as enum members);
7
+ * the compiler renders them to the exact argv the command handlers'
8
+ * `parseArgs` consumes, which stays the single semantic validation
9
+ * authority. Manifest parse checks shape only: required fields, types,
10
+ * enum membership, subcommand scoping.
11
+ *
12
+ * Strict parse throughout: unknown fields reject at every level (top
13
+ * level, op level, inside `input`) naming the first offender, and old
14
+ * manifests either parse cleanly or reject — never silently reinterpreted.
15
+ *
16
+ * This module performs no I/O of its own: provider capability checks run
17
+ * against injected descriptors (never `descriptor.create()`), and output
18
+ * dirname existence runs against an injected `dirExists` probe.
19
+ */
20
+ import * as path from "node:path";
21
+ import { ValidationError } from "./errors.js";
22
+ import { PROVIDER_IDS } from "../providers/types.js";
23
+ import { visionOperationToCapability } from "../capabilities/vision.js";
24
+ // ---------------------------------------------------------------------------
25
+ // D3 — capability-operation allowlist
26
+ // ---------------------------------------------------------------------------
27
+ /**
28
+ * Commands a batch manifest may carry (owner decision 2026-08-17):
29
+ * capability operations only. The array order fixes the rejection
30
+ * message wording and is part of the pinned contract.
31
+ */
32
+ export const BATCH_ALLOWED_COMMANDS = [
33
+ "search",
34
+ "read",
35
+ "research",
36
+ "repo",
37
+ "vision",
38
+ "crawl",
39
+ "map",
40
+ ];
41
+ /** Rejection message for out-of-allowlist commands (D3, verbatim). */
42
+ export const BATCH_ALLOWLIST_MESSAGE = `batch accepts capability operations only (${BATCH_ALLOWED_COMMANDS.join(", ")})`;
43
+ /**
44
+ * Mirror of `visionOperationForCommand` (module-private in `index.ts`,
45
+ * deliberately not exported from there). Kept as an explicit local switch
46
+ * so the footprint stays two regions, chained through the exported
47
+ * `visionOperationToCapability` single source of truth for ids.
48
+ */
49
+ function visionSubcommandToOperation(subcommand) {
50
+ switch (subcommand) {
51
+ case "analyze":
52
+ return "interpret-image";
53
+ case "ui-to-code":
54
+ return "ui-artifact";
55
+ case "extract-text":
56
+ return "extract-text";
57
+ case "diagnose-error":
58
+ return "diagnose-error";
59
+ case "diagram":
60
+ return "diagram";
61
+ case "chart":
62
+ return "chart";
63
+ case "diff":
64
+ return "diff";
65
+ case "video":
66
+ return "video";
67
+ default:
68
+ throw new ValidationError(`Unknown vision command: ${subcommand}`);
69
+ }
70
+ }
71
+ /**
72
+ * Capability id a batch op exercises (DESIGN D4). Vision ops resolve to
73
+ * their per-operation `vision.<operation>` id; this is the grouping key
74
+ * provider assignment uses and the id the per-op pin is validated
75
+ * against.
76
+ */
77
+ export function batchCommandCapabilityId(command, input) {
78
+ switch (command) {
79
+ case "search":
80
+ return "search";
81
+ case "read":
82
+ return "reader";
83
+ case "crawl":
84
+ return "crawl";
85
+ case "map":
86
+ return "map";
87
+ case "research":
88
+ return "research";
89
+ case "repo":
90
+ return "repository-exploration";
91
+ case "vision":
92
+ return visionOperationToCapability(visionSubcommandToOperation(String(input.subcommand)));
93
+ }
94
+ }
95
+ const RECENCY_VALUES = ["oneDay", "oneWeek", "oneMonth", "oneYear", "noLimit"];
96
+ const CONTENT_SIZE_VALUES = ["medium", "high"];
97
+ const LOCATION_VALUES = ["cn", "us"];
98
+ const TOPIC_VALUES = ["general", "news", "finance"];
99
+ const SEARCH_TYPE_VALUES = ["video"];
100
+ const FORMAT_VALUES = ["markdown", "text"];
101
+ const RESEARCH_MODEL_VALUES = ["mini", "pro", "auto"];
102
+ const OUTPUT_LENGTH_VALUES = ["short", "standard", "long"];
103
+ const CITATION_FORMAT_VALUES = ["numbered", "mla", "apa", "chicago"];
104
+ const REPO_LANGUAGE_VALUES = ["en", "zh"];
105
+ const REPO_SUBCOMMAND_VALUES = ["search", "tree", "read", "brief"];
106
+ const VISION_SUBCOMMAND_VALUES = [
107
+ "analyze",
108
+ "ui-to-code",
109
+ "extract-text",
110
+ "diagnose-error",
111
+ "diagram",
112
+ "chart",
113
+ "diff",
114
+ "video",
115
+ ];
116
+ const VISION_OUTPUT_VALUES = ["code", "prompt", "spec", "description"];
117
+ const VISION_NON_DIFF_SUBCOMMANDS = [
118
+ "analyze",
119
+ "ui-to-code",
120
+ "extract-text",
121
+ "diagnose-error",
122
+ "diagram",
123
+ "chart",
124
+ "video",
125
+ ];
126
+ /**
127
+ * Field tables in canonical order. The declaration order is the compile
128
+ * order for flags (D2.8) and the validation order within an input
129
+ * object, making both deterministic. `subcommand` is first for `repo`
130
+ * and `vision` so scoped rules always have a validated value.
131
+ */
132
+ const INPUT_FIELD_TABLES = {
133
+ search: [
134
+ ["query", { kind: "string", required: true }],
135
+ ["count", { kind: "number" }],
136
+ ["domain", { kind: "string" }],
137
+ ["recency", { kind: "string", enumValues: RECENCY_VALUES }],
138
+ ["contentSize", { kind: "string", enumValues: CONTENT_SIZE_VALUES }],
139
+ ["location", { kind: "string", enumValues: LOCATION_VALUES }],
140
+ ["topic", { kind: "string", enumValues: TOPIC_VALUES }],
141
+ ["type", { kind: "string", enumValues: SEARCH_TYPE_VALUES }],
142
+ ["maxSummary", { kind: "number" }],
143
+ ["fields", { kind: "stringArray" }],
144
+ ["noCache", { kind: "boolean" }],
145
+ ["merge", { kind: "boolean" }],
146
+ ],
147
+ read: [
148
+ ["url", { kind: "string", required: true }],
149
+ ["format", { kind: "string", enumValues: FORMAT_VALUES }],
150
+ ["noImages", { kind: "boolean" }],
151
+ ["withLinks", { kind: "boolean" }],
152
+ ["withImagesSummary", { kind: "boolean" }],
153
+ ["noCache", { kind: "boolean" }],
154
+ ],
155
+ crawl: [
156
+ ["url", { kind: "string", required: true }],
157
+ ["depth", { kind: "number" }],
158
+ ["breadth", { kind: "number" }],
159
+ ["limit", { kind: "number" }],
160
+ ["selectPaths", { kind: "string" }],
161
+ ["excludePaths", { kind: "string" }],
162
+ ["instructions", { kind: "string" }],
163
+ ["format", { kind: "string", enumValues: FORMAT_VALUES }],
164
+ ["contentSize", { kind: "string", enumValues: CONTENT_SIZE_VALUES }],
165
+ ["timeout", { kind: "number" }],
166
+ ["maxChars", { kind: "number" }],
167
+ ["noCache", { kind: "boolean" }],
168
+ ],
169
+ map: [
170
+ ["url", { kind: "string", required: true }],
171
+ ["depth", { kind: "number" }],
172
+ ["breadth", { kind: "number" }],
173
+ ["limit", { kind: "number" }],
174
+ ["selectPaths", { kind: "string" }],
175
+ ["excludePaths", { kind: "string" }],
176
+ ["instructions", { kind: "string" }],
177
+ ["noCache", { kind: "boolean" }],
178
+ ],
179
+ research: [
180
+ ["query", { kind: "string", required: true }],
181
+ ["model", { kind: "string", enumValues: RESEARCH_MODEL_VALUES }],
182
+ ["outputLength", { kind: "string", enumValues: OUTPUT_LENGTH_VALUES }],
183
+ ["citationFormat", { kind: "string", enumValues: CITATION_FORMAT_VALUES }],
184
+ ["domain", { kind: "string" }],
185
+ ["maxChars", { kind: "number" }],
186
+ ["timeout", { kind: "number" }],
187
+ ["noCache", { kind: "boolean" }],
188
+ ],
189
+ repo: [
190
+ ["subcommand", { kind: "string", enumValues: REPO_SUBCOMMAND_VALUES, required: true }],
191
+ ["repository", { kind: "string", required: true }],
192
+ ["query", { kind: "string", requiredFor: ["search"], allowedFor: ["search"] }],
193
+ [
194
+ "path",
195
+ { kind: "string", requiredFor: ["read"], allowedFor: ["tree", "read", "brief"] },
196
+ ],
197
+ // Scoped to the subcommands whose handlers consume the compiled
198
+ // flag: the repo handler threads `language` only into `search`, and
199
+ // `maxChars` into `search`/`read`/`brief` (`tree` ignores both) —
200
+ // a manifest must never request an option the handler silently
201
+ // discards.
202
+ ["language", { kind: "string", enumValues: REPO_LANGUAGE_VALUES, allowedFor: ["search"] }],
203
+ ["maxChars", { kind: "number", allowedFor: ["search", "read", "brief"] }],
204
+ ["focus", { kind: "string", allowedFor: ["brief"] }],
205
+ ["depth", { kind: "number", allowedFor: ["tree", "brief"] }],
206
+ ["noCache", { kind: "boolean" }],
207
+ ],
208
+ vision: [
209
+ ["subcommand", { kind: "string", enumValues: VISION_SUBCOMMAND_VALUES, required: true }],
210
+ [
211
+ "source",
212
+ { kind: "string", requiredFor: VISION_NON_DIFF_SUBCOMMANDS, allowedFor: VISION_NON_DIFF_SUBCOMMANDS },
213
+ ],
214
+ ["prompt", { kind: "string" }],
215
+ ["language", { kind: "string", allowedFor: ["extract-text"] }],
216
+ ["context", { kind: "string", allowedFor: ["diagnose-error"] }],
217
+ ["type", { kind: "string", allowedFor: ["diagram"] }],
218
+ ["focus", { kind: "string", allowedFor: ["chart"] }],
219
+ ["output", { kind: "string", enumValues: VISION_OUTPUT_VALUES, allowedFor: ["ui-to-code"] }],
220
+ ["expected", { kind: "string", requiredFor: ["diff"], allowedFor: ["diff"] }],
221
+ ["actual", { kind: "string", requiredFor: ["diff"], allowedFor: ["diff"] }],
222
+ ],
223
+ };
224
+ // ---------------------------------------------------------------------------
225
+ // Parse helpers
226
+ // ---------------------------------------------------------------------------
227
+ const MANIFEST_KEYS = new Set(["schemaVersion", "operations"]);
228
+ const OPERATION_KEYS = new Set(["name", "command", "input", "provider", "output"]);
229
+ const OPERATION_NAME_PATTERN = /^[A-Za-z0-9._-]+$/;
230
+ const MAX_OPERATIONS = 256;
231
+ function isPlainObject(value) {
232
+ return typeof value === "object" && value !== null && !Array.isArray(value);
233
+ }
234
+ function isAllowedBatchCommand(value) {
235
+ return BATCH_ALLOWED_COMMANDS.includes(value);
236
+ }
237
+ function isKnownProviderId(value) {
238
+ return PROVIDER_IDS.includes(value);
239
+ }
240
+ function isRequiredFor(spec, subcommand) {
241
+ if (spec.required)
242
+ return true;
243
+ return spec.requiredFor !== undefined && subcommand !== undefined && spec.requiredFor.includes(subcommand);
244
+ }
245
+ function validateInputObject(where, command, input) {
246
+ const table = INPUT_FIELD_TABLES[command];
247
+ const knownFields = new Set(table.map(([field]) => field));
248
+ // Unknown fields reject before any value validation (strict parse,
249
+ // first offender in key order).
250
+ for (const key of Object.keys(input)) {
251
+ if (!knownFields.has(key)) {
252
+ throw new ValidationError(`${where}.input: unknown field "${key}" for command "${command}"`);
253
+ }
254
+ }
255
+ let subcommand;
256
+ for (const [field, spec] of table) {
257
+ const present = input[field] !== undefined;
258
+ const value = input[field];
259
+ if (field === "subcommand") {
260
+ if (!present) {
261
+ throw new ValidationError(`${where}.input: missing required field "subcommand"`);
262
+ }
263
+ if (typeof value !== "string") {
264
+ throw new ValidationError(`${where}.input: field "subcommand" must be a string`);
265
+ }
266
+ if (spec.enumValues && !spec.enumValues.includes(value)) {
267
+ throw new ValidationError(`${where}.input: field "subcommand" must be one of: ${spec.enumValues.join(", ")}`);
268
+ }
269
+ subcommand = value;
270
+ continue;
271
+ }
272
+ if (!present) {
273
+ if (isRequiredFor(spec, subcommand)) {
274
+ throw new ValidationError(`${where}.input: missing required field "${field}"`);
275
+ }
276
+ continue;
277
+ }
278
+ // Present: subcommand scoping is checked before type/enum so a
279
+ // misplaced field is reported as a scoping error, never as a type
280
+ // error against the wrong subcommand's expectations.
281
+ if (spec.allowedFor !== undefined && (subcommand === undefined || !spec.allowedFor.includes(subcommand))) {
282
+ throw new ValidationError(`${where}.input: field "${field}" is not valid for ${command} subcommand "${subcommand}"`);
283
+ }
284
+ switch (spec.kind) {
285
+ case "string": {
286
+ if (typeof value !== "string") {
287
+ throw new ValidationError(`${where}.input: field "${field}" must be a string`);
288
+ }
289
+ if (spec.enumValues !== undefined && !spec.enumValues.includes(value)) {
290
+ throw new ValidationError(`${where}.input: field "${field}" must be one of: ${spec.enumValues.join(", ")}`);
291
+ }
292
+ if (value.length === 0) {
293
+ // D2: an empty required value rejects as missing-required, so
294
+ // the parser normalizes across handlers' differing guards.
295
+ if (isRequiredFor(spec, subcommand)) {
296
+ throw new ValidationError(`${where}.input: missing required field "${field}"`);
297
+ }
298
+ throw new ValidationError(`${where}.input: field "${field}" must be a non-empty string`);
299
+ }
300
+ break;
301
+ }
302
+ case "number": {
303
+ if (typeof value !== "number" || !Number.isFinite(value)) {
304
+ throw new ValidationError(`${where}.input: field "${field}" must be a number`);
305
+ }
306
+ break;
307
+ }
308
+ case "boolean": {
309
+ if (typeof value !== "boolean") {
310
+ throw new ValidationError(`${where}.input: field "${field}" must be a boolean`);
311
+ }
312
+ break;
313
+ }
314
+ case "stringArray": {
315
+ if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) {
316
+ throw new ValidationError(`${where}.input: field "${field}" must be an array of strings`);
317
+ }
318
+ // Review fix: `fields: [""]` would render `--fields ""` — the
319
+ // handler's parseArgs treats a falsy next token as NO value
320
+ // (`flags.fields === true`) and its .split then crashes on the
321
+ // boolean. Reject empty members at parse (strict-parse posture).
322
+ if (!value.every((entry) => entry.length > 0)) {
323
+ throw new ValidationError(`${where}.input: field "${field}" must not contain empty entries`);
324
+ }
325
+ break;
326
+ }
327
+ }
328
+ }
329
+ }
330
+ /**
331
+ * Strict-parse a batch manifest (D2). Every rejection throws
332
+ * `ValidationError` naming the first offender; nothing about a rejected
333
+ * manifest is partially accepted. Provider pins are validated against
334
+ * the injected descriptor metadata only — `create()` is never called.
335
+ */
336
+ export function parseBatchManifest(raw, deps) {
337
+ if (!isPlainObject(raw)) {
338
+ throw new ValidationError("batch manifest must be a JSON object");
339
+ }
340
+ for (const key of Object.keys(raw)) {
341
+ if (!MANIFEST_KEYS.has(key)) {
342
+ throw new ValidationError(`unknown manifest field "${key}"`);
343
+ }
344
+ }
345
+ if (raw.schemaVersion === undefined) {
346
+ throw new ValidationError('missing required manifest field "schemaVersion"');
347
+ }
348
+ // Strict-equality gate (config-store precedent): only `1` parses.
349
+ if (raw.schemaVersion !== 1) {
350
+ throw new ValidationError(`unsupported schemaVersion ${JSON.stringify(raw.schemaVersion)}: expected 1`);
351
+ }
352
+ if (raw.operations === undefined) {
353
+ throw new ValidationError('missing required manifest field "operations"');
354
+ }
355
+ const rawOperations = raw.operations;
356
+ if (!Array.isArray(rawOperations)) {
357
+ throw new ValidationError('manifest field "operations" must be an array');
358
+ }
359
+ if (rawOperations.length < 1 || rawOperations.length > MAX_OPERATIONS) {
360
+ throw new ValidationError(`manifest "operations" must contain between 1 and ${MAX_OPERATIONS} entries`);
361
+ }
362
+ const seenNames = new Set();
363
+ // Declared output targets, path -> owning op name: two ops writing the
364
+ // same target would silently overwrite each other at the D9 rename, so
365
+ // duplicates reject at parse naming the earlier owner.
366
+ const seenOutputs = new Map();
367
+ const operations = [];
368
+ for (let index = 0; index < rawOperations.length; index++) {
369
+ const where = `operations[${index}]`;
370
+ const rawOp = rawOperations[index];
371
+ if (!isPlainObject(rawOp)) {
372
+ throw new ValidationError(`${where} must be an object`);
373
+ }
374
+ for (const key of Object.keys(rawOp)) {
375
+ if (!OPERATION_KEYS.has(key)) {
376
+ throw new ValidationError(`${where}: unknown field "${key}"`);
377
+ }
378
+ }
379
+ if (rawOp.name === undefined) {
380
+ throw new ValidationError(`${where}: missing required field "name"`);
381
+ }
382
+ const name = rawOp.name;
383
+ if (typeof name !== "string") {
384
+ throw new ValidationError(`${where}: field "name" must be a string`);
385
+ }
386
+ if (name.length < 1 || name.length > 64 || !OPERATION_NAME_PATTERN.test(name)) {
387
+ throw new ValidationError(`${where}: invalid operation name "${name}" (must be 1-64 characters from letters, digits, ".", "_", "-")`);
388
+ }
389
+ if (seenNames.has(name)) {
390
+ throw new ValidationError(`${where}: duplicate operation name "${name}"`);
391
+ }
392
+ seenNames.add(name);
393
+ if (rawOp.command === undefined) {
394
+ throw new ValidationError(`${where}: missing required field "command"`);
395
+ }
396
+ const command = rawOp.command;
397
+ if (typeof command !== "string") {
398
+ throw new ValidationError(`${where}: field "command" must be a string`);
399
+ }
400
+ if (!isAllowedBatchCommand(command)) {
401
+ // D3 pins this rejection message verbatim (no per-op prefix).
402
+ throw new ValidationError(BATCH_ALLOWLIST_MESSAGE);
403
+ }
404
+ if (rawOp.input === undefined) {
405
+ throw new ValidationError(`${where}: missing required field "input"`);
406
+ }
407
+ const input = rawOp.input;
408
+ if (!isPlainObject(input)) {
409
+ throw new ValidationError(`${where}: field "input" must be an object`);
410
+ }
411
+ validateInputObject(where, command, input);
412
+ let provider;
413
+ if (rawOp.provider !== undefined) {
414
+ const rawProvider = rawOp.provider;
415
+ if (typeof rawProvider !== "string") {
416
+ throw new ValidationError(`${where}: field "provider" must be a string`);
417
+ }
418
+ if (!isKnownProviderId(rawProvider)) {
419
+ throw new ValidationError(`${where}: unknown provider "${rawProvider}". Built-in providers: ${PROVIDER_IDS.join(", ")}.`);
420
+ }
421
+ const capabilityId = batchCommandCapabilityId(command, input);
422
+ const descriptor = deps.descriptors.find((entry) => entry.id === rawProvider);
423
+ if (descriptor === undefined || !descriptor.capabilities().has(capabilityId)) {
424
+ throw new ValidationError(`${where}: provider "${rawProvider}" does not support capability "${capabilityId}"`);
425
+ }
426
+ provider = rawProvider;
427
+ }
428
+ let output;
429
+ if (rawOp.output !== undefined) {
430
+ const rawOutput = rawOp.output;
431
+ if (typeof rawOutput !== "string" || rawOutput.length === 0) {
432
+ throw new ValidationError(`${where}: field "output" must be a non-empty string`);
433
+ }
434
+ const dir = path.dirname(rawOutput);
435
+ if (!deps.dirExists(dir)) {
436
+ throw new ValidationError(`${where}: output directory "${dir}" does not exist`);
437
+ }
438
+ // Review fix: key duplicate detection on the RESOLVED path —
439
+ // `out/a.json`, `out/./a.json`, `out//a.json`, and `./out/a.json`
440
+ // are string-distinct but name the same file; the D9 temp+rename
441
+ // write would let the later op silently overwrite the earlier
442
+ // op's result. The operation record keeps the raw declared string.
443
+ const outputKey = path.resolve(rawOutput);
444
+ const priorOwner = seenOutputs.get(outputKey);
445
+ if (priorOwner !== undefined) {
446
+ throw new ValidationError(`${where}: duplicate output target "${rawOutput}" (already declared by operation "${priorOwner}")`);
447
+ }
448
+ seenOutputs.set(outputKey, name);
449
+ output = rawOutput;
450
+ }
451
+ // `validateInputObject` proved `input` matches the field table for
452
+ // `command` (only known keys, validated types and enums), but that
453
+ // runtime guarantee cannot narrow the Record to the per-command
454
+ // input union here — hence the double cast.
455
+ operations.push({
456
+ name,
457
+ command,
458
+ input: { ...input },
459
+ ...(provider !== undefined ? { provider } : {}),
460
+ ...(output !== undefined ? { output } : {}),
461
+ });
462
+ }
463
+ return { schemaVersion: 1, operations };
464
+ }
465
+ // ---------------------------------------------------------------------------
466
+ // D2.8 — input → argv compiler
467
+ // ---------------------------------------------------------------------------
468
+ /** Mechanical camelCase → kebab-case flag name mapping (D2.8, no exceptions). */
469
+ function flagName(field) {
470
+ return `--${field.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`;
471
+ }
472
+ function assertNoLeadingDash(rendered, field) {
473
+ if (rendered.startsWith("-")) {
474
+ // parseArgs would split the value into a new flag + stray positional,
475
+ // so the compiler refuses rather than corrupt argv.
476
+ throw new ValidationError(`input field "${field}" must not begin with "-"`);
477
+ }
478
+ return rendered;
479
+ }
480
+ /**
481
+ * Emit a flag. Booleans render as the bare flag only when `true` —
482
+ * `false` and `undefined` both emit nothing (the handler default is the
483
+ * off-state). Numbers and strings render as the next argv element;
484
+ * `fields` joins with "," (the handler re-splits on comma).
485
+ */
486
+ function emitFlag(argv, field, value) {
487
+ if (value === undefined || value === false)
488
+ return;
489
+ if (value === true) {
490
+ argv.push(flagName(field));
491
+ return;
492
+ }
493
+ let rendered;
494
+ if (Array.isArray(value)) {
495
+ if (value.length === 0)
496
+ return;
497
+ // Defensive twin of the parse-layer check (review fix): a joined
498
+ // empty member renders a falsy flag value parseArgs turns boolean.
499
+ if (value.some((entry) => String(entry).length === 0)) {
500
+ throw new ValidationError(`input field "${field}" must not contain empty entries`);
501
+ }
502
+ rendered = value.map((entry) => String(entry)).join(",");
503
+ }
504
+ else if (typeof value === "number") {
505
+ rendered = String(value);
506
+ }
507
+ else if (typeof value === "string") {
508
+ if (value.length === 0) {
509
+ throw new ValidationError(`input field "${field}" must not be empty`);
510
+ }
511
+ rendered = value;
512
+ }
513
+ else {
514
+ throw new ValidationError(`input field "${field}" has an unsupported value`);
515
+ }
516
+ argv.push(flagName(field), assertNoLeadingDash(rendered, field));
517
+ }
518
+ /** Emit a positional value; defensive against unparsed inputs. */
519
+ function emitPositional(argv, field, value) {
520
+ if (value === undefined || value.length === 0) {
521
+ throw new ValidationError(`input field "${field}" must not be empty`);
522
+ }
523
+ argv.push(assertNoLeadingDash(value, field));
524
+ }
525
+ /**
526
+ * Compile a parsed operation's `input` to the argv its handler consumes
527
+ * (D2.8). Deterministic: same input → same argv, always. Output is a
528
+ * `string[]` — no shell joining, no escaping hazards. Required fields
529
+ * for the five positional handlers never render as flags.
530
+ */
531
+ export function compileInput(op) {
532
+ const argv = [];
533
+ switch (op.command) {
534
+ case "search": {
535
+ const input = op.input;
536
+ emitPositional(argv, "query", input.query);
537
+ emitFlag(argv, "count", input.count);
538
+ emitFlag(argv, "domain", input.domain);
539
+ emitFlag(argv, "recency", input.recency);
540
+ emitFlag(argv, "contentSize", input.contentSize);
541
+ emitFlag(argv, "location", input.location);
542
+ emitFlag(argv, "topic", input.topic);
543
+ emitFlag(argv, "type", input.type);
544
+ emitFlag(argv, "maxSummary", input.maxSummary);
545
+ emitFlag(argv, "fields", input.fields);
546
+ emitFlag(argv, "noCache", input.noCache);
547
+ emitFlag(argv, "merge", input.merge);
548
+ return argv;
549
+ }
550
+ case "read": {
551
+ const input = op.input;
552
+ emitPositional(argv, "url", input.url);
553
+ emitFlag(argv, "format", input.format);
554
+ emitFlag(argv, "noImages", input.noImages);
555
+ emitFlag(argv, "withLinks", input.withLinks);
556
+ emitFlag(argv, "withImagesSummary", input.withImagesSummary);
557
+ emitFlag(argv, "noCache", input.noCache);
558
+ return argv;
559
+ }
560
+ case "research": {
561
+ const input = op.input;
562
+ emitPositional(argv, "query", input.query);
563
+ emitFlag(argv, "model", input.model);
564
+ emitFlag(argv, "outputLength", input.outputLength);
565
+ emitFlag(argv, "citationFormat", input.citationFormat);
566
+ emitFlag(argv, "domain", input.domain);
567
+ emitFlag(argv, "maxChars", input.maxChars);
568
+ emitFlag(argv, "timeout", input.timeout);
569
+ emitFlag(argv, "noCache", input.noCache);
570
+ return argv;
571
+ }
572
+ case "repo": {
573
+ const input = op.input;
574
+ argv.push(input.subcommand);
575
+ emitPositional(argv, "repository", input.repository);
576
+ if (input.subcommand === "search") {
577
+ emitPositional(argv, "query", input.query);
578
+ }
579
+ else if (input.subcommand === "read") {
580
+ emitPositional(argv, "path", input.path);
581
+ }
582
+ else {
583
+ // tree / brief: path renders as the --path flag.
584
+ emitFlag(argv, "path", input.path);
585
+ }
586
+ emitFlag(argv, "language", input.language);
587
+ emitFlag(argv, "maxChars", input.maxChars);
588
+ emitFlag(argv, "focus", input.focus);
589
+ emitFlag(argv, "depth", input.depth);
590
+ emitFlag(argv, "noCache", input.noCache);
591
+ return argv;
592
+ }
593
+ case "vision": {
594
+ const input = op.input;
595
+ argv.push(input.subcommand);
596
+ if (input.subcommand === "diff") {
597
+ emitPositional(argv, "expected", input.expected);
598
+ emitPositional(argv, "actual", input.actual);
599
+ if (input.prompt !== undefined)
600
+ emitPositional(argv, "prompt", input.prompt);
601
+ }
602
+ else {
603
+ emitPositional(argv, "source", input.source);
604
+ if (input.prompt !== undefined)
605
+ emitPositional(argv, "prompt", input.prompt);
606
+ }
607
+ emitFlag(argv, "language", input.language);
608
+ emitFlag(argv, "context", input.context);
609
+ emitFlag(argv, "type", input.type);
610
+ emitFlag(argv, "focus", input.focus);
611
+ emitFlag(argv, "output", input.output);
612
+ return argv;
613
+ }
614
+ case "crawl": {
615
+ const input = op.input;
616
+ emitPositional(argv, "url", input.url);
617
+ emitFlag(argv, "depth", input.depth);
618
+ emitFlag(argv, "breadth", input.breadth);
619
+ emitFlag(argv, "limit", input.limit);
620
+ emitFlag(argv, "selectPaths", input.selectPaths);
621
+ emitFlag(argv, "excludePaths", input.excludePaths);
622
+ emitFlag(argv, "instructions", input.instructions);
623
+ emitFlag(argv, "format", input.format);
624
+ emitFlag(argv, "contentSize", input.contentSize);
625
+ emitFlag(argv, "timeout", input.timeout);
626
+ emitFlag(argv, "maxChars", input.maxChars);
627
+ emitFlag(argv, "noCache", input.noCache);
628
+ return argv;
629
+ }
630
+ case "map": {
631
+ const input = op.input;
632
+ emitPositional(argv, "url", input.url);
633
+ emitFlag(argv, "depth", input.depth);
634
+ emitFlag(argv, "breadth", input.breadth);
635
+ emitFlag(argv, "limit", input.limit);
636
+ emitFlag(argv, "selectPaths", input.selectPaths);
637
+ emitFlag(argv, "excludePaths", input.excludePaths);
638
+ emitFlag(argv, "instructions", input.instructions);
639
+ emitFlag(argv, "noCache", input.noCache);
640
+ return argv;
641
+ }
642
+ }
643
+ }
644
+ //# sourceMappingURL=batch-manifest.js.map