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.
- package/README.md +47 -0
- package/dist/command-invocation.d.ts +2 -2
- package/dist/command-invocation.d.ts.map +1 -1
- package/dist/command-invocation.js +1 -1
- package/dist/command-invocation.js.map +1 -1
- package/dist/commands/research.d.ts +45 -0
- package/dist/commands/research.d.ts.map +1 -1
- package/dist/commands/research.js +152 -4
- package/dist/commands/research.js.map +1 -1
- package/dist/commands/search.d.ts +11 -0
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +31 -2
- package/dist/commands/search.js.map +1 -1
- package/dist/commands/usage.d.ts +101 -0
- package/dist/commands/usage.d.ts.map +1 -0
- package/dist/commands/usage.js +228 -0
- package/dist/commands/usage.js.map +1 -0
- package/dist/commands/vision.d.ts +28 -0
- package/dist/commands/vision.d.ts.map +1 -1
- package/dist/commands/vision.js +452 -0
- package/dist/commands/vision.js.map +1 -1
- package/dist/index.d.ts +25 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +572 -10
- package/dist/index.js.map +1 -1
- package/dist/lib/batch-assign.d.ts +61 -0
- package/dist/lib/batch-assign.d.ts.map +1 -0
- package/dist/lib/batch-assign.js +118 -0
- package/dist/lib/batch-assign.js.map +1 -0
- package/dist/lib/batch-manifest.d.ts +183 -0
- package/dist/lib/batch-manifest.d.ts.map +1 -0
- package/dist/lib/batch-manifest.js +644 -0
- package/dist/lib/batch-manifest.js.map +1 -0
- package/dist/lib/batch-runner.d.ts +169 -0
- package/dist/lib/batch-runner.d.ts.map +1 -0
- package/dist/lib/batch-runner.js +357 -0
- package/dist/lib/batch-runner.js.map +1 -0
- package/dist/lib/consumption.d.ts +17 -0
- package/dist/lib/consumption.d.ts.map +1 -1
- package/dist/lib/consumption.js +38 -0
- package/dist/lib/consumption.js.map +1 -1
- package/dist/lib/context-file.d.ts +90 -0
- package/dist/lib/context-file.d.ts.map +1 -0
- package/dist/lib/context-file.js +244 -0
- package/dist/lib/context-file.js.map +1 -0
- package/dist/lib/trigger-detection.d.ts +21 -0
- package/dist/lib/trigger-detection.d.ts.map +1 -1
- package/dist/lib/trigger-detection.js +63 -0
- package/dist/lib/trigger-detection.js.map +1 -1
- package/dist/lib/usage-ledger.d.ts +221 -0
- package/dist/lib/usage-ledger.d.ts.map +1 -0
- package/dist/lib/usage-ledger.js +408 -0
- package/dist/lib/usage-ledger.js.map +1 -0
- package/dist/node-command-invocation-adapter.d.ts +25 -0
- package/dist/node-command-invocation-adapter.d.ts.map +1 -1
- package/dist/node-command-invocation-adapter.js +73 -38
- package/dist/node-command-invocation-adapter.js.map +1 -1
- 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
|