@xemahq/dsl 0.5.6 → 0.5.8

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.
Files changed (72) hide show
  1. package/dist/deliverable-spec/lib/schema.d.ts.map +1 -1
  2. package/dist/deliverable-spec/lib/schema.js.map +1 -1
  3. package/dist/payload-codec/index.d.ts +1 -0
  4. package/dist/payload-codec/index.d.ts.map +1 -1
  5. package/dist/payload-codec/index.js.map +1 -1
  6. package/dist/payload-codec/lib/codec.d.ts +1 -1
  7. package/dist/payload-codec/lib/codec.d.ts.map +1 -1
  8. package/dist/payload-codec/lib/codec.js +12 -1
  9. package/dist/payload-codec/lib/codec.js.map +1 -1
  10. package/dist/payload-codec/lib/payload.d.ts +9 -0
  11. package/dist/payload-codec/lib/payload.d.ts.map +1 -0
  12. package/dist/payload-codec/lib/payload.js +3 -0
  13. package/dist/payload-codec/lib/payload.js.map +1 -0
  14. package/dist/payload-codec/temporal/index.d.ts +5 -0
  15. package/dist/payload-codec/temporal/index.d.ts.map +1 -0
  16. package/dist/payload-codec/temporal/index.js +7 -0
  17. package/dist/payload-codec/temporal/index.js.map +1 -0
  18. package/dist/schema/workflow.schema.json +29 -2
  19. package/dist/workflow/index.d.ts +2 -2
  20. package/dist/workflow/index.d.ts.map +1 -1
  21. package/dist/workflow/index.js +3 -1
  22. package/dist/workflow/index.js.map +1 -1
  23. package/dist/workflow/lib/compiler/canonical-json.js +15 -4
  24. package/dist/workflow/lib/compiler/canonical-json.js.map +1 -1
  25. package/dist/workflow/lib/compiler/compile.d.ts.map +1 -1
  26. package/dist/workflow/lib/compiler/compile.js +3 -1
  27. package/dist/workflow/lib/compiler/compile.js.map +1 -1
  28. package/dist/workflow/lib/compiler/gate-defaults.d.ts +5 -0
  29. package/dist/workflow/lib/compiler/gate-defaults.d.ts.map +1 -0
  30. package/dist/workflow/lib/compiler/gate-defaults.js +76 -0
  31. package/dist/workflow/lib/compiler/gate-defaults.js.map +1 -0
  32. package/dist/workflow/lib/compiler/index.d.ts +1 -0
  33. package/dist/workflow/lib/compiler/index.d.ts.map +1 -1
  34. package/dist/workflow/lib/compiler/index.js +4 -1
  35. package/dist/workflow/lib/compiler/index.js.map +1 -1
  36. package/dist/workflow/lib/duration.d.ts.map +1 -1
  37. package/dist/workflow/lib/duration.js +3 -9
  38. package/dist/workflow/lib/duration.js.map +1 -1
  39. package/dist/workflow/lib/types.d.ts +12 -3
  40. package/dist/workflow/lib/types.d.ts.map +1 -1
  41. package/dist/workspace-manifest/lib/compile.js.map +1 -1
  42. package/dist/workspace-manifest/lib/interpolate.js +26 -4
  43. package/dist/workspace-manifest/lib/interpolate.js.map +1 -1
  44. package/package.json +28 -5
  45. package/schema/workflow.schema.json +29 -2
  46. package/src/deliverable-spec/index.ts +1 -1
  47. package/src/deliverable-spec/lib/schema.ts +1 -2
  48. package/src/payload-codec/index.ts +8 -4
  49. package/src/payload-codec/lib/codec.ts +14 -2
  50. package/src/payload-codec/lib/payload.ts +26 -0
  51. package/src/payload-codec/temporal/index.ts +36 -0
  52. package/src/workflow/index.ts +3 -0
  53. package/src/workflow/lib/compiler/canonical-json.ts +17 -4
  54. package/src/workflow/lib/compiler/compile.ts +14 -7
  55. package/src/workflow/lib/compiler/gate-defaults.ts +153 -0
  56. package/src/workflow/lib/compiler/index.ts +1 -0
  57. package/src/workflow/lib/compiler/inputs.ts +1 -1
  58. package/src/workflow/lib/compiler/payload-reach-in.ts +6 -6
  59. package/src/workflow/lib/compiler/review-step.ts +3 -3
  60. package/src/workflow/lib/duration.ts +5 -10
  61. package/src/workflow/lib/expression/context.ts +2 -2
  62. package/src/workflow/lib/expression/evaluator.ts +2 -2
  63. package/src/workflow/lib/expression/walk-artifact-refs.ts +2 -2
  64. package/src/workflow/lib/installation-resource-kind.ts +1 -1
  65. package/src/workflow/lib/serializer.ts +1 -1
  66. package/src/workflow/lib/types.ts +33 -7
  67. package/src/workflow/lib/validate.ts +2 -2
  68. package/src/workspace-manifest/lib/compile.ts +1 -2
  69. package/src/workspace-manifest/lib/interpolate.ts +29 -3
  70. package/src/workspace-manifest/lib/resolve-extends.ts +3 -3
  71. package/src/workspace-manifest/lib/schema.ts +2 -2
  72. package/src/workspace-manifest/lib/types.ts +3 -3
@@ -57,6 +57,8 @@ export {
57
57
  export { dispatchInputsToJsonSchema } from './lib/dispatch-inputs/to-json-schema';
58
58
  export { serializeWorkflowDocument } from './lib/serializer';
59
59
  export {
60
+ DECISION_GATE_ACTION_ID,
61
+ applyGateDefaults,
60
62
  canonicalJsonSha256,
61
63
  canonicalJsonStringify,
62
64
  compileManifestSource,
@@ -85,6 +87,7 @@ export type {
85
87
  WorkflowCallDeclaration,
86
88
  WorkflowConcurrencyDeclaration,
87
89
  WorkflowDefaults,
90
+ WorkflowGateDefaults,
88
91
  WorkflowDispatchDeclaration,
89
92
  WorkflowDocument,
90
93
  WorkflowDynamicStrategyDeclaration,
@@ -14,7 +14,7 @@ import { createHash } from 'node:crypto';
14
14
  * - Symbols / functions are forbidden (throws).
15
15
  */
16
16
  export function canonicalJsonStringify(value: unknown): string {
17
- return stringify(value);
17
+ return stringify(value, new Set());
18
18
  }
19
19
 
20
20
  /** Convenience: canonical stringify + sha256 hex. */
@@ -22,7 +22,7 @@ export function canonicalJsonSha256(value: unknown): string {
22
22
  return createHash('sha256').update(canonicalJsonStringify(value), 'utf8').digest('hex');
23
23
  }
24
24
 
25
- function stringify(value: unknown): string {
25
+ function stringify(value: unknown, seen: Set<object>): string {
26
26
  if (value === null) return 'null';
27
27
  if (value === undefined) {
28
28
  throw new Error('canonicalJsonStringify: undefined is not serializable');
@@ -36,17 +36,30 @@ function stringify(value: unknown): string {
36
36
  }
37
37
  if (typeof value === 'boolean') return value ? 'true' : 'false';
38
38
  if (Array.isArray(value)) {
39
- return `[${value.map(stringify).join(',')}]`;
39
+ // Guard against circular references (e.g. cyclic YAML anchors) so a
40
+ // self-referential input yields a clear error instead of a stack overflow.
41
+ if (seen.has(value)) {
42
+ throw new Error('canonicalJsonStringify: circular reference is not serializable');
43
+ }
44
+ seen.add(value);
45
+ const out = `[${value.map((child) => stringify(child, seen)).join(',')}]`;
46
+ seen.delete(value);
47
+ return out;
40
48
  }
41
49
  if (typeof value === 'object') {
50
+ if (seen.has(value)) {
51
+ throw new Error('canonicalJsonStringify: circular reference is not serializable');
52
+ }
53
+ seen.add(value);
42
54
  const record = value as Record<string, unknown>;
43
55
  const keys = Object.keys(record).sort();
44
56
  const entries: string[] = [];
45
57
  for (const key of keys) {
46
58
  const child = record[key];
47
59
  if (child === undefined) continue; // drop undefined; do not emit
48
- entries.push(`${JSON.stringify(key)}:${stringify(child)}`);
60
+ entries.push(`${JSON.stringify(key)}:${stringify(child, seen)}`);
49
61
  }
62
+ seen.delete(value);
50
63
  return `{${entries.join(',')}}`;
51
64
  }
52
65
  throw new Error(`canonicalJsonStringify: unsupported type ${typeof value}`);
@@ -25,6 +25,7 @@ import {
25
25
  validateConcurrencyGroupTemplate,
26
26
  } from './concurrency';
27
27
  import { topologicalSort } from './dag';
28
+ import { applyGateDefaults } from './gate-defaults';
28
29
  import { bindTriggerInputs } from './inputs';
29
30
  import { compileStrategy } from './matrix';
30
31
  import { compileManifestSource } from './manifest-source';
@@ -74,7 +75,13 @@ import type {
74
75
  * 8. Emit CompiledRun with a deterministic sha256.
75
76
  */
76
77
  export function compileWorkflow(input: CompileInput): CompiledRun {
77
- const { workflow, workflowRef, trigger, resolvedRefs, workflowDefinitionVersionSha256 } = input;
78
+ const { workflowRef, trigger, resolvedRefs, workflowDefinitionVersionSha256 } = input;
79
+
80
+ // Fold `defaults.gate` into every decision-gate job's `with:` BEFORE any
81
+ // other pass — expression validation, needs-shape validation, and the
82
+ // snapshot all observe the merged document, so defaults behave exactly
83
+ // like inline-authored values. Explicit per-gate keys always win.
84
+ const workflow = applyGateDefaults(input.workflow, resolvedRefs);
78
85
 
79
86
  const inputs = bindTriggerInputs(workflow, trigger, input.previewMode ?? false);
80
87
  const vars: Readonly<Record<string, unknown>> = workflow.vars ?? {};
@@ -156,7 +163,7 @@ export function compileWorkflow(input: CompileInput): CompiledRun {
156
163
  // shape).
157
164
  validateDeliverableValueExpressions(workflow, input.resolvedDeliverableSpecs ?? {});
158
165
 
159
- // §C.1 payload reach-in validation: `needs.<X>.outputs.<name>.<field>`
166
+ // Payload reach-in validation: `needs.<X>.outputs.<name>.<field>`
160
167
  // chains where `<field>` is not an ArtifactRef envelope field reach
161
168
  // INTO the artifact's parsed payload. The validator asserts the
162
169
  // upstream job has a schema-bearing `with.deliverableSpecRef` and
@@ -235,11 +242,11 @@ export function compileWorkflow(input: CompileInput): CompiledRun {
235
242
  );
236
243
 
237
244
  // Validate `if` expression shape now — runtime evaluator only fails on
238
- // unknown bindings after this point. Plan §G shorthand: rewrite a
245
+ // unknown bindings after this point. Shorthand: rewrite a
239
246
  // bare `needs.<X>` reference to `needs.<X>.outcome == 'ok'` so the
240
247
  // evaluator sees the explicit success check (an envelope object is
241
- // truthy even on failure — silent always-true is the trap §G
242
- // explicitly avoids).
248
+ // truthy even on failure — silent always-true is the trap this
249
+ // rewrite explicitly avoids).
243
250
  const ifExpression = payload.if !== undefined
244
251
  ? (() => {
245
252
  const body = stripInterpolation(payload.if!);
@@ -260,7 +267,7 @@ export function compileWorkflow(input: CompileInput): CompiledRun {
260
267
  // `x-installation-resource` field in the `with:` block and confirm
261
268
  // its value is bound to the calling installation. Rejects unbound
262
269
  // walletIds (etc.) BEFORE the dispatch creates a run, instead of
263
- // failing 3 activities later when integration-adapters refuses to
270
+ // failing 3 activities later when connector-gateway-api refuses to
264
271
  // mint credentials. Skips entirely for system / org-wide dispatches.
265
272
  validateInstallationResourceBindings({
266
273
  jobKey: key,
@@ -397,7 +404,7 @@ function assertNoVarsBriefcaseCollision(
397
404
  * safe with this compiler's closed enum.
398
405
  */
399
406
  /**
400
- * Plan §G shorthand: rewrite a bare `needs.<X>` reference at the top
407
+ * Shorthand: rewrite a bare `needs.<X>` reference at the top
401
408
  * level of an `if:` expression to `needs.<X>.outcome == 'ok'`. Authors
402
409
  * write `if: needs.draft` to mean "run if draft succeeded"; without
403
410
  * this rewrite the evaluator sees the upstream's envelope object
@@ -0,0 +1,153 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // ── Workflow-level decision-gate defaults (`defaults.gate`) ──
3
+ //
4
+ // Gate-heavy workflows (e.g. the software-dev product-development pipeline)
5
+ // repeat the same decision-gate configuration on every gate job:
6
+ // timeoutSeconds, onTimeout, autonomyMode, recipients, policy. `defaults.gate`
7
+ // lets the author declare that configuration ONCE at the workflow level; the
8
+ // compiler folds it into each gate job's `with:` before any other compile
9
+ // pass runs, so downstream validation, expression compilation, and the
10
+ // CompiledRun snapshot all observe the merged document — expressions inside
11
+ // defaults compile per-job exactly as if written inline.
12
+ //
13
+ // Merge contract (deterministic, no silent deep-merging):
14
+ // • Applies ONLY to jobs whose `uses:` resolves to the `xema/decision-gate`
15
+ // action id — matched on the resolved ref's id, regardless of version.
16
+ // • Fills ABSENT `with:` keys only. Explicit per-gate values always win.
17
+ // • Fill is per top-level key: a gate that declares its own `policy` or
18
+ // `recipients` keeps that value verbatim — there is no partial merge
19
+ // inside a key's object/array value.
20
+ // • Fail-fast validation: every `defaults.gate` key must be declared in
21
+ // the resolved gate manifest's `inputs.properties` (guards drift between
22
+ // the DSL schema's closed key set and the pinned gate action version).
23
+ // ═══════════════════════════════════════════════════════════════════════════
24
+
25
+ import { WorkflowErrorCode } from '@xemahq/kernel-contracts/workflow';
26
+ import { WorkflowDslError } from '../errors';
27
+ import { compileExpression, extractInterpolations } from '../expression';
28
+ import type { WorkflowDocument, WorkflowGateDefaults } from '../types';
29
+ import type { ResolvedRef } from './types';
30
+
31
+ /**
32
+ * Canonical action id of the decision-gate primitive. `defaults.gate`
33
+ * targets jobs by this id — never by the raw `uses:` string — so any
34
+ * pinned version (`xema/decision-gate@1.2.0`, `@2.x`, …) participates.
35
+ */
36
+ export const DECISION_GATE_ACTION_ID = 'xema/decision-gate';
37
+
38
+ /**
39
+ * Fold `defaults.gate` into every decision-gate job's `with:` block and
40
+ * return the (possibly rewritten) document. Pure: never mutates the input.
41
+ *
42
+ * Runs BEFORE all other compile passes so the merged `with:` flows through
43
+ * expression validation, needs-shape validation, review rewriting, the
44
+ * action input-schema pin, and the snapshot hash exactly like authored
45
+ * values.
46
+ *
47
+ * Jobs whose `uses:` is missing from `resolvedRefs` are skipped here — the
48
+ * per-job compile loop already fails them with `DSL_UNKNOWN_ACTION`, and
49
+ * pre-empting that error would reorder author-facing diagnostics.
50
+ */
51
+ export function applyGateDefaults(
52
+ workflow: WorkflowDocument,
53
+ resolvedRefs: Readonly<Record<string, ResolvedRef>>,
54
+ ): WorkflowDocument {
55
+ const gate = workflow.defaults?.gate;
56
+ if (gate === undefined) return workflow;
57
+ const gateEntries = Object.entries(gate).filter(
58
+ ([, value]) => value !== undefined,
59
+ );
60
+ if (gateEntries.length === 0) return workflow;
61
+
62
+ // Validate every `${{ … }}` expression inside the defaults block up
63
+ // front — even when no gate job consumes it. A malformed expression in
64
+ // `defaults.gate` is an authoring error regardless of merge fan-out;
65
+ // deferring it until a gate job appears would be a silent latency.
66
+ for (const ext of extractInterpolations(
67
+ gate as Readonly<Record<string, unknown>>,
68
+ ['defaults', 'gate'],
69
+ )) {
70
+ compileExpression(ext.source);
71
+ }
72
+
73
+ let anyChanged = false;
74
+ const rewrittenJobs: Record<string, WorkflowDocument['jobs'][string]> = {};
75
+ for (const [jobKey, job] of Object.entries(workflow.jobs)) {
76
+ const resolved = resolvedRefs[job.uses];
77
+ if (
78
+ resolved === undefined ||
79
+ resolved.isReusableWorkflow ||
80
+ resolved.id !== DECISION_GATE_ACTION_ID
81
+ ) {
82
+ rewrittenJobs[jobKey] = job;
83
+ continue;
84
+ }
85
+
86
+ assertGateDefaultKeysDeclared(jobKey, gate, resolved);
87
+
88
+ const authored = job.with ?? {};
89
+ let jobChanged = false;
90
+ const merged: Record<string, unknown> = { ...authored };
91
+ for (const [key, value] of gateEntries) {
92
+ if (Object.hasOwn(authored, key)) continue;
93
+ merged[key] = value;
94
+ jobChanged = true;
95
+ }
96
+ if (!jobChanged) {
97
+ rewrittenJobs[jobKey] = job;
98
+ continue;
99
+ }
100
+ anyChanged = true;
101
+ rewrittenJobs[jobKey] = { ...job, with: merged };
102
+ }
103
+
104
+ if (!anyChanged) return workflow;
105
+ return { ...workflow, jobs: rewrittenJobs };
106
+ }
107
+
108
+ /**
109
+ * Fail-fast subset check: every `defaults.gate` key must be a declared
110
+ * input of the resolved gate action version. The JSON schema already
111
+ * closes the key set to the canonical decision-gate input names; this
112
+ * check guards manifest drift (e.g. a future gate version renaming or
113
+ * dropping an input while a workflow still defaults it).
114
+ *
115
+ * Skipped only when the manifest declares no introspectable
116
+ * `inputs.properties` map — nothing to prove against; the worker's
117
+ * pinned input-schema validation still rejects unknown keys at dispatch.
118
+ */
119
+ function assertGateDefaultKeysDeclared(
120
+ jobKey: string,
121
+ gate: WorkflowGateDefaults,
122
+ resolved: ResolvedRef,
123
+ ): void {
124
+ const inputs = resolved.actionManifest?.spec.inputs;
125
+ const properties =
126
+ inputs && typeof inputs === 'object'
127
+ ? (inputs as { properties?: unknown }).properties
128
+ : undefined;
129
+ if (
130
+ properties === null ||
131
+ properties === undefined ||
132
+ typeof properties !== 'object' ||
133
+ Array.isArray(properties)
134
+ ) {
135
+ return;
136
+ }
137
+ const declared = properties as Readonly<Record<string, unknown>>;
138
+ for (const [key, value] of Object.entries(gate)) {
139
+ if (value === undefined) continue;
140
+ if (Object.hasOwn(declared, key)) continue;
141
+ throw new WorkflowDslError(
142
+ WorkflowErrorCode.DSL_SEMANTIC_INVALID,
143
+ `defaults.gate.${key} is not a declared input of ${resolved.id}@${resolved.version} (consumed by job '${jobKey}'). Declared inputs: [${Object.keys(declared).sort((a, b) => a.localeCompare(b)).join(', ')}].`,
144
+ {
145
+ jobKey,
146
+ key,
147
+ actionId: resolved.id,
148
+ actionVersion: resolved.version,
149
+ declaredInputs: Object.keys(declared),
150
+ },
151
+ );
152
+ }
153
+ }
@@ -1,4 +1,5 @@
1
1
  export { compileWorkflow } from './compile';
2
+ export { DECISION_GATE_ACTION_ID, applyGateDefaults } from './gate-defaults';
2
3
  export { canonicalJsonSha256, canonicalJsonStringify } from './canonical-json';
3
4
  export { compileManifestSource } from './manifest-source';
4
5
  export { getWorkspaceManifestContract, isAgentShapedAction } from './action-shape';
@@ -60,7 +60,7 @@ export function bindTriggerInputs(
60
60
  }
61
61
  case WorkflowTriggerKind.WEBHOOK: {
62
62
  // Webhook does not declare typed inputs at the workflow level —
63
- // validation of the canonical envelope happens at integration-adapters-api.
63
+ // validation of the canonical envelope happens at connector-gateway-api.
64
64
  return trigger.payload;
65
65
  }
66
66
  }
@@ -1,5 +1,5 @@
1
1
  // ═══════════════════════════════════════════════════════════════════════════
2
- // ── Payload reach-in validation (plan §C.1) ──
2
+ // ── Payload reach-in validation ──
3
3
  //
4
4
  // Workflow YAML supports two flavours of `needs.<X>.outputs.<name>.<field>`:
5
5
  //
@@ -74,7 +74,7 @@ function lookupOutputBindingSpecRef(
74
74
  * Decision table for each `<name>.<field>`:
75
75
  * • `<field>` is an ArtifactRef envelope field → OK (no payload fetch).
76
76
  * • Upstream activity manifest declares `outputBindings.<name>.deliverableSpecRef`
77
- * (plan §B per-output binding) → use that spec; otherwise fall back
77
+ * (per-output binding) → use that spec; otherwise fall back
78
78
  * to the upstream job's `with.deliverableSpecRef`. Then:
79
79
  * - resolved spec has `topLevelKeys` and `<field>` is one → OK.
80
80
  * - resolved spec has `topLevelKeys` and `<field>` is NOT one →
@@ -136,7 +136,7 @@ export function validatePayloadReachIns(
136
136
  /**
137
137
  * Walk every reach-in for a single job and emit one pin per
138
138
  * `(upstreamJobKey, specRef)` pair. Pins drive the runtime spec-version
139
- * assertion (plan §C.1 risks-and-mitigations) — the worker pre-fetches
139
+ * assertion — the worker pre-fetches
140
140
  * the artifact and refuses to proceed when its `schemaVersion` doesn't
141
141
  * match the pin's `specVersion`.
142
142
  *
@@ -203,8 +203,8 @@ function resolveReachInPin(
203
203
  return null;
204
204
  }
205
205
 
206
- // Prefer per-output binding from the upstream activity manifest
207
- // (plan §B). Falls back to the job-level `with.deliverableSpecRef`
206
+ // Prefer per-output binding from the upstream activity manifest.
207
+ // Falls back to the job-level `with.deliverableSpecRef`
208
208
  // when no per-output binding is declared OR when the reach-in's
209
209
  // outputName is one of the indexed sentinels.
210
210
  let specRef: string | null = null;
@@ -431,7 +431,7 @@ function validateReachIn(
431
431
  return;
432
432
  }
433
433
 
434
- // Per-output binding from the upstream activity manifest (plan §B)
434
+ // Per-output binding from the upstream activity manifest
435
435
  // wins over the job-level `with.deliverableSpecRef` when present.
436
436
  const perOutputBinding =
437
437
  access.outputName === '<indexed>' || access.outputName === '<byKey>'
@@ -75,15 +75,15 @@ interface ReviewWithShape {
75
75
  /**
76
76
  * Per-job redraft-strategy descriptor carried on the IR `redraft` block
77
77
  * when the producer step has a matrix strategy. Mirrors the worker
78
- * consumer in `biomes/workflow-runtime/api/workflow-runtime-worker/src/workflows/review.workflow.ts`
79
- * — kept JSON-serializable so the IR survives Temporal payload encoding.
78
+ * consumer that runs the review workflow — kept JSON-serializable so the
79
+ * IR survives Temporal payload encoding.
80
80
  *
81
81
  * `static` entries are already cartesian-expanded at compile time;
82
82
  * `dynamic.fromExpression` is the producer's expression body (already
83
83
  * `${{ … }}`-stripped) which the parent dispatcher evaluates against
84
84
  * the review step's expression context before spawning the child
85
85
  * workflow. `keyBy` is informational only — per-CU keyed targeting is
86
- * a Phase-2 feature; Phase 1 always re-runs every matrix entry.
86
+ * a planned future feature; today every matrix entry is always re-run.
87
87
  */
88
88
  export type ReviewRedraftStrategyIR =
89
89
  | {
@@ -7,7 +7,8 @@ import { WorkflowDslError } from './errors';
7
7
  * no silent zero fallback — callers must pass a value that matches the
8
8
  * schema pattern (already enforced by Ajv upstream).
9
9
  *
10
- * Cap: hours limited to 8760 (= 365 days). The cap is a sanity guard
10
+ * Cap: total duration limited to 8760h (= 365 days), regardless of the
11
+ * unit used. The cap is a sanity guard
11
12
  * against typos (`100000h` ≈ 11 years), not a policy bound — real
12
13
  * human-driven flows (compliance reviews, multi-stakeholder approvals,
13
14
  * SCM PRs that go a quarter without merging) routinely take months.
@@ -30,19 +31,13 @@ export function parseDurationMs(literal: string): number {
30
31
  }
31
32
  const value = Number.parseInt(match[1]!, 10);
32
33
  const unit = match[2] as 's' | 'm' | 'h';
33
- if (unit === 'h' && value > MAX_HOURS) {
34
+ const ms = unit === 's' ? value * 1_000 : unit === 'm' ? value * 60_000 : value * 3_600_000;
35
+ if (ms > MAX_HOURS * 3_600_000) {
34
36
  throw new WorkflowDslError(
35
37
  WorkflowErrorCode.DSL_SCHEMA_INVALID,
36
38
  `Duration exceeds cap of ${MAX_HOURS}h: ${literal}`,
37
39
  { literal, maxHours: MAX_HOURS },
38
40
  );
39
41
  }
40
- switch (unit) {
41
- case 's':
42
- return value * 1_000;
43
- case 'm':
44
- return value * 60_000;
45
- case 'h':
46
- return value * 3_600_000;
47
- }
42
+ return ms;
48
43
  }
@@ -3,8 +3,8 @@ import type { TriggerPayload } from '@xemahq/kernel-contracts/workflow';
3
3
  /**
4
4
  * Per-run, read-only cache of pre-fetched artifact payloads keyed by
5
5
  * `versionId`. Populated by the workflow runtime BEFORE each step's
6
- * expressions are evaluated see `biomes/workflow-runtime/api/workflow-runtime-worker` for the
7
- * fetch + cache plumbing. The evaluator only ever reads from this map;
6
+ * expressions are evaluated, which owns the fetch + cache plumbing.
7
+ * The evaluator only ever reads from this map;
8
8
  * concurrent reads are safe by construction (no writes once frozen for a
9
9
  * step). Empty when no expression in scope reaches into a typed
10
10
  * payload or invokes `fromJSON(ref)`.
@@ -172,8 +172,8 @@ function evalCall(node: CallNode, ctx: ExpressionContext): unknown {
172
172
  * evaluator runs. A cache miss is a runtime infra bug, not an
173
173
  * author bug, so it carries a runtime-coded error.
174
174
  *
175
- * This is the §C.2 escape hatch. Authors who want compile-time field
176
- * validation use the typed reach-in (`outputs.foo.field`, §C.1).
175
+ * This is the untyped escape hatch. Authors who want compile-time field
176
+ * validation use the typed reach-in (`outputs.foo.field`).
177
177
  */
178
178
  function evalFromJson(node: CallNode, ctx: ExpressionContext): unknown {
179
179
  const argNode = singleArg(node, 'fromJSON');
@@ -1,6 +1,6 @@
1
1
  // ═══════════════════════════════════════════════════════════════════════════
2
2
  // ── Runtime walker — find every AST sub-expression that resolves to an
3
- // ArtifactRef the runtime must pre-fetch (plan §C.1 / §C.2). ──
3
+ // ArtifactRef the runtime must pre-fetch. ──
4
4
  //
5
5
  // Two flavours of pre-fetch target:
6
6
  //
@@ -202,7 +202,7 @@ export function extractUpstreamJobKey(node: ExpressionNode): string | null {
202
202
  * known there.
203
203
  *
204
204
  * Pair with `extractUpstreamJobKey` to resolve per-output spec pins at
205
- * runtime (plan §B). Falling back to `null` is the cue the prefetch
205
+ * runtime. Falling back to `null` is the cue the prefetch
206
206
  * helper uses to consult the job-level pin instead.
207
207
  */
208
208
  export function extractUpstreamOutputName(
@@ -12,7 +12,7 @@
12
12
  * how to query bound resources of that kind from biome-host-api.
13
13
  */
14
14
  export enum InstallationResourceKind {
15
- /** SMTP / IMAP wallet — `OrgIntegrationCredential` row attached to the installation. */
15
+ /** SMTP / IMAP wallet — `OrgConnectionCredential` row attached to the installation. */
16
16
  Wallet = 'wallet',
17
17
  /** SCM repository binding on the installation. */
18
18
  Repo = 'repo',
@@ -8,7 +8,7 @@ import type { WorkflowDocument } from './types';
8
8
  * Note on comment preservation: this serializer does NOT carry over
9
9
  * source comments from the original YAML — it dumps the AST verbatim.
10
10
  * Use it when the canonical representation lives in the AST (e.g. the
11
- * canvas editor in PR 5.x) and the YAML view is a derived presentation.
11
+ * canvas editor) and the YAML view is a derived presentation.
12
12
  * If you need to apply a single mutation to existing hand-authored YAML
13
13
  * while keeping comments + key ordering intact, use the `yaml`
14
14
  * package's CST/Document API directly (follow-up; not in this PR).
@@ -98,22 +98,22 @@ export interface WorkflowRequiresDeclaration {
98
98
  /** Wallet names; must match `^[a-z][a-z0-9-]{0,62}$`. */
99
99
  readonly wallets?: readonly string[];
100
100
  /**
101
- * Integration adapter kinds the workflow consumes. Biome-shipped
101
+ * Connector adapter kinds the workflow consumes. Biome-shipped
102
102
  * workflows declare these so `biome-host-api` can cross-validate
103
- * them against the biome's manifest `integrationRequirements` at
103
+ * them against the biome's manifest `connectorRequirements` at
104
104
  * boot. Stand-alone workflows (not biome-owned) may also declare
105
105
  * them as documentation; dispatch-time enforcement only kicks in
106
106
  * for biome-gated runs.
107
107
  */
108
- readonly integrations?: readonly WorkflowIntegrationRequirement[];
108
+ readonly connectors?: readonly WorkflowConnectorRequirement[];
109
109
  }
110
110
 
111
- export interface WorkflowIntegrationRequirement {
111
+ export interface WorkflowConnectorRequirement {
112
112
  /** Adapter kind slug (e.g. `scm`, `tracker`, `documentation`). */
113
113
  readonly adapterKind: string;
114
114
  /** Capability slugs the workflow exercises against the adapter. */
115
115
  readonly capabilities: readonly string[];
116
- /** Whether the workflow can degrade if the integration is unbound. */
116
+ /** Whether the workflow can degrade if the connector is unbound. */
117
117
  readonly optional?: boolean;
118
118
  }
119
119
 
@@ -171,6 +171,32 @@ export interface WorkflowConcurrencyDeclaration {
171
171
  export interface WorkflowDefaults {
172
172
  readonly retry?: WorkflowRetryDeclaration;
173
173
  readonly timeout?: string;
174
+ readonly gate?: WorkflowGateDefaults;
175
+ }
176
+
177
+ /**
178
+ * Workflow-level defaults for decision-gate jobs (`defaults.gate`).
179
+ *
180
+ * For every job whose `uses:` resolves to the `xema/decision-gate` action
181
+ * (matched on the action id, regardless of version), the compiler fills
182
+ * each ABSENT `with:` key from this block before any other compile pass
183
+ * runs. Explicit per-gate values always win, and the fill happens per
184
+ * top-level key only — a gate that declares its own `policy` /
185
+ * `recipients` keeps that value verbatim (no partial merge inside it).
186
+ *
187
+ * Every value may be a `${{ … }}` expression; merged values compile and
188
+ * evaluate per-job exactly as if they had been written inline. The key
189
+ * set is closed by the JSON schema AND cross-validated at compile time
190
+ * against the resolved gate manifest's declared `inputs` (fail-fast on
191
+ * drift between the schema's key set and the pinned gate version).
192
+ */
193
+ export interface WorkflowGateDefaults {
194
+ readonly timeoutSeconds?: unknown;
195
+ readonly onTimeout?: unknown;
196
+ readonly autonomyMode?: unknown;
197
+ readonly recipients?: unknown;
198
+ readonly policy?: unknown;
199
+ readonly escalationChain?: unknown;
174
200
  }
175
201
 
176
202
  export interface WorkflowRetryDeclaration {
@@ -181,7 +207,7 @@ export interface WorkflowRetryDeclaration {
181
207
  readonly nonRetryableErrorTypes?: readonly string[];
182
208
  }
183
209
 
184
- export type WorkflowPermissions = Partial<Record<'repos' | 'kb' | 'backlog' | 'integrations' | 'artifacts' | 'memory', PermissionScope>>;
210
+ export type WorkflowPermissions = Partial<Record<'repos' | 'kb' | 'backlog' | 'connectors' | 'artifacts' | 'memory', PermissionScope>>;
185
211
 
186
212
  export interface WorkflowJobDeclaration {
187
213
  readonly title?: string;
@@ -263,7 +289,7 @@ export interface ActionManifestSpec {
263
289
  readonly inputs: Readonly<Record<string, unknown>>;
264
290
  readonly outputs: Readonly<Record<string, unknown>>;
265
291
  /**
266
- * Per-output deliverable-spec bindings (plan §B per-output bindings).
292
+ * Per-output deliverable-spec bindings.
267
293
  * Each entry pins a named output to a deliverable-spec ref like
268
294
  * `agent-summary@1.0.0`. The DSL compiler consults this first when
269
295
  * resolving typed payload reach-in (`outputs.<name>.<field>`); it
@@ -137,7 +137,7 @@ function parseAndValidate<T>(
137
137
  { kind },
138
138
  );
139
139
  }
140
- // Plan §G shorthand: `needs: <string>` → `[<string>]`. Normalize
140
+ // Shorthand: `needs: <string>` → `[<string>]`. Normalize
141
141
  // BEFORE validation so the schema stays strict (one canonical shape)
142
142
  // and the rest of the compiler never branches on shorthand. Workflow +
143
143
  // reusable-workflow documents both have `jobs: { <key>: { needs?... } }`;
@@ -151,7 +151,7 @@ function parseAndValidate<T>(
151
151
  /**
152
152
  * Mutates `doc.jobs.<key>.needs` from `<string>` to `[<string>]` in
153
153
  * place. Authors can write either form; the canonical form is the
154
- * array. Documented in plan §G under "Simplified DSL".
154
+ * array. Documented under "Simplified DSL".
155
155
  */
156
156
  function normalizeJobNeedsShorthand(doc: object): void {
157
157
  const jobs = (doc as { jobs?: unknown }).jobs;
@@ -580,8 +580,7 @@ function compileWorkingFiles(
580
580
  }
581
581
  // `binaryFormat` is the closed-enum hint that signals a companion
582
582
  // binary (XLSX/PPTX/DOCX/PDF) lives alongside the synopsis. Validate now so
583
- // a typo cannot reach the worker as a silent no-op — see
584
- // `.claude/plans/xlsx-pptx-binary-kb-attachment.md` D6.
583
+ // a typo cannot reach the worker as a silent no-op.
585
584
  const binaryFormatRaw = wf.sourceRef['binaryFormat'];
586
585
  if (
587
586
  typeof binaryFormatRaw === 'string' &&
@@ -42,7 +42,7 @@ export function interpolate(
42
42
  basePath = '$',
43
43
  ): { readonly resolved: unknown; readonly issues: readonly InterpolationIssue[] } {
44
44
  const issues: InterpolationIssue[] = [];
45
- const resolved = walk(value, ctx, basePath, issues);
45
+ const resolved = walk(value, ctx, basePath, issues, new Set());
46
46
  return { resolved, issues };
47
47
  }
48
48
 
@@ -51,16 +51,42 @@ function walk(
51
51
  ctx: InterpolationContext,
52
52
  path: string,
53
53
  issues: InterpolationIssue[],
54
+ seen: Set<object>,
54
55
  ): unknown {
55
56
  if (typeof value === 'string') return interpolateString(value, ctx, path, issues);
56
57
  if (Array.isArray(value)) {
57
- return value.map((item, idx) => walk(item, ctx, `${path}[${idx}]`, issues));
58
+ // Reject cyclic structures (e.g. self-referential YAML anchors) with a
59
+ // clear error rather than recursing until the stack overflows.
60
+ if (seen.has(value)) {
61
+ throw new Error(`interpolate: circular reference at ${path}`);
62
+ }
63
+ seen.add(value);
64
+ const out = value.map((item, idx) => walk(item, ctx, `${path}[${idx}]`, issues, seen));
65
+ seen.delete(value);
66
+ return out;
58
67
  }
59
68
  if (value !== null && typeof value === 'object') {
69
+ if (seen.has(value)) {
70
+ throw new Error(`interpolate: circular reference at ${path}`);
71
+ }
72
+ seen.add(value);
60
73
  const out: Record<string, unknown> = {};
61
74
  for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
62
- out[key] = walk(child, ctx, `${path}.${key}`, issues);
75
+ const resolvedChild = walk(child, ctx, `${path}.${key}`, issues, seen);
76
+ // Assign `__proto__` as an own data property instead of invoking the
77
+ // prototype setter, which would silently drop the key and reparent `out`.
78
+ if (key === '__proto__') {
79
+ Object.defineProperty(out, key, {
80
+ value: resolvedChild,
81
+ writable: true,
82
+ enumerable: true,
83
+ configurable: true,
84
+ });
85
+ } else {
86
+ out[key] = resolvedChild;
87
+ }
63
88
  }
89
+ seen.delete(value);
64
90
  return out;
65
91
  }
66
92
  return value;
@@ -1,9 +1,9 @@
1
1
  // ═══════════════════════════════════════════════════════════════════════════
2
2
  // ── WorkspaceManifest `extends:` resolver ──
3
3
  //
4
- // Section 4.4 of the engineering plan. Provides a deterministic merge
5
- // primitive plus a thin orchestrator the composer / preview endpoint /
6
- // llm-registry calls to materialize the effective manifest.
4
+ // Provides a deterministic merge primitive plus a thin orchestrator the
5
+ // composer / preview endpoint / llm-registry calls to materialize the
6
+ // effective manifest.
7
7
  //
8
8
  // Design constraints:
9
9
  // • Single-level extends only. The base MUST NOT carry its own