@uipath/maestro-builder-sdk 6.64.0 → 6.66.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.
@@ -1,7 +1,8 @@
1
1
  import { createTypedNode, typedNodeOutputVar } from './typed-node.js';
2
2
  import { connectorBindingRef } from './connector.js';
3
- import { eventFilterJmes, eventFilterProblem, jmesString, jmesStringProblem, readEventFilter } from '../core/event-filters.js';
3
+ import { eventFilterJmes, jmesString, jmesStringProblem, readEventFilterLeaf } from '../core/event-filters.js';
4
4
  import { resolveEventWhere } from '../core/lookups.js';
5
+ import { CompileRefusal } from '../core/compile-refusal.js';
5
6
  /**
6
7
  * Which extension type an external invocation lowers to.
7
8
  *
@@ -57,14 +58,14 @@ export function intsvcOutputVar(n) {
57
58
  */
58
59
  export function createConnectorEvent(ctx, n) {
59
60
  if (!ctx.library)
60
- throw new Error(`connector event "${n.id}" needs the connector library — pass { library } to serialize ` +
61
+ throw new CompileRefusal('LIBRARY_REQUIRED', `connector event "${n.id}" needs the connector library — pass { library } to serialize ` +
61
62
  `or use uip maestro bpmn compile --library.`);
62
63
  const rc = ctx.library.resolveEvent(n.subscribe, n.key, n.eventName, n.version, n.object);
63
64
  const op = (rc.entry.operation ?? {});
64
65
  const runtime = rc.entry.runtime ?? {};
65
66
  const type = runtime.serviceType;
66
67
  if (!type) {
67
- throw new Error(`connector event ${n.key}/${n.eventName} has no runtime.serviceType in the library; ` +
68
+ throw new CompileRefusal('BPMN_EVENT_NO_SERVICE_TYPE', `connector event ${n.key}/${n.eventName} has no runtime.serviceType in the library; ` +
68
69
  `it cannot be authored as a BPMN node.`);
69
70
  }
70
71
  // The PLACEMENT decides the extension type — `.eventTrigger()` starts the
@@ -79,13 +80,13 @@ export function createConnectorEvent(ctx, n) {
79
80
  // .eventTrigger() instead", while the unprepared same node compiled.
80
81
  const expected = intsvcServiceType(n);
81
82
  if (type !== 'Intsvc.EventTrigger' && type !== 'Intsvc.WaitForEvent') {
82
- throw new Error(`connector event "${n.id}" was authored as a ${n.subscribe} (${expected}), but the library ` +
83
+ throw new CompileRefusal('BPMN_EVENT_KIND_MISMATCH', `connector event "${n.id}" was authored as a ${n.subscribe} (${expected}), but the library ` +
83
84
  `describes ${n.key}/${n.eventName} as ${type}, which is not a connector event.`);
84
85
  }
85
86
  const conn = connectorBindingRef(ctx, n.connection, 'Connection', 'ConnectionId', `${rc.connectorKey} connection`);
86
87
  const folder = connectorBindingRef(ctx, n.folder, 'Connection', 'folderKey', 'FolderKey', conn?.resourceKey);
87
88
  if (!conn || !folder) {
88
- throw new Error(`connector event "${n.id}" needs both a connection and a folder binding name — ` +
89
+ throw new CompileRefusal('BPMN_EVENT_BINDINGS_MISSING', `connector event "${n.id}" needs both a connection and a folder binding name — ` +
89
90
  `the type declares connectionId, folderId, folderPath and folderKey all required.`);
90
91
  }
91
92
  // The registry marks `operation` and `objectName` required AND hidden, so an
@@ -144,20 +145,20 @@ export function eventFilterBody(nodeId, scope, filters) {
144
145
  const scopeClauses = scopeEntries.map(([k, v]) => {
145
146
  const problem = jmesStringProblem(String(v));
146
147
  if (problem)
147
- throw new Error(`connector event "${nodeId}": \`filter.${k}\`: ${problem}`);
148
+ throw new CompileRefusal('EVENT_FILTER_BACKSLASH', `connector event "${nodeId}": \`filter.${k}\`: ${problem}`);
148
149
  return `(${k} == ${jmesString(String(v))})`;
149
150
  });
150
151
  const conditions = (filters ?? []).map((leaf, i) => {
151
- const { read } = readEventFilter(leaf);
152
- if (!read) {
153
- throw new Error(`connector event "${nodeId}": filters[${i}] must carry a field and exactly ONE operator ` +
154
- `(contains, startsWith, endsWith, equals, notEquals, lessThan, lessThanOrEqual, greaterThan, ` +
155
- `greaterThanOrEqual) — got ${JSON.stringify(leaf)}.`);
152
+ // The code `check` gives the same leaf (`core/event-checks.ts`).
153
+ const judged = readEventFilterLeaf(leaf);
154
+ if (judged.code !== undefined) {
155
+ throw new CompileRefusal(judged.code, judged.problem !== undefined
156
+ ? `connector event "${nodeId}": filters[${i}] on \`${judged.read?.field}\`: ${judged.problem}`
157
+ : `connector event "${nodeId}": filters[${i}] must carry a field and exactly ONE operator ` +
158
+ `(contains, startsWith, endsWith, equals, notEquals, lessThan, lessThanOrEqual, greaterThan, ` +
159
+ `greaterThanOrEqual) — got ${JSON.stringify(leaf)}.`);
156
160
  }
157
- const problem = eventFilterProblem(read);
158
- if (problem)
159
- throw new Error(`connector event "${nodeId}": filters[${i}] on \`${read.field}\`: ${problem}`);
160
- return `(${eventFilterJmes(read)})`;
161
+ return `(${eventFilterJmes(judged.read)})`;
161
162
  });
162
163
  const expression = [...scopeClauses, ...conditions].join(' && ');
163
164
  const body = {};
@@ -176,7 +177,7 @@ export function eventFilterBody(nodeId, scope, filters) {
176
177
  */
177
178
  export function createExternalTask(ctx, n) {
178
179
  if (!ctx.library)
179
- throw new Error(`external ${n.family} "${n.id}" needs the connector library — pass { library } to serialize ` +
180
+ throw new CompileRefusal('LIBRARY_REQUIRED', `external ${n.family} "${n.id}" needs the connector library — pass { library } to serialize ` +
180
181
  `or use uip maestro bpmn compile --library.`);
181
182
  const rc = ctx.library.resolve(n.key, n.action, n.version);
182
183
  const op = (rc.entry.operation ?? {});
@@ -186,14 +187,14 @@ export function createExternalTask(ctx, n) {
186
187
  // validator accepts and the runtime dispatches to the wrong handler family —
187
188
  // the Case SDK records the same trap in its Phase 4.4 notes.
188
189
  if (op.subType !== wanted) {
189
- throw new Error(`external ${n.family} "${n.id}" needs ${wanted === 'AgentExecution' ? 'an' : 'a'} ${wanted} descriptor, but ` +
190
+ throw new CompileRefusal('EXTERNAL_TASK_DESCRIPTOR_CONTRACT', `external ${n.family} "${n.id}" needs ${wanted === 'AgentExecution' ? 'an' : 'a'} ${wanted} descriptor, but ` +
190
191
  `${n.key}/${n.action} is "${String(op.subType ?? 'untyped')}". ` +
191
192
  `The shipped library has very few of these — ${wanted === 'AgentExecution' ? '2' : '1'} operation(s).`);
192
193
  }
193
194
  const conn = connectorBindingRef(ctx, n.connection, 'Connection', 'ConnectionId', `${rc.connectorKey} connection`);
194
195
  const folder = connectorBindingRef(ctx, n.folder, 'Connection', 'folderKey', 'FolderKey', conn?.resourceKey);
195
196
  if (!conn || !folder) {
196
- throw new Error(`external ${n.family} "${n.id}" needs both a connection and a folder binding name.`);
197
+ throw new CompileRefusal(conn ? 'EXTERNAL_TASK_FOLDER' : 'EXTERNAL_TASK_CONNECTION', `external ${n.family} "${n.id}" needs both a connection and a folder binding name.`);
197
198
  }
198
199
  return createTypedNode(ctx.m, n.id, EXTERNAL_SERVICE_TYPE[n.family][n.mode], {
199
200
  name: n.name ?? rc.entry.display?.label ?? n.action,
package/dist/bpmn/load.js CHANGED
@@ -3,22 +3,23 @@ import { pathToFileURL } from 'node:url';
3
3
  import { resolve } from 'node:path';
4
4
  import { existsSync } from 'node:fs';
5
5
  import { useThisSdkForAuthoredSource } from '../sdk-resolve.js';
6
+ import { CompileRefusal } from '../core/compile-refusal.js';
6
7
  /** Resolve a user argument (a `.ts` path or a BaseName) to a bpmn source file. */
7
8
  export function resolveBpmnFile(arg) {
8
9
  const candidates = [arg, `${arg}.bpmn.ts`, `${arg}.ts`];
9
10
  for (const c of candidates)
10
11
  if (existsSync(resolve(c)))
11
12
  return resolve(c);
12
- throw new Error(`bpmn source not found: tried ${candidates.join(', ')}`);
13
+ throw new CompileRefusal('BPMN_SOURCE_NOT_FOUND', `bpmn source not found: tried ${candidates.join(', ')}`);
13
14
  }
14
15
  export async function loadBuiltBpmn(file) {
15
16
  useThisSdkForAuthoredSource();
16
17
  const mod = await import(pathToFileURL(file).href);
17
18
  const exp = mod.default ?? mod.bpmn;
18
19
  if (!exp)
19
- throw new Error(`${file} has no default export. Add: export default bpmn('id')...build();`);
20
+ throw new CompileRefusal('BPMN_NO_DEFAULT_EXPORT', `${file} has no default export. Add: export default bpmn('id')...build();`);
20
21
  const built = typeof exp.build === 'function' ? exp.build() : exp;
21
22
  if (!built || !Array.isArray(built.nodes))
22
- throw new Error(`${file} default export is not a built bpmn. Did you call .build()?`);
23
+ throw new CompileRefusal('BPMN_EXPORT_NOT_BUILT', `${file} default export is not a built bpmn. Did you call .build()?`);
23
24
  return built;
24
25
  }
@@ -22,6 +22,7 @@ import { connectorOutputVars, typedNodeDeclaredVars } from './declared-vars.js';
22
22
  import { defaultEntryPointId } from './entry-point-id.js';
23
23
  import { parseXmlRanges } from './xml-ranges.js';
24
24
  import { ownEntry } from '../core/own-entry.js';
25
+ import { CompileRefusal } from '../core/compile-refusal.js';
25
26
  const require = createRequire(import.meta.url);
26
27
  const uipathDescriptor = require('./uipath-moddle.v1.json');
27
28
  const NS = {
@@ -251,7 +252,16 @@ function jsonInputBodiesAsCdata(xml) {
251
252
  collect(node.children);
252
253
  }
253
254
  };
254
- collect(parseXmlRanges(xml));
255
+ let ranges;
256
+ try {
257
+ ranges = parseXmlRanges(xml);
258
+ }
259
+ catch (e) {
260
+ // `xml` is moddle's own output, so malformed XML here is the serializer's
261
+ // defect, not the author's. BPMN_XML_MALFORMED stays for what `bpmn merge` reads.
262
+ throw new Error(`internal SDK error: the serializer wrote malformed XML: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
263
+ }
264
+ collect(ranges);
255
265
  let out = xml;
256
266
  // Splice from the end so the earlier offsets stay valid.
257
267
  for (const input of bodies.sort((a, b) => b.innerStart - a.innerStart)) {
@@ -289,12 +299,12 @@ function cdata(name, text) {
289
299
  value = JSON.parse(text);
290
300
  }
291
301
  catch {
292
- throw new Error(`json input "${name}" contains "]]>" and is not valid JSON, so it cannot be written as the single CDATA section the engine reads.`);
302
+ throw new CompileRefusal('BPMN_JSON_INPUT_CDATA_END', `json input "${name}" contains "]]>" and is not valid JSON, so it cannot be written as the single CDATA section the engine reads.`);
293
303
  }
294
304
  const escaped = text.replaceAll(']]>', ']]\\u003e');
295
305
  // Holds by the argument above; checked because a wrong payload here would be silent.
296
306
  if (JSON.stringify(JSON.parse(escaped)) !== JSON.stringify(value)) {
297
- throw new Error(`json input "${name}": escaping "]]>" changed its value.`);
307
+ throw new Error(`internal SDK error: json input "${name}": escaping "]]>" changed its value.`);
298
308
  }
299
309
  return `<![CDATA[${escaped}]]>`;
300
310
  }
@@ -356,30 +366,35 @@ function stampEntryPointIds(ctx, built, flowElements, rootDefault) {
356
366
  /** Build the flowElements array for one scope, wiring all references within it. */
357
367
  function buildScope(ctx, nodes, flows) {
358
368
  const { m } = ctx;
359
- const byId = new Map();
369
+ const nodeById = new Map();
370
+ const flowById = new Map();
360
371
  const nodeEls = nodes.map((n) => {
361
372
  const el = createNode(ctx, n);
362
- byId.set(n.id, el);
373
+ nodeById.set(n.id, el);
363
374
  return el;
364
375
  });
365
376
  const flowEls = flows.map((f) => {
366
377
  const el = m.create('bpmn:SequenceFlow', { id: f.id, name: f.name });
367
378
  if (f.condition !== undefined)
368
379
  el.conditionExpression = m.create('bpmn:FormalExpression', { body: f.condition });
369
- byId.set(f.id, el);
380
+ flowById.set(f.id, el);
370
381
  return el;
371
382
  });
372
- const ref = (id, where) => {
383
+ // A flow endpoint and a boundary attachment name a node in this scope, as
384
+ // `check` resolves them (DANGLING_FLOW, BOUNDARY_UNATTACHED); a sequence flow
385
+ // is never one. A gateway default names a flow, or any element as before.
386
+ const ref = (byId, id, where, code) => {
373
387
  const el = byId.get(id);
374
388
  if (!el)
375
- throw new Error(`${where} references unknown element id "${id}".`);
389
+ throw new CompileRefusal(code, `${where} references unknown element id "${id}".`);
376
390
  return el;
377
391
  };
392
+ const anyById = new Map([...nodeById, ...flowById]);
378
393
  // Wire sequence flows to source/target and populate incoming/outgoing.
379
394
  flows.forEach((f, i) => {
380
395
  const flowEl = flowEls[i];
381
- const src = ref(f.source, `sequenceFlow "${f.id}"`);
382
- const tgt = ref(f.target, `sequenceFlow "${f.id}"`);
396
+ const src = ref(nodeById, f.source, `sequenceFlow "${f.id}"`, 'DANGLING_FLOW');
397
+ const tgt = ref(nodeById, f.target, `sequenceFlow "${f.id}"`, 'DANGLING_FLOW');
383
398
  flowEl.sourceRef = src;
384
399
  flowEl.targetRef = tgt;
385
400
  (src.outgoing ??= []).push(flowEl);
@@ -389,9 +404,9 @@ function buildScope(ctx, nodes, flows) {
389
404
  nodes.forEach((n, i) => {
390
405
  const el = nodeEls[i];
391
406
  if (n.kind === 'boundaryEvent')
392
- el.attachedToRef = ref(n.attachedTo, `boundaryEvent "${n.id}"`);
407
+ el.attachedToRef = ref(nodeById, n.attachedTo, `boundaryEvent "${n.id}"`, 'BOUNDARY_UNATTACHED');
393
408
  if ((n.kind === 'exclusiveGateway' || n.kind === 'inclusiveGateway') && n.default !== undefined)
394
- el.default = ref(n.default, `gateway "${n.id}" default`);
409
+ el.default = ref(anyById, n.default, `gateway "${n.id}" default`, 'BPMN_GATEWAY_DEFAULT_UNKNOWN');
395
410
  });
396
411
  return [...nodeEls, ...flowEls];
397
412
  }
@@ -729,8 +744,9 @@ function eventDefinition(ctx, def) {
729
744
  */
730
745
  function moddleTaskType(element) {
731
746
  const [ns, local] = element.split(':');
747
+ // `.plainTask()` refuses any element outside `PlainTaskElement` (BPMN_PLAIN_TASK_ELEMENT_INVALID).
732
748
  if (!ns || !local)
733
- throw new Error(`plainTask: "${element}" is not a qualified element name`);
749
+ throw new Error(`internal SDK error: plainTask: "${element}" is not a qualified element name`);
734
750
  return `${ns}:${local.charAt(0).toUpperCase()}${local.slice(1)}`;
735
751
  }
736
752
  function outputRows(ctx, map, custom = false) {
@@ -3,6 +3,7 @@ import { resolveFromRegistry } from './registry-fallback.js';
3
3
  import { nestConnectorBody } from '../core/connector-inputs.js';
4
4
  import { types } from '../core/expr.js';
5
5
  import { ownEntry } from '../core/own-entry.js';
6
+ import { CompileRefusal } from '../core/compile-refusal.js';
6
7
  const require = createRequire(import.meta.url);
7
8
  const REGISTRY = require('./registry-types.json');
8
9
  /** Every extension type in the snapshot, sorted. */
@@ -33,7 +34,7 @@ export function registryType(type, inline) {
33
34
  // for one the snapshot describes wrongly. See `ActivityNodeOpts.spec`.
34
35
  const spec = inline ?? REGISTRY.types[type] ?? resolveFromRegistry(type);
35
36
  if (!spec) {
36
- throw new Error(`unknown extension type "${type}". The committed registry snapshot has ${REGISTRY.provenance.typeCount} types `
37
+ throw new CompileRefusal('BPMN_TYPE_UNKNOWN', `unknown extension type "${type}". The committed registry snapshot has ${REGISTRY.provenance.typeCount} types `
37
38
  + `(uip ${REGISTRY.provenance.uipVersion}) and the registry could not be asked for this one either — `
38
39
  + 'no `uip` on PATH, not logged in, or the type does not exist. '
39
40
  + 'Check the spelling with `uip maestro bpmn registry search <term>`, then refresh the snapshot with '
@@ -306,7 +307,7 @@ function contextRows(m, type, spec, supplied, verbatim = false) {
306
307
  ]);
307
308
  const unknown = Object.keys(supplied).filter((k) => !known.has(k));
308
309
  if (unknown.length) {
309
- throw new Error(`context field(s) ${unknown.map((u) => `"${u}"`).join(', ')} are not declared by this extension type. ` +
310
+ throw new CompileRefusal('BPMN_CONTEXT_FIELD_UNKNOWN', `context field(s) ${unknown.map((u) => `"${u}"`).join(', ')} are not declared by this extension type. ` +
310
311
  `Declared: ${[...known].join(', ') || '(none)'}.`);
311
312
  }
312
313
  // Permitted-but-undeclared fields (a recorded `extraContext` gap) still have to
@@ -355,7 +356,7 @@ function inputRows(m, spec, inputs) {
355
356
  const keys = Object.keys(inputs);
356
357
  if (spec.inputPattern === 'none') {
357
358
  if (keys.length) {
358
- throw new Error(`this extension type takes no inputs, but ${keys.length} were supplied (${keys.join(', ')}).`);
359
+ throw new CompileRefusal('BPMN_TYPE_NO_INPUTS', `this extension type takes no inputs, but ${keys.length} were supplied (${keys.join(', ')}).`);
359
360
  }
360
361
  return [];
361
362
  }
@@ -502,7 +503,7 @@ export function createTypedNode(m, id, type, input,
502
503
  varType) {
503
504
  const spec = registryType(type, input.spec);
504
505
  if (!spec.element || !spec.tag) {
505
- throw new Error(`extension type "${type}" has no element/tag in the registry snapshot; it cannot be authored.`);
506
+ throw new CompileRefusal('BPMN_TYPE_NOT_AUTHORABLE', `extension type "${type}" has no element/tag in the registry snapshot; it cannot be authored.`);
506
507
  }
507
508
  const payload = {
508
509
  version: 'v1',
@@ -512,7 +513,7 @@ varType) {
512
513
  // carry it, and moddle would silently drop it (see ActivityOpts in bpmn-sdk).
513
514
  if (input.skipCondition !== undefined) {
514
515
  if (spec.tag === 'uipath:mapping') {
515
- throw new Error(`extension type "${type}" serializes a uipath:mapping, which cannot carry a skipCondition.`);
516
+ throw new CompileRefusal('BPMN_TYPE_SKIP_UNSUPPORTED', `extension type "${type}" serializes a uipath:mapping, which cannot carry a skipCondition.`);
516
517
  }
517
518
  payload.skipCondition = input.skipCondition;
518
519
  }
@@ -636,7 +637,7 @@ export function verbatimOutputRows(m, rows) {
636
637
  export function createExtensionPayload(m, type, rows) {
637
638
  const spec = registryType(type);
638
639
  if (!spec.tag) {
639
- throw new Error(`extension type "${type}" has no tag in the registry snapshot; it cannot be authored.`);
640
+ throw new CompileRefusal('BPMN_TYPE_NOT_AUTHORABLE', `extension type "${type}" has no tag in the registry snapshot; it cannot be authored.`);
640
641
  }
641
642
  const payload = {
642
643
  version: 'v1',
@@ -664,7 +665,7 @@ function finish(m, spec, id, name, payload) {
664
665
  /** `bpmn:SendTask` is already the moddle type name; keep it, but validate the shape. */
665
666
  function moddleElementOf(element) {
666
667
  if (!/^bpmn:[A-Z]/.test(element)) {
667
- throw new Error(`registry element "${element}" is not a moddle type name (expected e.g. bpmn:SendTask).`);
668
+ throw new CompileRefusal('BPMN_TYPE_ELEMENT_INVALID', `registry element "${element}" is not a moddle type name (expected e.g. bpmn:SendTask).`);
668
669
  }
669
670
  return element;
670
671
  }
@@ -1,22 +1,3 @@
1
- /**
2
- * xml-ranges — locate elements by their SOURCE RANGE in an XML document.
3
- *
4
- * A tolerant scanner, not a parser: it reports where each element's text begins and
5
- * ends and nothing about what it means. That is all a merge needs, and it is the only
6
- * shape that can preserve an element **byte for byte** — the merged document is the
7
- * original text with a few ranges spliced, so anything untouched is untouched in the
8
- * literal sense, down to attribute order, quoting and indentation.
9
- *
10
- * Why not `bpmn-moddle`, which the SDK already depends on: a moddle round trip
11
- * normalises. It drops attributes its descriptor does not declare, and the whole point
12
- * of a brownfield merge is to carry through exactly the payloads the builder does NOT
13
- * model. Reading with moddle would lose the thing being preserved.
14
- *
15
- * Correspondingly, this file must not be mistaken for a general XML implementation. It
16
- * handles what a `.bpmn` contains — elements, attributes, comments, CDATA, processing
17
- * instructions, a doctype — and it assumes well-formed input, because every caller has
18
- * either just written the file or is about to hand it to a real parser anyway.
19
- */
20
1
  /** One element's place in the source text. */
21
2
  export interface XmlRange {
22
3
  /** Qualified name as written, e.g. `bpmn:scriptTask`. */
@@ -17,6 +17,7 @@
17
17
  * instructions, a doctype — and it assumes well-formed input, because every caller has
18
18
  * either just written the file or is about to hand it to a real parser anyway.
19
19
  */
20
+ import { CompileRefusal } from '../core/compile-refusal.js';
20
21
  const ATTRIBUTE = /([^\s=/>]+)\s*=\s*("([^"]*)"|'([^']*)')/g;
21
22
  function parseAttributes(source) {
22
23
  const attributes = {};
@@ -44,13 +45,13 @@ function endOfTag(xml, from) {
44
45
  return i;
45
46
  }
46
47
  }
47
- throw new Error(`xml-ranges: unterminated tag at offset ${from}`);
48
+ throw new CompileRefusal('BPMN_XML_MALFORMED', `xml-ranges: unterminated tag at offset ${from}`);
48
49
  }
49
50
  /** Skip a construct that ends with a known terminator; returns the offset past it. */
50
51
  function skipUntil(xml, from, terminator) {
51
52
  const at = xml.indexOf(terminator, from);
52
53
  if (at < 0)
53
- throw new Error(`xml-ranges: unterminated ${terminator.trim()} at offset ${from}`);
54
+ throw new CompileRefusal('BPMN_XML_MALFORMED', `xml-ranges: unterminated ${terminator.trim()} at offset ${from}`);
54
55
  return at + terminator.length;
55
56
  }
56
57
  function* rawTags(xml) {
@@ -122,7 +123,7 @@ export function parseXmlRanges(xml) {
122
123
  if (tag.closing) {
123
124
  const node = stack.pop();
124
125
  if (!node)
125
- throw new Error(`xml-ranges: stray </${tag.name}> at offset ${tag.start}`);
126
+ throw new CompileRefusal('BPMN_XML_MALFORMED', `xml-ranges: stray </${tag.name}> at offset ${tag.start}`);
126
127
  finish(tag, node, tag.end);
127
128
  continue;
128
129
  }
@@ -134,7 +135,7 @@ export function parseXmlRanges(xml) {
134
135
  stack.push({ tag, children: [] });
135
136
  }
136
137
  if (stack.length > 0) {
137
- throw new Error(`xml-ranges: unclosed <${stack[stack.length - 1].tag.name}>`);
138
+ throw new CompileRefusal('BPMN_XML_MALFORMED', `xml-ranges: unclosed <${stack[stack.length - 1].tag.name}>`);
138
139
  }
139
140
  return roots;
140
141
  }
@@ -16,6 +16,7 @@
16
16
  * `case/check.ts`, not here.
17
17
  */
18
18
  import { type ExprDiagnostic } from '../core/expr-check.js';
19
+ import { CompileRefusal } from '../core/compile-refusal.js';
19
20
  import type { BuiltCase } from './case-sdk.js';
20
21
  /** A diagnostic plus the part of the case it was found in. */
21
22
  export interface CaseLocatedDiagnostic extends ExprDiagnostic {
@@ -29,7 +30,7 @@ export declare function checkCaseExpressions(built: BuiltCase): CaseLocatedDiagn
29
30
  * `FlowBuildError`: `message` lists every problem; `diagnostics` carries them
30
31
  * structured.
31
32
  */
32
- export declare class CaseBuildError extends Error {
33
+ export declare class CaseBuildError extends CompileRefusal {
33
34
  readonly diagnostics: CaseLocatedDiagnostic[];
34
35
  constructor(built: BuiltCase, diagnostics: CaseLocatedDiagnostic[]);
35
36
  }
@@ -16,6 +16,7 @@
16
16
  * `case/check.ts`, not here.
17
17
  */
18
18
  import { checkExpression } from '../core/expr-check.js';
19
+ import { CompileRefusal } from '../core/compile-refusal.js';
19
20
  import { defaultActionOutputVar } from './action-output-var.js';
20
21
  const NOUN = 'case variable';
21
22
  const NAMESPACE = 'vars';
@@ -83,12 +84,13 @@ export function checkCaseExpressions(built) {
83
84
  * `FlowBuildError`: `message` lists every problem; `diagnostics` carries them
84
85
  * structured.
85
86
  */
86
- export class CaseBuildError extends Error {
87
+ export class CaseBuildError extends CompileRefusal {
87
88
  diagnostics;
88
89
  constructor(built, diagnostics) {
89
90
  const label = built.name || built.id || 'case';
90
91
  const lines = diagnostics.map((d) => ` ✗ [${d.code}] in ${d.where}: ${d.message}${d.suggestion ? ` (did you mean "${d.suggestion}"?)` : ''}`);
91
- super(`Cannot build case "${label}" — ${diagnostics.length} expression problem(s):\n${lines.join('\n')}`);
92
+ // The first problem's code; `diagnostics` carries every one.
93
+ super(diagnostics[0]?.code ?? 'EXPR_UNDECLARED_REF', `Cannot build case "${label}" — ${diagnostics.length} expression problem(s):\n${lines.join('\n')}`);
92
94
  this.name = 'CaseBuildError';
93
95
  this.diagnostics = diagnostics;
94
96
  }
@@ -3,6 +3,7 @@ import { isTriggerDescriptor } from '../core/connectors.js';
3
3
  import { checkCaseExpressions, CaseBuildError } from './case-expr-check.js';
4
4
  import { CASE_FORMAT_PROFILE, CASE_SCHEMA_VERSIONS, recordAuthoredCaseVersion } from './format-profile.js';
5
5
  import { schemaVersionRefusal } from '../schema-version.js';
6
+ import { CompileRefusal } from '../core/compile-refusal.js';
6
7
  export { preserveCaseJson } from './preserve.js';
7
8
  export { CaseBuildError } from './case-expr-check.js';
8
9
  /**
@@ -115,11 +116,11 @@ export function eventTrigger(descriptorOrOpts = {}, descriptorOpts = {}) {
115
116
  const object = descriptor ? undefined : opts.object;
116
117
  if (object !== undefined) {
117
118
  if (opts.subscription)
118
- throw new Error('eventTrigger: pass the watched object as `subscription.object` on a resolved subscription, not as `object`.');
119
+ throw new CompileRefusal('CASE_TRIGGER_OBJECT_AND_SUBSCRIPTION', 'eventTrigger: pass the watched object as `subscription.object` on a resolved subscription, not as `object`.');
119
120
  // Refuse padding rather than trim it: the value must match the SDD's
120
121
  // source object exactly, and a silent trim would hide the typo.
121
122
  if (typeof object !== 'string' || !object.trim() || object !== object.trim())
122
- throw new Error('eventTrigger: `object` must be a non-empty string with no surrounding whitespace.');
123
+ throw new CompileRefusal('CASE_TRIGGER_OBJECT_INVALID', 'eventTrigger: `object` must be a non-empty string with no surrounding whitespace.');
123
124
  }
124
125
  return {
125
126
  kind: 'event',
@@ -178,7 +179,7 @@ class TaskBuilder {
178
179
  }
179
180
  setKind(kind) {
180
181
  if (this._kind)
181
- throw new Error(`task "${this._name}": kind already set to "${this._kind}", cannot also be "${kind}".`);
182
+ throw new CompileRefusal('TASK_KIND_TWICE', `task "${this._name}": kind already set to "${this._kind}", cannot also be "${kind}".`);
182
183
  this._kind = kind;
183
184
  }
184
185
  reference(kind, name, opts = {}) {
@@ -530,7 +531,7 @@ class TaskBuilder {
530
531
  const type = (v.type ?? 'string');
531
532
  if ('assign' in v) {
532
533
  if ('source' in v)
533
- throw new Error(`task "${this._name}": output "${name}" sets both source and assign — use source to extract a result field, assign for a custom output.`);
534
+ throw new CompileRefusal('TASK_OUTPUT_SOURCE_AND_ASSIGN', `task "${this._name}": output "${name}" sets both source and assign — use source to extract a result field, assign for a custom output.`);
534
535
  this._outputs.push({ var: name, source: customOutputValue(v.assign), type, custom: true });
535
536
  }
536
537
  else {
@@ -553,7 +554,7 @@ class TaskBuilder {
553
554
  /** @internal */
554
555
  _build() {
555
556
  if (!this._kind)
556
- throw new Error(`task "${this._name}" has no kind — call .process()/.agent()/.rpa()/.action()/.connector()/.waitForTimer()/… once.`);
557
+ throw new CompileRefusal('TASK_NO_KIND', `task "${this._name}" has no kind — call .process()/.agent()/.rpa()/.action()/.connector()/.waitForTimer()/… once.`);
557
558
  // Task input binding is reference-mode only (REF_IO): a connector task takes
558
559
  // its inputs in `.connector()` / `.waitForConnector()`. Output extracts also
559
560
  // work on connector and wait-for-connector tasks: the task writes its own
@@ -563,14 +564,14 @@ class TaskBuilder {
563
564
  const REF_IO = new Set(['process', 'agent', 'rpa', 'api-workflow', 'case-management', 'flow-process', 'function', 'business-rule']);
564
565
  const CONNECTOR_OUTPUTS = new Set(['connector', 'wait-for-connector']);
565
566
  if (this._inputs && !REF_IO.has(this._kind))
566
- throw new Error(`task "${this._name}": .inputs()/.outputs() are only supported on reference-mode tasks (process/agent/rpa/api-workflow/case-management/flow-process/function/business-rule), not "${this._kind}". A connector task takes its inputs in .connector() / .waitForConnector().`);
567
+ throw new CompileRefusal('TASK_INPUTS_UNSUPPORTED', `task "${this._name}": .inputs()/.outputs() are only supported on reference-mode tasks (process/agent/rpa/api-workflow/case-management/flow-process/function/business-rule), not "${this._kind}". A connector task takes its inputs in .connector() / .waitForConnector().`);
567
568
  if (this._outputs && !REF_IO.has(this._kind) && !CONNECTOR_OUTPUTS.has(this._kind))
568
- throw new Error(`task "${this._name}": .outputs() is only supported on reference-mode tasks (process/agent/rpa/api-workflow/case-management/flow-process/function/business-rule) and on connector / wait-for-connector tasks, not "${this._kind}".`);
569
+ throw new CompileRefusal('TASK_OUTPUTS_UNSUPPORTED', `task "${this._name}": .outputs() is only supported on reference-mode tasks (process/agent/rpa/api-workflow/case-management/flow-process/function/business-rule) and on connector / wait-for-connector tasks, not "${this._kind}".`);
569
570
  if (REF_IO.has(this._kind) && !this._ref && (this._inputs || this._outputs))
570
- throw new Error(`task "${this._name}": an unresolved reference task cannot declare inputs or outputs.`);
571
+ throw new CompileRefusal('TASK_IO_UNRESOLVED_REFERENCE', `task "${this._name}": an unresolved reference task cannot declare inputs or outputs.`);
571
572
  if (this._kind === 'wait-for-connector' && this._outputs
572
573
  && !(this._waitConnector && 'connector' in this._waitConnector && 'event' in this._waitConnector))
573
- throw new Error(`task "${this._name}": a placeholder wait-for-connector task has no event response to extract outputs from — name the event (.waitForConnector(<trigger descriptor>) or { connector, event }).`);
574
+ throw new CompileRefusal('TASK_OUTPUTS_PLACEHOLDER_EVENT', `task "${this._name}": a placeholder wait-for-connector task has no event response to extract outputs from — name the event (.waitForConnector(<trigger descriptor>) or { connector, event }).`);
574
575
  return {
575
576
  name: this._name,
576
577
  kind: this._kind,
@@ -824,7 +825,7 @@ class CaseBuilder {
824
825
  schemaVersion(version) {
825
826
  const refusal = schemaVersionRefusal(CASE_SCHEMA_VERSIONS, version);
826
827
  if (refusal)
827
- throw new Error(refusal);
828
+ throw new CompileRefusal('SCHEMA_VERSION_UNSUPPORTED', refusal);
828
829
  this._schemaVersion = version;
829
830
  return this;
830
831
  }
package/dist/case/load.js CHANGED
@@ -3,6 +3,7 @@ import { pathToFileURL } from 'node:url';
3
3
  import { resolve } from 'node:path';
4
4
  import { existsSync, statSync } from 'node:fs';
5
5
  import { useThisSdkForAuthoredSource } from '../sdk-resolve.js';
6
+ import { CompileRefusal } from '../core/compile-refusal.js';
6
7
  /**
7
8
  * Resolve a user argument (a `.ts` path or a BaseName) to a case source file.
8
9
  *
@@ -19,16 +20,16 @@ export function resolveCaseFile(arg) {
19
20
  if (existsSync(resolved) && statSync(resolved).isFile())
20
21
  return resolved;
21
22
  }
22
- throw new Error(`case source not found: tried ${candidates.join(', ')}`);
23
+ throw new CompileRefusal('CASE_SOURCE_NOT_FOUND', `case source not found: tried ${candidates.join(', ')}`);
23
24
  }
24
25
  export async function loadBuiltCase(file) {
25
26
  useThisSdkForAuthoredSource();
26
27
  const mod = await import(pathToFileURL(file).href);
27
28
  const exp = mod.default ?? mod.casePlan ?? mod.case;
28
29
  if (!exp)
29
- throw new Error(`${file} has no default export. Add: export default casePlan('id')...build();`);
30
+ throw new CompileRefusal('CASE_NO_DEFAULT_EXPORT', `${file} has no default export. Add: export default casePlan('id')...build();`);
30
31
  const built = typeof exp.build === 'function' ? exp.build() : exp;
31
32
  if (!built || !Array.isArray(built.stages))
32
- throw new Error(`${file} default export is not a built case. Did you call .build()?`);
33
+ throw new CompileRefusal('CASE_EXPORT_NOT_BUILT', `${file} default export is not a built case. Did you call .build()?`);
33
34
  return built;
34
35
  }