botmux-workflow-core 3.16.0 → 3.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.
@@ -155,6 +155,79 @@ function isRecord(value) {
155
155
  return typeof value === "object" && value !== null && !Array.isArray(value);
156
156
  }
157
157
 
158
+ // src/workflows/v3/artifact-contract.ts
159
+ var MANIFEST_FILE_KINDS = [
160
+ "markdown",
161
+ "json",
162
+ "text",
163
+ "code",
164
+ "log",
165
+ "binary",
166
+ "directory"
167
+ ];
168
+ var MANIFEST_SUMMARY_MAX_BYTES = 4 * 1024;
169
+ var MANIFEST_PREVIEW_MAX_BYTES = 4 * 1024;
170
+
171
+ // src/workflows/v3/artifact-contract-declarations.ts
172
+ var V3_ARTIFACT_OUTPUT_KEY_RE = /^[A-Za-z0-9._-]+$/;
173
+ var V3_ARTIFACT_OUTPUT_MAX_COUNT = 32;
174
+ var V3_ARTIFACT_OUTPUT_MAX_BYTES = 4 * 1024;
175
+ function normalizeArtifactOutputs(value, where, problems) {
176
+ if (value === void 0) return void 0;
177
+ if (!isRecord2(value) || Object.keys(value).length === 0) {
178
+ problems.push(`${where}.outputs must be a non-empty object when present`);
179
+ return void 0;
180
+ }
181
+ const entries = Object.entries(value);
182
+ if (entries.length > V3_ARTIFACT_OUTPUT_MAX_COUNT) {
183
+ problems.push(`${where}.outputs has ${entries.length} entries (max ${V3_ARTIFACT_OUTPUT_MAX_COUNT})`);
184
+ }
185
+ if (Buffer.byteLength(JSON.stringify(value), "utf-8") > V3_ARTIFACT_OUTPUT_MAX_BYTES) {
186
+ problems.push(`${where}.outputs exceeds ${V3_ARTIFACT_OUTPUT_MAX_BYTES} serialized bytes`);
187
+ }
188
+ const out = /* @__PURE__ */ Object.create(null);
189
+ const paths = /* @__PURE__ */ new Set();
190
+ for (const [key, raw] of entries) {
191
+ const itemWhere = `${where}.outputs.${JSON.stringify(key)}`;
192
+ if (!V3_ARTIFACT_OUTPUT_KEY_RE.test(key)) {
193
+ problems.push(`${itemWhere} key must match ${V3_ARTIFACT_OUTPUT_KEY_RE}`);
194
+ continue;
195
+ }
196
+ if (!isRecord2(raw)) {
197
+ problems.push(`${itemWhere} must be { path, kind }`);
198
+ continue;
199
+ }
200
+ const extra = Object.keys(raw).filter((field) => field !== "path" && field !== "kind");
201
+ if (extra.length > 0) {
202
+ problems.push(`${itemWhere} has unsupported key(s): ${extra.join(", ")} (allowed: path, kind)`);
203
+ continue;
204
+ }
205
+ if (!isPortableRelativeArtifactPath(raw.path)) {
206
+ problems.push(`${itemWhere}.path must be a portable relative path without '.', '..', or empty segments`);
207
+ continue;
208
+ }
209
+ if (typeof raw.kind !== "string" || !MANIFEST_FILE_KINDS.includes(raw.kind)) {
210
+ problems.push(`${itemWhere}.kind must be one of ${MANIFEST_FILE_KINDS.join(" | ")}`);
211
+ continue;
212
+ }
213
+ if (paths.has(raw.path)) {
214
+ problems.push(`${where}.outputs contains duplicate path ${JSON.stringify(raw.path)}`);
215
+ continue;
216
+ }
217
+ paths.add(raw.path);
218
+ out[key] = { path: raw.path, kind: raw.kind };
219
+ }
220
+ return out;
221
+ }
222
+ function isPortableRelativeArtifactPath(value) {
223
+ if (typeof value !== "string" || value.length === 0 || value.startsWith("/") || value.includes("\\") || value.includes("\0") || /^[A-Za-z]:/.test(value)) return false;
224
+ const segments = value.split("/");
225
+ return segments.every((segment) => segment !== "" && segment !== "." && segment !== "..");
226
+ }
227
+ function isRecord2(value) {
228
+ return typeof value === "object" && value !== null && !Array.isArray(value);
229
+ }
230
+
158
231
  // src/workflows/v3/dag.ts
159
232
  var NODE_KINDS = ["goal", "host", "loop"];
160
233
  var V3_HOST_EXECUTORS = ["feishu-send", "feishu-reply", "botmux-schedule"];
@@ -205,6 +278,10 @@ function validateDag(raw) {
205
278
  if (!isObject(raw)) {
206
279
  throw new DagValidationError(["root must be a JSON object"]);
207
280
  }
281
+ const schemaVersion = raw.schemaVersion === void 0 ? 1 : raw.schemaVersion;
282
+ if (schemaVersion !== 1 && schemaVersion !== 2) {
283
+ problems.push(`schemaVersion must be 1 or 2 (got ${JSON.stringify(raw.schemaVersion)})`);
284
+ }
208
285
  if (typeof raw.runId !== "string" || !SEGMENT_RE.test(raw.runId)) {
209
286
  problems.push(`runId must be a path-safe string matching ${SEGMENT_RE} (got ${JSON.stringify(raw.runId)})`);
210
287
  }
@@ -241,9 +318,9 @@ function validateDag(raw) {
241
318
  if (fromList.includes(id)) problems.push(`node "${id}" depends on itself`);
242
319
  if (new Set(fromList).size !== fromList.length) problems.push(`node "${id}".depends has duplicates`);
243
320
  const triggerRule = normTriggerRule(n.triggerRule, depends.length, `node "${id}"`, problems);
244
- const inputs = normInputs(n.inputs, id, problems);
321
+ const inputs = normInputs(n.inputs, id, problems, schemaVersion === 2 ? 2 : 1);
245
322
  if (type === "loop") {
246
- const loopFields = normLoopFields(n, id, problems);
323
+ const loopFields = normLoopFields(n, id, problems, schemaVersion === 2 ? 2 : 1);
247
324
  if (loopFields) {
248
325
  nodes.push({
249
326
  id,
@@ -260,6 +337,7 @@ function validateDag(raw) {
260
337
  continue;
261
338
  }
262
339
  if (type === "host") {
340
+ if (n.outputs !== void 0) problems.push(`host node "${id}".outputs is not supported`);
263
341
  if (n.goal !== void 0) problems.push(`host node "${id}".goal is not supported`);
264
342
  if (n.bot !== void 0) problems.push(`host node "${id}".bot is not supported \u2014 host nodes do not spawn a CLI`);
265
343
  if (n.override !== void 0) problems.push(`host node "${id}".override is not supported`);
@@ -335,6 +413,10 @@ function validateDag(raw) {
335
413
  const humanGate = normHumanGate(n.humanGate, `node "${id}"`, problems);
336
414
  const override = normOverride(n.override, `node "${id}"`, problems);
337
415
  const revisitTo = normRevisitTo(n.revisitTo, id, problems);
416
+ if (schemaVersion !== 2 && n.outputs !== void 0) {
417
+ problems.push(`goal node "${id}".outputs requires schemaVersion 2`);
418
+ }
419
+ const outputs = schemaVersion === 2 ? normalizeArtifactOutputs(n.outputs, `goal node "${id}"`, problems) : void 0;
338
420
  nodes.push({
339
421
  id,
340
422
  type,
@@ -344,6 +426,7 @@ function validateDag(raw) {
344
426
  triggerRule,
345
427
  override,
346
428
  inputs,
429
+ outputs,
347
430
  timeoutSec,
348
431
  humanGate,
349
432
  resultSchema,
@@ -359,6 +442,14 @@ function validateDag(raw) {
359
442
  problems.push(`node "${node.id}".inputs references unknown node "${inp.from}"`);
360
443
  } else if (!node.depends.some((d) => d.from === inp.from)) {
361
444
  problems.push(`node "${node.id}".inputs.from "${inp.from}" must also be in depends`);
445
+ } else if (inp.output !== void 0) {
446
+ const source = nodes.find((candidate) => candidate.id === inp.from);
447
+ const outputs = source?.type === "loop" ? source.body?.nodes.find((bodyNode) => bodyNode.id === source.output?.from)?.outputs : source?.outputs;
448
+ if (!outputs || !Object.prototype.hasOwnProperty.call(outputs, inp.output)) {
449
+ problems.push(
450
+ `node "${node.id}".inputs output ${JSON.stringify(inp.output)} is not declared by source "${inp.from}"`
451
+ );
452
+ }
362
453
  }
363
454
  }
364
455
  if (node.type === "host") {
@@ -437,7 +528,11 @@ function validateDag(raw) {
437
528
  }
438
529
  }
439
530
  if (problems.length > 0) throw new DagValidationError(problems);
440
- const dag = { runId: raw.runId, nodes };
531
+ const dag = {
532
+ ...raw.schemaVersion !== void 0 ? { schemaVersion } : {},
533
+ runId: raw.runId,
534
+ nodes
535
+ };
441
536
  topologicalOrder(dag);
442
537
  return dag;
443
538
  }
@@ -751,7 +846,7 @@ function downstreamCone(nodeId, nodes) {
751
846
  function nodeByIdUnsafe(nodes, nodeId) {
752
847
  return nodes.find((node) => node.id === nodeId);
753
848
  }
754
- function normLoopFields(n, id, problems) {
849
+ function normLoopFields(n, id, problems, schemaVersion) {
755
850
  const where = `loop node "${id}"`;
756
851
  const before = problems.length;
757
852
  if (n.timeoutSec !== void 0) {
@@ -822,10 +917,14 @@ function normLoopFields(n, id, problems) {
822
917
  const bFromList = bdepends.map((d) => d.from);
823
918
  if (bFromList.includes(bid)) problems.push(`${where}.body node "${bid}" depends on itself`);
824
919
  if (new Set(bFromList).size !== bFromList.length) problems.push(`${where}.body node "${bid}".depends has duplicates`);
825
- const binputs = normInputs(b.inputs, `${id}.body.${bid}`, problems);
920
+ const binputs = normInputs(b.inputs, `${id}.body.${bid}`, problems, schemaVersion);
826
921
  const btimeout = normTimeoutSec(b.timeoutSec, `${where}.body node "${bid}"`, problems);
827
922
  const bschema = normResultSchema(b.resultSchema, `${id}.body.${bid}`, problems);
828
923
  const boverride = normOverride(b.override, `${where}.body node "${bid}"`, problems);
924
+ if (schemaVersion !== 2 && b.outputs !== void 0) {
925
+ problems.push(`${where}.body node "${bid}".outputs requires schemaVersion 2`);
926
+ }
927
+ const boutputs = schemaVersion === 2 ? normalizeArtifactOutputs(b.outputs, `${where}.body node "${bid}"`, problems) : void 0;
829
928
  bodyNodes.push({
830
929
  id: bid,
831
930
  type: "goal",
@@ -834,6 +933,7 @@ function normLoopFields(n, id, problems) {
834
933
  depends: bdepends,
835
934
  override: boverride,
836
935
  inputs: binputs,
936
+ outputs: boutputs,
837
937
  timeoutSec: btimeout,
838
938
  humanGate: null,
839
939
  resultSchema: bschema
@@ -848,6 +948,13 @@ function normLoopFields(n, id, problems) {
848
948
  problems.push(`${where}.body node "${bn.id}".inputs references unknown body node "${inp.from}"`);
849
949
  } else if (!bn.depends.some((d) => d.from === inp.from)) {
850
950
  problems.push(`${where}.body node "${bn.id}".inputs.from "${inp.from}" must also be in depends`);
951
+ } else if (inp.output !== void 0) {
952
+ const source = bodyNodes.find((candidate) => candidate.id === inp.from);
953
+ if (!source?.outputs || !Object.prototype.hasOwnProperty.call(source.outputs, inp.output)) {
954
+ problems.push(
955
+ `${where}.body node "${bn.id}".inputs output ${JSON.stringify(inp.output)} is not declared by source "${inp.from}"`
956
+ );
957
+ }
851
958
  }
852
959
  }
853
960
  }
@@ -1073,7 +1180,7 @@ function normResultSchema(v, id, problems) {
1073
1180
  }
1074
1181
  return schema;
1075
1182
  }
1076
- function normInputs(v, id, problems) {
1183
+ function normInputs(v, id, problems, schemaVersion) {
1077
1184
  if (v === void 0) return [];
1078
1185
  if (!Array.isArray(v)) {
1079
1186
  problems.push(`node "${id}".inputs must be an array`);
@@ -1086,9 +1193,29 @@ function normInputs(v, id, problems) {
1086
1193
  problems.push(`node "${id}".inputs[${j}] must be { from: <nodeId>, select? }`);
1087
1194
  continue;
1088
1195
  }
1089
- const extra = Object.keys(inp).filter((k) => k !== "from" && k !== "select");
1196
+ const extra = Object.keys(inp).filter((k) => k !== "from" && k !== "select" && k !== "output");
1090
1197
  if (extra.length > 0) {
1091
- problems.push(`node "${id}".inputs[${j}] has unsupported key(s): ${extra.join(", ")} (allowed: from, select)`);
1198
+ problems.push(`node "${id}".inputs[${j}] has unsupported key(s): ${extra.join(", ")} (allowed: from, select/output)`);
1199
+ continue;
1200
+ }
1201
+ if (schemaVersion === 2) {
1202
+ if (inp.select !== void 0) {
1203
+ problems.push(`node "${id}".inputs[${j}].select is legacy-only; schemaVersion 2 must use output`);
1204
+ continue;
1205
+ }
1206
+ if (inp.output === void 0) {
1207
+ out.push({ from: inp.from });
1208
+ continue;
1209
+ }
1210
+ if (typeof inp.output !== "string" || !SEGMENT_RE.test(inp.output)) {
1211
+ problems.push(`node "${id}".inputs[${j}].output must be a stable key matching ${SEGMENT_RE}`);
1212
+ continue;
1213
+ }
1214
+ out.push({ from: inp.from, output: inp.output });
1215
+ continue;
1216
+ }
1217
+ if (inp.output !== void 0) {
1218
+ problems.push(`node "${id}".inputs[${j}].output requires schemaVersion 2`);
1092
1219
  continue;
1093
1220
  }
1094
1221
  if (inp.select === void 0) {