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.
@@ -147,6 +147,79 @@ function isRecord(value) {
147
147
  return typeof value === "object" && value !== null && !Array.isArray(value);
148
148
  }
149
149
 
150
+ // src/workflows/v3/artifact-contract.ts
151
+ var MANIFEST_FILE_KINDS = [
152
+ "markdown",
153
+ "json",
154
+ "text",
155
+ "code",
156
+ "log",
157
+ "binary",
158
+ "directory"
159
+ ];
160
+ var MANIFEST_SUMMARY_MAX_BYTES = 4 * 1024;
161
+ var MANIFEST_PREVIEW_MAX_BYTES = 4 * 1024;
162
+
163
+ // src/workflows/v3/artifact-contract-declarations.ts
164
+ var V3_ARTIFACT_OUTPUT_KEY_RE = /^[A-Za-z0-9._-]+$/;
165
+ var V3_ARTIFACT_OUTPUT_MAX_COUNT = 32;
166
+ var V3_ARTIFACT_OUTPUT_MAX_BYTES = 4 * 1024;
167
+ function normalizeArtifactOutputs(value, where, problems) {
168
+ if (value === void 0) return void 0;
169
+ if (!isRecord2(value) || Object.keys(value).length === 0) {
170
+ problems.push(`${where}.outputs must be a non-empty object when present`);
171
+ return void 0;
172
+ }
173
+ const entries = Object.entries(value);
174
+ if (entries.length > V3_ARTIFACT_OUTPUT_MAX_COUNT) {
175
+ problems.push(`${where}.outputs has ${entries.length} entries (max ${V3_ARTIFACT_OUTPUT_MAX_COUNT})`);
176
+ }
177
+ if (Buffer.byteLength(JSON.stringify(value), "utf-8") > V3_ARTIFACT_OUTPUT_MAX_BYTES) {
178
+ problems.push(`${where}.outputs exceeds ${V3_ARTIFACT_OUTPUT_MAX_BYTES} serialized bytes`);
179
+ }
180
+ const out = /* @__PURE__ */ Object.create(null);
181
+ const paths = /* @__PURE__ */ new Set();
182
+ for (const [key, raw] of entries) {
183
+ const itemWhere = `${where}.outputs.${JSON.stringify(key)}`;
184
+ if (!V3_ARTIFACT_OUTPUT_KEY_RE.test(key)) {
185
+ problems.push(`${itemWhere} key must match ${V3_ARTIFACT_OUTPUT_KEY_RE}`);
186
+ continue;
187
+ }
188
+ if (!isRecord2(raw)) {
189
+ problems.push(`${itemWhere} must be { path, kind }`);
190
+ continue;
191
+ }
192
+ const extra = Object.keys(raw).filter((field) => field !== "path" && field !== "kind");
193
+ if (extra.length > 0) {
194
+ problems.push(`${itemWhere} has unsupported key(s): ${extra.join(", ")} (allowed: path, kind)`);
195
+ continue;
196
+ }
197
+ if (!isPortableRelativeArtifactPath(raw.path)) {
198
+ problems.push(`${itemWhere}.path must be a portable relative path without '.', '..', or empty segments`);
199
+ continue;
200
+ }
201
+ if (typeof raw.kind !== "string" || !MANIFEST_FILE_KINDS.includes(raw.kind)) {
202
+ problems.push(`${itemWhere}.kind must be one of ${MANIFEST_FILE_KINDS.join(" | ")}`);
203
+ continue;
204
+ }
205
+ if (paths.has(raw.path)) {
206
+ problems.push(`${where}.outputs contains duplicate path ${JSON.stringify(raw.path)}`);
207
+ continue;
208
+ }
209
+ paths.add(raw.path);
210
+ out[key] = { path: raw.path, kind: raw.kind };
211
+ }
212
+ return out;
213
+ }
214
+ function isPortableRelativeArtifactPath(value) {
215
+ if (typeof value !== "string" || value.length === 0 || value.startsWith("/") || value.includes("\\") || value.includes("\0") || /^[A-Za-z]:/.test(value)) return false;
216
+ const segments = value.split("/");
217
+ return segments.every((segment) => segment !== "" && segment !== "." && segment !== "..");
218
+ }
219
+ function isRecord2(value) {
220
+ return typeof value === "object" && value !== null && !Array.isArray(value);
221
+ }
222
+
150
223
  // src/workflows/v3/dag.ts
151
224
  var NODE_KINDS = ["goal", "host", "loop"];
152
225
  var V3_HOST_EXECUTORS = ["feishu-send", "feishu-reply", "botmux-schedule"];
@@ -196,6 +269,10 @@ function validateDag(raw) {
196
269
  if (!isObject(raw)) {
197
270
  throw new DagValidationError(["root must be a JSON object"]);
198
271
  }
272
+ const schemaVersion = raw.schemaVersion === void 0 ? 1 : raw.schemaVersion;
273
+ if (schemaVersion !== 1 && schemaVersion !== 2) {
274
+ problems.push(`schemaVersion must be 1 or 2 (got ${JSON.stringify(raw.schemaVersion)})`);
275
+ }
199
276
  if (typeof raw.runId !== "string" || !SEGMENT_RE.test(raw.runId)) {
200
277
  problems.push(`runId must be a path-safe string matching ${SEGMENT_RE} (got ${JSON.stringify(raw.runId)})`);
201
278
  }
@@ -232,9 +309,9 @@ function validateDag(raw) {
232
309
  if (fromList.includes(id)) problems.push(`node "${id}" depends on itself`);
233
310
  if (new Set(fromList).size !== fromList.length) problems.push(`node "${id}".depends has duplicates`);
234
311
  const triggerRule = normTriggerRule(n.triggerRule, depends.length, `node "${id}"`, problems);
235
- const inputs = normInputs(n.inputs, id, problems);
312
+ const inputs = normInputs(n.inputs, id, problems, schemaVersion === 2 ? 2 : 1);
236
313
  if (type === "loop") {
237
- const loopFields = normLoopFields(n, id, problems);
314
+ const loopFields = normLoopFields(n, id, problems, schemaVersion === 2 ? 2 : 1);
238
315
  if (loopFields) {
239
316
  nodes.push({
240
317
  id,
@@ -251,6 +328,7 @@ function validateDag(raw) {
251
328
  continue;
252
329
  }
253
330
  if (type === "host") {
331
+ if (n.outputs !== void 0) problems.push(`host node "${id}".outputs is not supported`);
254
332
  if (n.goal !== void 0) problems.push(`host node "${id}".goal is not supported`);
255
333
  if (n.bot !== void 0) problems.push(`host node "${id}".bot is not supported \u2014 host nodes do not spawn a CLI`);
256
334
  if (n.override !== void 0) problems.push(`host node "${id}".override is not supported`);
@@ -326,6 +404,10 @@ function validateDag(raw) {
326
404
  const humanGate = normHumanGate(n.humanGate, `node "${id}"`, problems);
327
405
  const override = normOverride(n.override, `node "${id}"`, problems);
328
406
  const revisitTo = normRevisitTo(n.revisitTo, id, problems);
407
+ if (schemaVersion !== 2 && n.outputs !== void 0) {
408
+ problems.push(`goal node "${id}".outputs requires schemaVersion 2`);
409
+ }
410
+ const outputs = schemaVersion === 2 ? normalizeArtifactOutputs(n.outputs, `goal node "${id}"`, problems) : void 0;
329
411
  nodes.push({
330
412
  id,
331
413
  type,
@@ -335,6 +417,7 @@ function validateDag(raw) {
335
417
  triggerRule,
336
418
  override,
337
419
  inputs,
420
+ outputs,
338
421
  timeoutSec,
339
422
  humanGate,
340
423
  resultSchema,
@@ -350,6 +433,14 @@ function validateDag(raw) {
350
433
  problems.push(`node "${node.id}".inputs references unknown node "${inp.from}"`);
351
434
  } else if (!node.depends.some((d) => d.from === inp.from)) {
352
435
  problems.push(`node "${node.id}".inputs.from "${inp.from}" must also be in depends`);
436
+ } else if (inp.output !== void 0) {
437
+ const source = nodes.find((candidate) => candidate.id === inp.from);
438
+ const outputs = source?.type === "loop" ? source.body?.nodes.find((bodyNode) => bodyNode.id === source.output?.from)?.outputs : source?.outputs;
439
+ if (!outputs || !Object.prototype.hasOwnProperty.call(outputs, inp.output)) {
440
+ problems.push(
441
+ `node "${node.id}".inputs output ${JSON.stringify(inp.output)} is not declared by source "${inp.from}"`
442
+ );
443
+ }
353
444
  }
354
445
  }
355
446
  if (node.type === "host") {
@@ -428,7 +519,11 @@ function validateDag(raw) {
428
519
  }
429
520
  }
430
521
  if (problems.length > 0) throw new DagValidationError(problems);
431
- const dag = { runId: raw.runId, nodes };
522
+ const dag = {
523
+ ...raw.schemaVersion !== void 0 ? { schemaVersion } : {},
524
+ runId: raw.runId,
525
+ nodes
526
+ };
432
527
  topologicalOrder(dag);
433
528
  return dag;
434
529
  }
@@ -742,7 +837,7 @@ function downstreamCone(nodeId, nodes) {
742
837
  function nodeByIdUnsafe(nodes, nodeId) {
743
838
  return nodes.find((node) => node.id === nodeId);
744
839
  }
745
- function normLoopFields(n, id, problems) {
840
+ function normLoopFields(n, id, problems, schemaVersion) {
746
841
  const where = `loop node "${id}"`;
747
842
  const before = problems.length;
748
843
  if (n.timeoutSec !== void 0) {
@@ -813,10 +908,14 @@ function normLoopFields(n, id, problems) {
813
908
  const bFromList = bdepends.map((d) => d.from);
814
909
  if (bFromList.includes(bid)) problems.push(`${where}.body node "${bid}" depends on itself`);
815
910
  if (new Set(bFromList).size !== bFromList.length) problems.push(`${where}.body node "${bid}".depends has duplicates`);
816
- const binputs = normInputs(b.inputs, `${id}.body.${bid}`, problems);
911
+ const binputs = normInputs(b.inputs, `${id}.body.${bid}`, problems, schemaVersion);
817
912
  const btimeout = normTimeoutSec(b.timeoutSec, `${where}.body node "${bid}"`, problems);
818
913
  const bschema = normResultSchema(b.resultSchema, `${id}.body.${bid}`, problems);
819
914
  const boverride = normOverride(b.override, `${where}.body node "${bid}"`, problems);
915
+ if (schemaVersion !== 2 && b.outputs !== void 0) {
916
+ problems.push(`${where}.body node "${bid}".outputs requires schemaVersion 2`);
917
+ }
918
+ const boutputs = schemaVersion === 2 ? normalizeArtifactOutputs(b.outputs, `${where}.body node "${bid}"`, problems) : void 0;
820
919
  bodyNodes.push({
821
920
  id: bid,
822
921
  type: "goal",
@@ -825,6 +924,7 @@ function normLoopFields(n, id, problems) {
825
924
  depends: bdepends,
826
925
  override: boverride,
827
926
  inputs: binputs,
927
+ outputs: boutputs,
828
928
  timeoutSec: btimeout,
829
929
  humanGate: null,
830
930
  resultSchema: bschema
@@ -839,6 +939,13 @@ function normLoopFields(n, id, problems) {
839
939
  problems.push(`${where}.body node "${bn.id}".inputs references unknown body node "${inp.from}"`);
840
940
  } else if (!bn.depends.some((d) => d.from === inp.from)) {
841
941
  problems.push(`${where}.body node "${bn.id}".inputs.from "${inp.from}" must also be in depends`);
942
+ } else if (inp.output !== void 0) {
943
+ const source = bodyNodes.find((candidate) => candidate.id === inp.from);
944
+ if (!source?.outputs || !Object.prototype.hasOwnProperty.call(source.outputs, inp.output)) {
945
+ problems.push(
946
+ `${where}.body node "${bn.id}".inputs output ${JSON.stringify(inp.output)} is not declared by source "${inp.from}"`
947
+ );
948
+ }
842
949
  }
843
950
  }
844
951
  }
@@ -1064,7 +1171,7 @@ function normResultSchema(v, id, problems) {
1064
1171
  }
1065
1172
  return schema;
1066
1173
  }
1067
- function normInputs(v, id, problems) {
1174
+ function normInputs(v, id, problems, schemaVersion) {
1068
1175
  if (v === void 0) return [];
1069
1176
  if (!Array.isArray(v)) {
1070
1177
  problems.push(`node "${id}".inputs must be an array`);
@@ -1077,9 +1184,29 @@ function normInputs(v, id, problems) {
1077
1184
  problems.push(`node "${id}".inputs[${j}] must be { from: <nodeId>, select? }`);
1078
1185
  continue;
1079
1186
  }
1080
- const extra = Object.keys(inp).filter((k) => k !== "from" && k !== "select");
1187
+ const extra = Object.keys(inp).filter((k) => k !== "from" && k !== "select" && k !== "output");
1081
1188
  if (extra.length > 0) {
1082
- problems.push(`node "${id}".inputs[${j}] has unsupported key(s): ${extra.join(", ")} (allowed: from, select)`);
1189
+ problems.push(`node "${id}".inputs[${j}] has unsupported key(s): ${extra.join(", ")} (allowed: from, select/output)`);
1190
+ continue;
1191
+ }
1192
+ if (schemaVersion === 2) {
1193
+ if (inp.select !== void 0) {
1194
+ problems.push(`node "${id}".inputs[${j}].select is legacy-only; schemaVersion 2 must use output`);
1195
+ continue;
1196
+ }
1197
+ if (inp.output === void 0) {
1198
+ out.push({ from: inp.from });
1199
+ continue;
1200
+ }
1201
+ if (typeof inp.output !== "string" || !SEGMENT_RE.test(inp.output)) {
1202
+ problems.push(`node "${id}".inputs[${j}].output must be a stable key matching ${SEGMENT_RE}`);
1203
+ continue;
1204
+ }
1205
+ out.push({ from: inp.from, output: inp.output });
1206
+ continue;
1207
+ }
1208
+ if (inp.output !== void 0) {
1209
+ problems.push(`node "${id}".inputs[${j}].output requires schemaVersion 2`);
1083
1210
  continue;
1084
1211
  }
1085
1212
  if (inp.select === void 0) {