@sweny-ai/core 0.1.160 → 0.1.162

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.
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Workflow spec versioning and migrations.
3
+ *
4
+ * Every workflow file carries an optional top-level `spec_version` (a string
5
+ * holding a positive integer, e.g. "1"). A file without the field is version 1:
6
+ * that is what every file written before the field existed means.
7
+ *
8
+ * Breaking spec changes bump CURRENT_SPEC_VERSION and add one entry to
9
+ * MIGRATIONS: a pure function from the raw (unvalidated) vN object to the raw
10
+ * vN+1 object. The loader applies the chain in memory so old files keep
11
+ * running; `sweny workflow upgrade <file>` writes the result back to disk.
12
+ *
13
+ * Adding a migration: see `__tests__/migrations.test.ts` for the harness
14
+ * (registry validity check plus a chain test to copy).
15
+ */
16
+ /** The spec version this build of sweny reads natively. */
17
+ export const CURRENT_SPEC_VERSION = 1;
18
+ /**
19
+ * Ordered registry, one entry per version step. Empty while v1 is current.
20
+ * Append only; never edit a shipped migration.
21
+ */
22
+ export const MIGRATIONS = [];
23
+ export const DEFAULT_MIGRATION_CONFIG = {
24
+ current: CURRENT_SPEC_VERSION,
25
+ migrations: MIGRATIONS,
26
+ };
27
+ /** Throws if a registry is not an unbroken chain of single steps ending at `current`. */
28
+ export function assertRegistryValid(config) {
29
+ const { current, migrations } = config;
30
+ if (!Number.isInteger(current) || current < 1)
31
+ throw new Error("current spec version must be a positive integer");
32
+ if (migrations.length !== current - 1) {
33
+ throw new Error(`expected ${current - 1} migration(s) to reach v${current}, found ${migrations.length}`);
34
+ }
35
+ migrations.forEach((m, i) => {
36
+ if (m.from !== i + 1 || m.to !== i + 2) {
37
+ throw new Error(`migration #${i} must be v${i + 1} -> v${i + 2}, got v${m.from} -> v${m.to}`);
38
+ }
39
+ });
40
+ }
41
+ function parseVersion(value) {
42
+ if (typeof value === "number" && Number.isInteger(value) && value >= 1)
43
+ return value;
44
+ if (typeof value === "string" && /^[1-9]\d*$/.test(value))
45
+ return Number(value);
46
+ return null;
47
+ }
48
+ export function migrateWorkflow(raw, config = DEFAULT_MIGRATION_CONFIG) {
49
+ const declared = "spec_version" in raw && raw.spec_version !== undefined;
50
+ const from = declared ? parseVersion(raw.spec_version) : 1;
51
+ if (from === null) {
52
+ return {
53
+ ok: false,
54
+ code: "INVALID_VERSION",
55
+ message: `spec_version must be a positive integer string like "1", got ${JSON.stringify(raw.spec_version)}`,
56
+ };
57
+ }
58
+ if (from > config.current) {
59
+ return {
60
+ ok: false,
61
+ code: "TOO_NEW",
62
+ message: `This workflow declares spec_version "${from}", but this version of sweny supports up to "${config.current}". ` +
63
+ `Upgrade sweny (run "sweny upgrade", or npm install -g @sweny-ai/core@latest) and try again.`,
64
+ };
65
+ }
66
+ let current = declared ? { ...raw, spec_version: String(from) } : raw;
67
+ const applied = [];
68
+ for (let v = from; v < config.current; v++) {
69
+ const step = config.migrations.find((m) => m.from === v);
70
+ if (!step) {
71
+ return { ok: false, code: "MIGRATION_FAILED", message: `No migration registered from spec_version "${v}".` };
72
+ }
73
+ try {
74
+ current = { ...step.migrate(current), spec_version: String(step.to) };
75
+ }
76
+ catch (err) {
77
+ const msg = err instanceof Error ? err.message : String(err);
78
+ return {
79
+ ok: false,
80
+ code: "MIGRATION_FAILED",
81
+ message: `Migration from spec_version "${step.from}" to "${step.to}" failed: ${msg}`,
82
+ };
83
+ }
84
+ applied.push(step);
85
+ }
86
+ return { ok: true, workflow: current, from, to: config.current, declared, applied };
87
+ }
88
+ /** The single warning the loader emits when it migrated in memory. */
89
+ export function migrationWarning(from, to, filePath) {
90
+ const target = filePath ? `"${filePath}"` : "<file>";
91
+ return (`Workflow uses spec_version "${from}" (current is "${to}"); migrated in memory for this run. ` +
92
+ `Run \`sweny workflow upgrade ${target}\` to update the file.`);
93
+ }
package/dist/schema.d.ts CHANGED
@@ -10,6 +10,7 @@ import { z } from "zod";
10
10
  import type { Workflow, WorkflowType } from "./types.js";
11
11
  import { sourceZ } from "./sources.js";
12
12
  import { workflowInputsZ } from "./inputs.js";
13
+ export { isWhenExpression, whenLabel } from "./when.js";
13
14
  export { sourceZ };
14
15
  export { workflowInputsZ };
15
16
  export declare const jsonSchemaZ: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -1683,17 +1684,27 @@ export declare const nodeZ: z.ZodObject<{
1683
1684
  export declare const edgeZ: z.ZodObject<{
1684
1685
  from: z.ZodString;
1685
1686
  to: z.ZodString;
1686
- when: z.ZodOptional<z.ZodString>;
1687
+ when: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodObject<{
1688
+ expr: z.ZodString;
1689
+ }, "strict", z.ZodTypeAny, {
1690
+ expr: string;
1691
+ }, {
1692
+ expr: string;
1693
+ }>]>>;
1687
1694
  max_iterations: z.ZodOptional<z.ZodNumber>;
1688
1695
  }, "strict", z.ZodTypeAny, {
1689
1696
  from: string;
1690
1697
  to: string;
1691
- when?: string | undefined;
1698
+ when?: string | {
1699
+ expr: string;
1700
+ } | undefined;
1692
1701
  max_iterations?: number | undefined;
1693
1702
  }, {
1694
1703
  from: string;
1695
1704
  to: string;
1696
- when?: string | undefined;
1705
+ when?: string | {
1706
+ expr: string;
1707
+ } | undefined;
1697
1708
  max_iterations?: number | undefined;
1698
1709
  }>;
1699
1710
  /**
@@ -1714,6 +1725,7 @@ export declare const workflowZ: z.ZodObject<{
1714
1725
  id: z.ZodString;
1715
1726
  name: z.ZodString;
1716
1727
  description: z.ZodDefault<z.ZodString>;
1728
+ spec_version: z.ZodOptional<z.ZodString>;
1717
1729
  workflow_type: z.ZodOptional<z.ZodEnum<["pr_review", "e2e_test", "content_generation", "monitor", "data_sync", "generic"]>>;
1718
1730
  nodes: z.ZodRecord<z.ZodString, z.ZodObject<{
1719
1731
  name: z.ZodString;
@@ -2465,17 +2477,27 @@ export declare const workflowZ: z.ZodObject<{
2465
2477
  edges: z.ZodArray<z.ZodObject<{
2466
2478
  from: z.ZodString;
2467
2479
  to: z.ZodString;
2468
- when: z.ZodOptional<z.ZodString>;
2480
+ when: z.ZodOptional<z.ZodUnion<[z.ZodString, z.ZodObject<{
2481
+ expr: z.ZodString;
2482
+ }, "strict", z.ZodTypeAny, {
2483
+ expr: string;
2484
+ }, {
2485
+ expr: string;
2486
+ }>]>>;
2469
2487
  max_iterations: z.ZodOptional<z.ZodNumber>;
2470
2488
  }, "strict", z.ZodTypeAny, {
2471
2489
  from: string;
2472
2490
  to: string;
2473
- when?: string | undefined;
2491
+ when?: string | {
2492
+ expr: string;
2493
+ } | undefined;
2474
2494
  max_iterations?: number | undefined;
2475
2495
  }, {
2476
2496
  from: string;
2477
2497
  to: string;
2478
- when?: string | undefined;
2498
+ when?: string | {
2499
+ expr: string;
2500
+ } | undefined;
2479
2501
  max_iterations?: number | undefined;
2480
2502
  }>, "many">;
2481
2503
  entry: z.ZodString;
@@ -2845,7 +2867,9 @@ export declare const workflowZ: z.ZodObject<{
2845
2867
  edges: {
2846
2868
  from: string;
2847
2869
  to: string;
2848
- when?: string | undefined;
2870
+ when?: string | {
2871
+ expr: string;
2872
+ } | undefined;
2849
2873
  max_iterations?: number | undefined;
2850
2874
  }[];
2851
2875
  entry: string;
@@ -2876,6 +2900,7 @@ export declare const workflowZ: z.ZodObject<{
2876
2900
  type?: string | undefined;
2877
2901
  })[] | undefined;
2878
2902
  judge_model?: string | undefined;
2903
+ spec_version?: string | undefined;
2879
2904
  workflow_type?: "pr_review" | "e2e_test" | "content_generation" | "monitor" | "data_sync" | "generic" | undefined;
2880
2905
  judge_budget?: number | undefined;
2881
2906
  context_mode?: "full" | "bounded" | undefined;
@@ -3019,7 +3044,9 @@ export declare const workflowZ: z.ZodObject<{
3019
3044
  edges: {
3020
3045
  from: string;
3021
3046
  to: string;
3022
- when?: string | undefined;
3047
+ when?: string | {
3048
+ expr: string;
3049
+ } | undefined;
3023
3050
  max_iterations?: number | undefined;
3024
3051
  }[];
3025
3052
  entry: string;
@@ -3064,6 +3091,7 @@ export declare const workflowZ: z.ZodObject<{
3064
3091
  type?: string | undefined;
3065
3092
  })[] | undefined;
3066
3093
  judge_model?: string | undefined;
3094
+ spec_version?: string | undefined;
3067
3095
  workflow_type?: "pr_review" | "e2e_test" | "content_generation" | "monitor" | "data_sync" | "generic" | undefined;
3068
3096
  judge_budget?: number | undefined;
3069
3097
  context_mode?: "full" | "bounded" | undefined;
@@ -3235,7 +3263,9 @@ export declare function parseWorkflow(raw: unknown): {
3235
3263
  edges: {
3236
3264
  from: string;
3237
3265
  to: string;
3238
- when?: string | undefined;
3266
+ when?: string | {
3267
+ expr: string;
3268
+ } | undefined;
3239
3269
  max_iterations?: number | undefined;
3240
3270
  }[];
3241
3271
  entry: string;
@@ -3266,6 +3296,7 @@ export declare function parseWorkflow(raw: unknown): {
3266
3296
  type?: string | undefined;
3267
3297
  })[] | undefined;
3268
3298
  judge_model?: string | undefined;
3299
+ spec_version?: string | undefined;
3269
3300
  workflow_type?: "pr_review" | "e2e_test" | "content_generation" | "monitor" | "data_sync" | "generic" | undefined;
3270
3301
  judge_budget?: number | undefined;
3271
3302
  context_mode?: "full" | "bounded" | undefined;
@@ -3286,7 +3317,7 @@ export declare function parseWorkflow(raw: unknown): {
3286
3317
  } | undefined;
3287
3318
  };
3288
3319
  export interface WorkflowError {
3289
- code: "MISSING_ENTRY" | "UNKNOWN_EDGE_SOURCE" | "UNKNOWN_EDGE_TARGET" | "UNREACHABLE_NODE" | "UNKNOWN_SKILL" | "SELF_LOOP" | "UNBOUNDED_CYCLE" | "AMBIGUOUS_EDGES" | "UNSUPPORTED_EVAL_POLICY" | "INVALID_INLINE_SKILL" | "EDGE_ITERATIONS_EXCEEDED" | "RETRY_MAX_EXCEEDED" | "PERMISSION_CEILING" | "BUDGET_CEILING" | "OUTPUT_NOT_ALLOWED" | "DUPLICATE_OUTPUT" | "UNSUPPORTED_OUTPUT";
3320
+ code: "MISSING_ENTRY" | "UNKNOWN_EDGE_SOURCE" | "UNKNOWN_EDGE_TARGET" | "UNREACHABLE_NODE" | "UNKNOWN_SKILL" | "SELF_LOOP" | "UNBOUNDED_CYCLE" | "AMBIGUOUS_EDGES" | "UNSUPPORTED_EVAL_POLICY" | "INVALID_INLINE_SKILL" | "EDGE_ITERATIONS_EXCEEDED" | "RETRY_MAX_EXCEEDED" | "PERMISSION_CEILING" | "BUDGET_CEILING" | "OUTPUT_NOT_ALLOWED" | "DUPLICATE_OUTPUT" | "UNSUPPORTED_OUTPUT" | "INVALID_WHEN_EXPRESSION" | "MIXED_EDGE_CONDITIONS";
3290
3321
  message: string;
3291
3322
  nodeId?: string;
3292
3323
  }
@@ -3649,6 +3680,11 @@ export declare const workflowJsonSchema: {
3649
3680
  readonly description: {
3650
3681
  readonly type: "string";
3651
3682
  };
3683
+ readonly spec_version: {
3684
+ readonly type: "string";
3685
+ readonly pattern: "^[1-9][0-9]*$";
3686
+ readonly description: "Workflow spec version as a positive integer string. Absent means 1. Older versions are migrated in memory on load; newer than the CLI supports is refused. Run `sweny workflow upgrade <file>` to rewrite a file at the current version.";
3687
+ };
3652
3688
  readonly entry: {
3653
3689
  readonly type: "string";
3654
3690
  readonly minLength: 1;
@@ -4090,8 +4126,22 @@ export declare const workflowJsonSchema: {
4090
4126
  readonly minLength: 1;
4091
4127
  };
4092
4128
  readonly when: {
4093
- readonly type: "string";
4094
- readonly description: "Natural language condition, evaluated at runtime by the workflow's harness.";
4129
+ readonly description: "Condition for taking this edge. A string is natural language, evaluated at runtime by the workflow's harness. An object { expr } is a deterministic expression over prior nodes' declared output fields, evaluated by sweny with no model call.";
4130
+ readonly oneOf: readonly [{
4131
+ readonly type: "string";
4132
+ }, {
4133
+ readonly type: "object";
4134
+ readonly required: readonly ["expr"];
4135
+ readonly additionalProperties: false;
4136
+ readonly properties: {
4137
+ readonly expr: {
4138
+ readonly type: "string";
4139
+ readonly minLength: 1;
4140
+ readonly maxLength: 2000;
4141
+ readonly description: "Boolean expression: == != < <= > >= in, && || !, exists, string/number/boolean/null literals, and node.field paths to declared output fields.";
4142
+ };
4143
+ };
4144
+ }];
4095
4145
  };
4096
4146
  readonly max_iterations: {
4097
4147
  readonly type: "integer";
package/dist/schema.js CHANGED
@@ -10,6 +10,9 @@ import { z } from "zod";
10
10
  import { AUTHOR_ASSOCIATIONS, CONTEXT_MODES, EVALUATOR_KINDS, EVAL_POLICIES, MCP_TRANSPORTS, NODE_ACCESS, NODE_ON_FAIL, REQUIRES_ON_FAIL, SAFE_OUTPUT_APPLIERS, SAFE_OUTPUT_EXPIRES_PATTERN, SAFE_OUTPUT_MAX_CEILING, SAFE_OUTPUT_STATES, SAFE_OUTPUT_TYPES, SKILL_CATEGORIES, SKILL_ID_MAX_LENGTH, SKILL_ID_PATTERN, TOOL_ACCESS, TOOL_CLASSES, WORKFLOW_TYPES, } from "./types.js";
11
11
  import { sourceZ } from "./sources.js";
12
12
  import { workflowInputsZ, WORKFLOW_INPUT_TYPES } from "./inputs.js";
13
+ import { checkExpression, isWhenExpression, WHEN_EXPR_MAX_LENGTH } from "./when.js";
14
+ // Studio and other schema-only consumers label edges without importing the root entry.
15
+ export { isWhenExpression, whenLabel } from "./when.js";
13
16
  export { sourceZ };
14
17
  export { workflowInputsZ };
15
18
  // ─── Zod Schemas ─────────────────────────────────────────────────
@@ -330,7 +333,9 @@ export const edgeZ = z
330
333
  .object({
331
334
  from: z.string().min(1),
332
335
  to: z.string().min(1),
333
- when: z.string().optional(),
336
+ // Natural language (the harness picks the edge) or `{ expr }`, a
337
+ // deterministic expression sweny evaluates itself (#461). One or the other.
338
+ when: z.union([z.string(), z.object({ expr: z.string().min(1).max(WHEN_EXPR_MAX_LENGTH) }).strict()]).optional(),
334
339
  max_iterations: z.number().int().min(1).max(EDGE_MAX_ITERATIONS_CEILING).optional(),
335
340
  })
336
341
  .strict();
@@ -357,6 +362,10 @@ export const workflowZ = z.object({
357
362
  id: z.string().min(1),
358
363
  name: z.string().min(1),
359
364
  description: z.string().default(""),
365
+ spec_version: z
366
+ .string()
367
+ .regex(/^[1-9]\d*$/, "spec_version must be a positive integer string")
368
+ .optional(),
360
369
  workflow_type: workflowTypeZ.optional(),
361
370
  nodes: z.record(nodeZ),
362
371
  edges: z.array(edgeZ),
@@ -503,6 +512,68 @@ export function validateWorkflow(workflow, knownSkills) {
503
512
  });
504
513
  }
505
514
  }
515
+ // Deterministic `when` expressions (#461). Every path must name a declared
516
+ // output field of the edge's source node or one of its ancestors, so a typo
517
+ // is a load error rather than a branch that never fires. A node routes on
518
+ // expressions or on natural language, never both: a mixed node would hand
519
+ // the expression edges to the model and lose the determinism the author
520
+ // asked for.
521
+ const ancestorsCache = new Map();
522
+ const inAll = new Map();
523
+ for (const [from, tos] of outAll)
524
+ for (const to of tos)
525
+ push(inAll, to, from);
526
+ const ancestorsOf = (id) => {
527
+ let seen = ancestorsCache.get(id);
528
+ if (seen)
529
+ return seen;
530
+ seen = new Set([id]);
531
+ const queue = [id];
532
+ for (let i = 0; i < queue.length; i++) {
533
+ for (const from of inAll.get(queue[i]) ?? []) {
534
+ if (!seen.has(from)) {
535
+ seen.add(from);
536
+ queue.push(from);
537
+ }
538
+ }
539
+ }
540
+ ancestorsCache.set(id, seen);
541
+ return seen;
542
+ };
543
+ const outputs = Object.fromEntries(Object.entries(workflow.nodes).map(([id, n]) => [id, n.output]));
544
+ const conditionKinds = new Map();
545
+ for (const edge of workflow.edges) {
546
+ if (!nodeIds.has(edge.from) || !edge.when)
547
+ continue;
548
+ const kinds = conditionKinds.get(edge.from) ?? { expr: 0, nl: 0 };
549
+ conditionKinds.set(edge.from, kinds);
550
+ if (!isWhenExpression(edge.when)) {
551
+ kinds.nl++;
552
+ continue;
553
+ }
554
+ kinds.expr++;
555
+ const problems = checkExpression(edge.when.expr, {
556
+ from: edge.from,
557
+ ancestors: ancestorsOf(edge.from),
558
+ outputs,
559
+ });
560
+ for (const problem of problems) {
561
+ errors.push({
562
+ code: "INVALID_WHEN_EXPRESSION",
563
+ message: `Edge "${edge.from}" -> "${edge.to}": ${problem}`,
564
+ nodeId: edge.from,
565
+ });
566
+ }
567
+ }
568
+ for (const [from, kinds] of conditionKinds) {
569
+ if (kinds.expr > 0 && kinds.nl > 0) {
570
+ errors.push({
571
+ code: "MIXED_EDGE_CONDITIONS",
572
+ message: `Node "${from}" mixes expression and natural-language 'when' conditions on its out-edges; use one kind per node (an unconditional default edge is fine with either)`,
573
+ nodeId: from,
574
+ });
575
+ }
576
+ }
506
577
  // Reachability: BFS from entry. Skipped only when the entry is missing
507
578
  // (every node would be reported, which is noise). All other diagnostics
508
579
  // accumulate so one pass reports every structural problem.
@@ -954,6 +1025,11 @@ export const workflowJsonSchema = {
954
1025
  id: { type: "string", minLength: 1 },
955
1026
  name: { type: "string", minLength: 1 },
956
1027
  description: { type: "string" },
1028
+ spec_version: {
1029
+ type: "string",
1030
+ pattern: "^[1-9][0-9]*$",
1031
+ description: "Workflow spec version as a positive integer string. Absent means 1. Older versions are migrated in memory on load; newer than the CLI supports is refused. Run `sweny workflow upgrade <file>` to rewrite a file at the current version.",
1032
+ },
957
1033
  entry: { type: "string", minLength: 1, description: "ID of the entry node" },
958
1034
  rules: {
959
1035
  type: "array",
@@ -1290,8 +1366,23 @@ export const workflowJsonSchema = {
1290
1366
  from: { type: "string", minLength: 1 },
1291
1367
  to: { type: "string", minLength: 1 },
1292
1368
  when: {
1293
- type: "string",
1294
- description: "Natural language condition, evaluated at runtime by the workflow's harness.",
1369
+ description: "Condition for taking this edge. A string is natural language, evaluated at runtime by the workflow's harness. An object { expr } is a deterministic expression over prior nodes' declared output fields, evaluated by sweny with no model call.",
1370
+ oneOf: [
1371
+ { type: "string" },
1372
+ {
1373
+ type: "object",
1374
+ required: ["expr"],
1375
+ additionalProperties: false,
1376
+ properties: {
1377
+ expr: {
1378
+ type: "string",
1379
+ minLength: 1,
1380
+ maxLength: WHEN_EXPR_MAX_LENGTH,
1381
+ description: "Boolean expression: == != < <= > >= in, && || !, exists, string/number/boolean/null literals, and node.field paths to declared output fields.",
1382
+ },
1383
+ },
1384
+ },
1385
+ ],
1295
1386
  },
1296
1387
  max_iterations: {
1297
1388
  type: "integer",
package/dist/studio.d.ts CHANGED
@@ -38,7 +38,7 @@
38
38
  * directly to node highlights. node:enter → glow blue,
39
39
  * tool:call → show tool name, node:exit → green/red.
40
40
  */
41
- import type { Workflow, Node, ExecutionEvent, NodeResult, Skill } from "./types.js";
41
+ import type { Workflow, Node, Edge, ExecutionEvent, NodeResult, Skill } from "./types.js";
42
42
  export interface FlowNode {
43
43
  id: string;
44
44
  type: "skillNode";
@@ -74,7 +74,7 @@ export interface FlowEdge {
74
74
  /** Array index of this edge in workflow.edges — used for stable lookups. */
75
75
  edgeIndex: number;
76
76
  data: {
77
- when?: string;
77
+ when?: Edge["when"];
78
78
  max_iterations?: number;
79
79
  isConditional: boolean;
80
80
  };
package/dist/types.d.ts CHANGED
@@ -489,12 +489,26 @@ export interface NodeToolFilter {
489
489
  /** Skill tool names removed from the node (applied after `allow`). */
490
490
  deny?: string[];
491
491
  }
492
+ /**
493
+ * A deterministic edge condition (#461): a boolean expression over prior
494
+ * nodes' declared output fields, evaluated by sweny with no model call.
495
+ * Grammar and semantics: `when.ts`.
496
+ */
497
+ export interface WhenExpression {
498
+ expr: string;
499
+ }
500
+ /** An edge condition: natural language (model-evaluated) or a `{ expr }` expression. */
501
+ export type EdgeWhen = string | WhenExpression;
492
502
  /** An edge connecting two nodes */
493
503
  export interface Edge {
494
504
  from: string;
495
505
  to: string;
496
- /** Natural language condition, evaluated at runtime by the workflow's harness. */
497
- when?: string;
506
+ /**
507
+ * Condition for taking this edge. A string is natural language, evaluated at
508
+ * runtime by the workflow's harness. `{ expr }` is a deterministic expression
509
+ * evaluated by sweny with no model call.
510
+ */
511
+ when?: EdgeWhen;
498
512
  /** Max times this edge can be followed (enables retry loops). Default: unlimited. */
499
513
  max_iterations?: number;
500
514
  }
@@ -514,6 +528,8 @@ export interface Workflow {
514
528
  id: string;
515
529
  name: string;
516
530
  description: string;
531
+ /** Spec version, a positive integer string (e.g. "1"). Absent means "1". See `migrations.ts`. */
532
+ spec_version?: string;
517
533
  /** Optional. Defaults to "generic" when absent. Required on marketplace templates. */
518
534
  workflow_type?: WorkflowType;
519
535
  nodes: Record<string, Node>;
package/dist/when.d.ts ADDED
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Deterministic `when` expressions (#461).
3
+ *
4
+ * An edge's `when` is either natural language (a model picks the edge at
5
+ * runtime) or `{ expr: "..." }`, a small boolean expression over prior nodes'
6
+ * declared output fields that sweny evaluates itself, with no model call.
7
+ *
8
+ * Grammar (precedence low to high):
9
+ *
10
+ * expr := or
11
+ * or := and ( "||" and )*
12
+ * and := unary ( "&&" unary )*
13
+ * unary := "!" unary | comparison
14
+ * comparison := operand ( ( "==" | "!=" | "<" | "<=" | ">" | ">=" | "in" ) operand )?
15
+ * operand := literal | list | path | "exists" path | "(" expr ")"
16
+ * list := "[" ( literal ( "," literal )* )? "]"
17
+ * literal := number | 'string' | "string" | true | false | null
18
+ * path := ident ( "." ident )+ (node id, then field, then nested fields)
19
+ *
20
+ * Semantics are strict and total. There is no coercion and no truthiness:
21
+ * `&&`, `||`, `!` and the expression itself need booleans; `<` and friends
22
+ * need two numbers or two strings; `in` needs a list on the right. A missing
23
+ * field (undefined, or a node that did not run or did not succeed) or a type
24
+ * mismatch makes the WHOLE expression false and is reported, so a negation
25
+ * can never turn a missing field into a silent true. `exists path` is the
26
+ * one explicit presence test: true when the value is present and not null,
27
+ * never a warning. `&&` and `||` short-circuit, so `exists a.x && a.x > 0`
28
+ * is safe.
29
+ *
30
+ * Pure module: no I/O, no eval, no Function, browser-safe. Strings in the
31
+ * routed data are compared as data only; nothing in them is ever parsed.
32
+ */
33
+ import type { EdgeWhen, JSONSchema, WhenExpression } from "./types.js";
34
+ /** Longest expression source accepted. Routing conditions are short. */
35
+ export declare const WHEN_EXPR_MAX_LENGTH = 2000;
36
+ /** True when an edge `when` is the structured `{ expr }` form. */
37
+ export declare function isWhenExpression(when: unknown): when is WhenExpression;
38
+ /** Human-readable label for a `when`: the NL text or the expression source. */
39
+ export declare function whenLabel(when: EdgeWhen | undefined): string | undefined;
40
+ export type Literal = string | number | boolean | null;
41
+ export type ExprNode = {
42
+ kind: "literal";
43
+ value: Literal;
44
+ } | {
45
+ kind: "list";
46
+ items: Literal[];
47
+ } | {
48
+ kind: "path";
49
+ segments: string[];
50
+ } | {
51
+ kind: "exists";
52
+ path: {
53
+ kind: "path";
54
+ segments: string[];
55
+ };
56
+ } | {
57
+ kind: "not";
58
+ operand: ExprNode;
59
+ } | {
60
+ kind: "and" | "or";
61
+ left: ExprNode;
62
+ right: ExprNode;
63
+ } | {
64
+ kind: "cmp";
65
+ op: CmpOp;
66
+ left: ExprNode;
67
+ right: ExprNode;
68
+ };
69
+ export type CmpOp = "==" | "!=" | "<" | "<=" | ">" | ">=" | "in";
70
+ export declare class ExpressionSyntaxError extends Error {
71
+ readonly position: number;
72
+ constructor(message: string, position: number);
73
+ }
74
+ /**
75
+ * Parse an expression source into an AST. Throws `ExpressionSyntaxError` on
76
+ * any syntax problem, including an empty source, an over-long source, or
77
+ * nesting deeper than the cap.
78
+ */
79
+ export declare function parseExpression(src: string): ExprNode;
80
+ /**
81
+ * Per-node data an expression may read, keyed by node id. A node that did not
82
+ * run or did not succeed is absent, so every path into it reads as missing.
83
+ */
84
+ export type ExpressionScope = Record<string, Record<string, unknown> | undefined>;
85
+ export interface ExpressionResult {
86
+ /** The edge is taken only when this is true. */
87
+ value: boolean;
88
+ /**
89
+ * Why the expression was forced false: a missing field or a type mismatch.
90
+ * Absent when it evaluated normally (to true or false).
91
+ */
92
+ problem?: string;
93
+ }
94
+ /**
95
+ * Evaluate a parsed expression against node outputs. Never throws: a missing
96
+ * field or a type mismatch yields `{ value: false, problem }`.
97
+ */
98
+ export declare function evaluateExpression(ast: ExprNode, scope: ExpressionScope): ExpressionResult;
99
+ /**
100
+ * Check an expression against the workflow it routes. Returns one message per
101
+ * problem, empty when the expression is valid:
102
+ * - the source must parse;
103
+ * - every path's node must be the edge's source or one of its ancestors;
104
+ * - that node must declare `output.properties`, and the field (and any
105
+ * nested field) must be declared there;
106
+ * - a literal compared with a declared field must fit its declared type,
107
+ * and a string literal compared with an enum field must be in the enum;
108
+ * - a field used where a boolean is needed must be declared boolean.
109
+ */
110
+ export declare function checkExpression(src: string, ctx: {
111
+ from: string;
112
+ ancestors: Set<string>;
113
+ outputs: Record<string, JSONSchema | undefined>;
114
+ }): string[];