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.
@@ -106,6 +106,79 @@ function isRecord(value) {
106
106
  return typeof value === "object" && value !== null && !Array.isArray(value);
107
107
  }
108
108
 
109
+ // src/workflows/v3/artifact-contract.ts
110
+ var MANIFEST_FILE_KINDS = [
111
+ "markdown",
112
+ "json",
113
+ "text",
114
+ "code",
115
+ "log",
116
+ "binary",
117
+ "directory"
118
+ ];
119
+ var MANIFEST_SUMMARY_MAX_BYTES = 4 * 1024;
120
+ var MANIFEST_PREVIEW_MAX_BYTES = 4 * 1024;
121
+
122
+ // src/workflows/v3/artifact-contract-declarations.ts
123
+ var V3_ARTIFACT_OUTPUT_KEY_RE = /^[A-Za-z0-9._-]+$/;
124
+ var V3_ARTIFACT_OUTPUT_MAX_COUNT = 32;
125
+ var V3_ARTIFACT_OUTPUT_MAX_BYTES = 4 * 1024;
126
+ function normalizeArtifactOutputs(value, where, problems) {
127
+ if (value === void 0) return void 0;
128
+ if (!isRecord2(value) || Object.keys(value).length === 0) {
129
+ problems.push(`${where}.outputs must be a non-empty object when present`);
130
+ return void 0;
131
+ }
132
+ const entries = Object.entries(value);
133
+ if (entries.length > V3_ARTIFACT_OUTPUT_MAX_COUNT) {
134
+ problems.push(`${where}.outputs has ${entries.length} entries (max ${V3_ARTIFACT_OUTPUT_MAX_COUNT})`);
135
+ }
136
+ if (Buffer.byteLength(JSON.stringify(value), "utf-8") > V3_ARTIFACT_OUTPUT_MAX_BYTES) {
137
+ problems.push(`${where}.outputs exceeds ${V3_ARTIFACT_OUTPUT_MAX_BYTES} serialized bytes`);
138
+ }
139
+ const out = /* @__PURE__ */ Object.create(null);
140
+ const paths = /* @__PURE__ */ new Set();
141
+ for (const [key, raw] of entries) {
142
+ const itemWhere = `${where}.outputs.${JSON.stringify(key)}`;
143
+ if (!V3_ARTIFACT_OUTPUT_KEY_RE.test(key)) {
144
+ problems.push(`${itemWhere} key must match ${V3_ARTIFACT_OUTPUT_KEY_RE}`);
145
+ continue;
146
+ }
147
+ if (!isRecord2(raw)) {
148
+ problems.push(`${itemWhere} must be { path, kind }`);
149
+ continue;
150
+ }
151
+ const extra = Object.keys(raw).filter((field) => field !== "path" && field !== "kind");
152
+ if (extra.length > 0) {
153
+ problems.push(`${itemWhere} has unsupported key(s): ${extra.join(", ")} (allowed: path, kind)`);
154
+ continue;
155
+ }
156
+ if (!isPortableRelativeArtifactPath(raw.path)) {
157
+ problems.push(`${itemWhere}.path must be a portable relative path without '.', '..', or empty segments`);
158
+ continue;
159
+ }
160
+ if (typeof raw.kind !== "string" || !MANIFEST_FILE_KINDS.includes(raw.kind)) {
161
+ problems.push(`${itemWhere}.kind must be one of ${MANIFEST_FILE_KINDS.join(" | ")}`);
162
+ continue;
163
+ }
164
+ if (paths.has(raw.path)) {
165
+ problems.push(`${where}.outputs contains duplicate path ${JSON.stringify(raw.path)}`);
166
+ continue;
167
+ }
168
+ paths.add(raw.path);
169
+ out[key] = { path: raw.path, kind: raw.kind };
170
+ }
171
+ return out;
172
+ }
173
+ function isPortableRelativeArtifactPath(value) {
174
+ if (typeof value !== "string" || value.length === 0 || value.startsWith("/") || value.includes("\\") || value.includes("\0") || /^[A-Za-z]:/.test(value)) return false;
175
+ const segments = value.split("/");
176
+ return segments.every((segment) => segment !== "" && segment !== "." && segment !== "..");
177
+ }
178
+ function isRecord2(value) {
179
+ return typeof value === "object" && value !== null && !Array.isArray(value);
180
+ }
181
+
109
182
  // src/workflows/v3/dag.ts
110
183
  var NODE_KINDS = ["goal", "host", "loop"];
111
184
  var V3_HOST_EXECUTORS = ["feishu-send", "feishu-reply", "botmux-schedule"];
@@ -156,6 +229,10 @@ function validateDag(raw) {
156
229
  if (!isObject(raw)) {
157
230
  throw new DagValidationError(["root must be a JSON object"]);
158
231
  }
232
+ const schemaVersion = raw.schemaVersion === void 0 ? 1 : raw.schemaVersion;
233
+ if (schemaVersion !== 1 && schemaVersion !== 2) {
234
+ problems.push(`schemaVersion must be 1 or 2 (got ${JSON.stringify(raw.schemaVersion)})`);
235
+ }
159
236
  if (typeof raw.runId !== "string" || !SEGMENT_RE.test(raw.runId)) {
160
237
  problems.push(`runId must be a path-safe string matching ${SEGMENT_RE} (got ${JSON.stringify(raw.runId)})`);
161
238
  }
@@ -192,9 +269,9 @@ function validateDag(raw) {
192
269
  if (fromList.includes(id)) problems.push(`node "${id}" depends on itself`);
193
270
  if (new Set(fromList).size !== fromList.length) problems.push(`node "${id}".depends has duplicates`);
194
271
  const triggerRule = normTriggerRule(n.triggerRule, depends.length, `node "${id}"`, problems);
195
- const inputs = normInputs(n.inputs, id, problems);
272
+ const inputs = normInputs(n.inputs, id, problems, schemaVersion === 2 ? 2 : 1);
196
273
  if (type === "loop") {
197
- const loopFields = normLoopFields(n, id, problems);
274
+ const loopFields = normLoopFields(n, id, problems, schemaVersion === 2 ? 2 : 1);
198
275
  if (loopFields) {
199
276
  nodes.push({
200
277
  id,
@@ -211,6 +288,7 @@ function validateDag(raw) {
211
288
  continue;
212
289
  }
213
290
  if (type === "host") {
291
+ if (n.outputs !== void 0) problems.push(`host node "${id}".outputs is not supported`);
214
292
  if (n.goal !== void 0) problems.push(`host node "${id}".goal is not supported`);
215
293
  if (n.bot !== void 0) problems.push(`host node "${id}".bot is not supported \u2014 host nodes do not spawn a CLI`);
216
294
  if (n.override !== void 0) problems.push(`host node "${id}".override is not supported`);
@@ -286,6 +364,10 @@ function validateDag(raw) {
286
364
  const humanGate = normHumanGate(n.humanGate, `node "${id}"`, problems);
287
365
  const override = normOverride(n.override, `node "${id}"`, problems);
288
366
  const revisitTo = normRevisitTo(n.revisitTo, id, problems);
367
+ if (schemaVersion !== 2 && n.outputs !== void 0) {
368
+ problems.push(`goal node "${id}".outputs requires schemaVersion 2`);
369
+ }
370
+ const outputs = schemaVersion === 2 ? normalizeArtifactOutputs(n.outputs, `goal node "${id}"`, problems) : void 0;
289
371
  nodes.push({
290
372
  id,
291
373
  type,
@@ -295,6 +377,7 @@ function validateDag(raw) {
295
377
  triggerRule,
296
378
  override,
297
379
  inputs,
380
+ outputs,
298
381
  timeoutSec,
299
382
  humanGate,
300
383
  resultSchema,
@@ -310,6 +393,14 @@ function validateDag(raw) {
310
393
  problems.push(`node "${node.id}".inputs references unknown node "${inp.from}"`);
311
394
  } else if (!node.depends.some((d) => d.from === inp.from)) {
312
395
  problems.push(`node "${node.id}".inputs.from "${inp.from}" must also be in depends`);
396
+ } else if (inp.output !== void 0) {
397
+ const source = nodes.find((candidate) => candidate.id === inp.from);
398
+ const outputs = source?.type === "loop" ? source.body?.nodes.find((bodyNode) => bodyNode.id === source.output?.from)?.outputs : source?.outputs;
399
+ if (!outputs || !Object.prototype.hasOwnProperty.call(outputs, inp.output)) {
400
+ problems.push(
401
+ `node "${node.id}".inputs output ${JSON.stringify(inp.output)} is not declared by source "${inp.from}"`
402
+ );
403
+ }
313
404
  }
314
405
  }
315
406
  if (node.type === "host") {
@@ -388,7 +479,11 @@ function validateDag(raw) {
388
479
  }
389
480
  }
390
481
  if (problems.length > 0) throw new DagValidationError(problems);
391
- const dag = { runId: raw.runId, nodes };
482
+ const dag = {
483
+ ...raw.schemaVersion !== void 0 ? { schemaVersion } : {},
484
+ runId: raw.runId,
485
+ nodes
486
+ };
392
487
  topologicalOrder(dag);
393
488
  return dag;
394
489
  }
@@ -702,7 +797,7 @@ function downstreamCone(nodeId, nodes) {
702
797
  function nodeByIdUnsafe(nodes, nodeId) {
703
798
  return nodes.find((node) => node.id === nodeId);
704
799
  }
705
- function normLoopFields(n, id, problems) {
800
+ function normLoopFields(n, id, problems, schemaVersion) {
706
801
  const where = `loop node "${id}"`;
707
802
  const before = problems.length;
708
803
  if (n.timeoutSec !== void 0) {
@@ -773,10 +868,14 @@ function normLoopFields(n, id, problems) {
773
868
  const bFromList = bdepends.map((d) => d.from);
774
869
  if (bFromList.includes(bid)) problems.push(`${where}.body node "${bid}" depends on itself`);
775
870
  if (new Set(bFromList).size !== bFromList.length) problems.push(`${where}.body node "${bid}".depends has duplicates`);
776
- const binputs = normInputs(b.inputs, `${id}.body.${bid}`, problems);
871
+ const binputs = normInputs(b.inputs, `${id}.body.${bid}`, problems, schemaVersion);
777
872
  const btimeout = normTimeoutSec(b.timeoutSec, `${where}.body node "${bid}"`, problems);
778
873
  const bschema = normResultSchema(b.resultSchema, `${id}.body.${bid}`, problems);
779
874
  const boverride = normOverride(b.override, `${where}.body node "${bid}"`, problems);
875
+ if (schemaVersion !== 2 && b.outputs !== void 0) {
876
+ problems.push(`${where}.body node "${bid}".outputs requires schemaVersion 2`);
877
+ }
878
+ const boutputs = schemaVersion === 2 ? normalizeArtifactOutputs(b.outputs, `${where}.body node "${bid}"`, problems) : void 0;
780
879
  bodyNodes.push({
781
880
  id: bid,
782
881
  type: "goal",
@@ -785,6 +884,7 @@ function normLoopFields(n, id, problems) {
785
884
  depends: bdepends,
786
885
  override: boverride,
787
886
  inputs: binputs,
887
+ outputs: boutputs,
788
888
  timeoutSec: btimeout,
789
889
  humanGate: null,
790
890
  resultSchema: bschema
@@ -799,6 +899,13 @@ function normLoopFields(n, id, problems) {
799
899
  problems.push(`${where}.body node "${bn.id}".inputs references unknown body node "${inp.from}"`);
800
900
  } else if (!bn.depends.some((d) => d.from === inp.from)) {
801
901
  problems.push(`${where}.body node "${bn.id}".inputs.from "${inp.from}" must also be in depends`);
902
+ } else if (inp.output !== void 0) {
903
+ const source = bodyNodes.find((candidate) => candidate.id === inp.from);
904
+ if (!source?.outputs || !Object.prototype.hasOwnProperty.call(source.outputs, inp.output)) {
905
+ problems.push(
906
+ `${where}.body node "${bn.id}".inputs output ${JSON.stringify(inp.output)} is not declared by source "${inp.from}"`
907
+ );
908
+ }
802
909
  }
803
910
  }
804
911
  }
@@ -1024,7 +1131,7 @@ function normResultSchema(v, id, problems) {
1024
1131
  }
1025
1132
  return schema;
1026
1133
  }
1027
- function normInputs(v, id, problems) {
1134
+ function normInputs(v, id, problems, schemaVersion) {
1028
1135
  if (v === void 0) return [];
1029
1136
  if (!Array.isArray(v)) {
1030
1137
  problems.push(`node "${id}".inputs must be an array`);
@@ -1037,9 +1144,29 @@ function normInputs(v, id, problems) {
1037
1144
  problems.push(`node "${id}".inputs[${j}] must be { from: <nodeId>, select? }`);
1038
1145
  continue;
1039
1146
  }
1040
- const extra = Object.keys(inp).filter((k) => k !== "from" && k !== "select");
1147
+ const extra = Object.keys(inp).filter((k) => k !== "from" && k !== "select" && k !== "output");
1041
1148
  if (extra.length > 0) {
1042
- problems.push(`node "${id}".inputs[${j}] has unsupported key(s): ${extra.join(", ")} (allowed: from, select)`);
1149
+ problems.push(`node "${id}".inputs[${j}] has unsupported key(s): ${extra.join(", ")} (allowed: from, select/output)`);
1150
+ continue;
1151
+ }
1152
+ if (schemaVersion === 2) {
1153
+ if (inp.select !== void 0) {
1154
+ problems.push(`node "${id}".inputs[${j}].select is legacy-only; schemaVersion 2 must use output`);
1155
+ continue;
1156
+ }
1157
+ if (inp.output === void 0) {
1158
+ out.push({ from: inp.from });
1159
+ continue;
1160
+ }
1161
+ if (typeof inp.output !== "string" || !SEGMENT_RE.test(inp.output)) {
1162
+ problems.push(`node "${id}".inputs[${j}].output must be a stable key matching ${SEGMENT_RE}`);
1163
+ continue;
1164
+ }
1165
+ out.push({ from: inp.from, output: inp.output });
1166
+ continue;
1167
+ }
1168
+ if (inp.output !== void 0) {
1169
+ problems.push(`node "${id}".inputs[${j}].output requires schemaVersion 2`);
1043
1170
  continue;
1044
1171
  }
1045
1172
  if (inp.select === void 0) {