@uipath/maestro-builder-sdk 6.63.0 → 6.65.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.
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
  }
@@ -19,16 +19,17 @@ import { bindingLabelRefusal } from '../core/binding-messages.js';
19
19
  import { resolveEventWhere } from '../core/lookups.js';
20
20
  import { resolveCaseSubscription } from './subscription.js';
21
21
  import { dedupeConditionNames } from './rule-names.js';
22
- import { externalDescriptorRefusal, externalHybridRefusal, externalServiceType, isExternalExecutionMode } from './external.js';
22
+ import { EXTERNAL_TASK_DESCRIPTOR_CONTRACT, EXTERNAL_TASK_HYBRID_UNSUPPORTED, externalDescriptorRefusal, externalHybridRefusal, externalServiceType, isExternalExecutionMode, } from './external.js';
23
23
  import { CASE_FORMAT_PROFILE, CASE_SCHEMA_VERSIONS } from './format-profile.js';
24
24
  import { isResourceLabelRefusal, resolveResourceRef, resourceIdentity } from './resource-identity.js';
25
25
  import { ownEntry } from '../core/own-entry.js';
26
26
  import { resolveSchemaVersion } from '../schema-version.js';
27
27
  import { caseJsonPreservation, caseJsonPreservationErrors, } from './preserve.js';
28
28
  import { businessEventProblems } from './business-event-shape.js';
29
- import { readEventFilter, eventFilterProblem, eventFilterTreeLeaf } from '../core/event-filters.js';
29
+ import { readEventFilterLeaf, eventFilterTreeLeaf } from '../core/event-filters.js';
30
30
  import { responseJsonSchema } from '../core/connector-response-schema.js';
31
31
  import { ruleShapeProblems } from './rule-options.js';
32
+ import { CompileRefusal } from '../core/compile-refusal.js';
32
33
  /**
33
34
  * Condition `displayName` allocator.
34
35
  *
@@ -135,7 +136,7 @@ export function serialize(built, opts = {}) {
135
136
  built.stages.forEach((stage, si) => {
136
137
  const sid = ids.stageId(`stage:${stage.label}`);
137
138
  if (resolver.stageIdByLabel.has(stage.label))
138
- throw new Error(`duplicate stage label "${stage.label}".`);
139
+ throw new CompileRefusal('DUP_STAGE', `duplicate stage label "${stage.label}".`);
139
140
  resolver.stageIdByLabel.set(stage.label, sid);
140
141
  stageIds.push(sid);
141
142
  taskIds.push([]);
@@ -143,7 +144,7 @@ export function serialize(built, opts = {}) {
143
144
  taskIds[si].push([]);
144
145
  lane.forEach((task) => {
145
146
  if (seenTaskNames.has(task.name))
146
- throw new Error(`duplicate task displayName "${task.name}" — task names must be unique across the case.`);
147
+ throw new CompileRefusal('DUP_TASK', `duplicate task displayName "${task.name}" — task names must be unique across the case.`);
147
148
  seenTaskNames.add(task.name);
148
149
  const tid = ids.taskId(`task:${stage.label}/${task.name}`);
149
150
  taskIds[si][li].push(tid);
@@ -187,13 +188,13 @@ export function serialize(built, opts = {}) {
187
188
  const selectNextStage = built.stages.flatMap((stage) => stage.exit)
188
189
  .filter((exit) => exit.selectNextStage !== undefined);
189
190
  if (selectNextStage.length > 1)
190
- throw new Error('select-next-stage can be configured on at most one stage exit per case.');
191
+ throw new CompileRefusal('SELECT_NEXT_STAGE_MULTIPLE', 'select-next-stage can be configured on at most one stage exit per case.');
191
192
  if (selectNextStage[0]?.selectNextStage) {
192
193
  const { objectName, connector } = selectNextStage[0].selectNextStage;
193
194
  if (selectNextStage[0].type !== 'wait-for-user')
194
- throw new Error('select-next-stage requires an exit with type "wait-for-user".');
195
+ throw new CompileRefusal('SELECT_NEXT_STAGE_EXIT_TYPE', 'select-next-stage requires an exit with type "wait-for-user".');
195
196
  if (!objectName.trim())
196
- throw new Error('select-next-stage objectName must be non-empty.');
197
+ throw new CompileRefusal('SELECT_NEXT_STAGE_OBJECT_NAME', 'select-next-stage objectName must be non-empty.');
197
198
  const context = [];
198
199
  if (connector) {
199
200
  context.push(io('connectorKey', 'string', { value: connector.connectorKey }));
@@ -234,7 +235,7 @@ export function serialize(built, opts = {}) {
234
235
  if (built.businessEvents?.length) {
235
236
  const [problem] = businessEventProblems(built.businessEvents);
236
237
  if (problem)
237
- throw new Error(problem.message);
238
+ throw new CompileRefusal(problem.code, problem.message);
238
239
  metadata.businessEvents = built.businessEvents.map(emitBusinessEvent);
239
240
  }
240
241
  // Variables: declared vars + the readable companions that REASSIGNED task
@@ -347,11 +348,11 @@ function applyAuthorLayout(result, authored) {
347
348
  const nodes = result.nodes;
348
349
  const layoutValue = result.layout;
349
350
  if (layoutValue !== undefined && (!layoutValue || typeof layoutValue !== 'object' || Array.isArray(layoutValue)))
350
- throw new Error('case layout: preserved layout must be an object before authored entries can be applied.');
351
+ throw new Error('internal SDK error: case layout: preserved layout must be an object before authored entries can be applied.');
351
352
  const existingLayout = (layoutValue ?? {});
352
353
  const existingNodes = existingLayout.nodes;
353
354
  if (existingNodes !== undefined && (!existingNodes || typeof existingNodes !== 'object' || Array.isArray(existingNodes)))
354
- throw new Error('case layout: preserved layout.nodes must be an object before authored entries can be applied.');
355
+ throw new Error('internal SDK error: case layout: preserved layout.nodes must be an object before authored entries can be applied.');
355
356
  const layoutNodes = { ...(existingNodes ?? {}) };
356
357
  const layout = { ...existingLayout, nodes: layoutNodes };
357
358
  for (const entry of entries) {
@@ -363,16 +364,16 @@ function applyAuthorLayout(result, authored) {
363
364
  return trigger && label === entry.name;
364
365
  });
365
366
  if (matches.length !== 1)
366
- throw new Error(`case layout: ${entry.kind} "${entry.name}" matches ${matches.length} emitted nodes; expected exactly 1.`);
367
+ throw new CompileRefusal('CASE_LAYOUT_NODE_MATCH', `case layout: ${entry.kind} "${entry.name}" matches ${matches.length} emitted nodes; expected exactly 1.`);
367
368
  const nodeId = matches[0].id;
368
369
  if (typeof nodeId !== 'string' || !nodeId)
369
- throw new Error(`case layout: ${entry.kind} "${entry.name}" has no emitted node id.`);
370
+ throw new Error(`internal SDK error: case layout: ${entry.kind} "${entry.name}" has no emitted node id.`);
370
371
  const unknown = Object.keys(entry.layout).filter((field) => !CASE_NODE_LAYOUT_FIELDS.includes(field));
371
372
  if (unknown.length)
372
- throw new Error(`case layout: ${entry.kind} "${entry.name}" has unsupported field "${unknown[0]}".`);
373
+ throw new CompileRefusal('CASE_LAYOUT_UNKNOWN_FIELD', `case layout: ${entry.kind} "${entry.name}" has unsupported field "${unknown[0]}".`);
373
374
  const previous = layoutNodes[nodeId];
374
375
  if (previous !== undefined && (!previous || typeof previous !== 'object' || Array.isArray(previous)))
375
- throw new Error(`case layout: preserved entry for ${entry.kind} "${entry.name}" must be an object.`);
376
+ throw new Error(`internal SDK error: case layout: preserved entry for ${entry.kind} "${entry.name}" must be an object.`);
376
377
  const merged = { ...(previous ?? {}) };
377
378
  for (const field of CASE_NODE_LAYOUT_FIELDS)
378
379
  if (entry.layout[field] !== undefined)
@@ -394,7 +395,7 @@ export function applyPreservation(result, preservation, options = {}) {
394
395
  return result;
395
396
  const errors = caseJsonPreservationErrors(preservation);
396
397
  if (errors.length)
397
- throw new Error(`invalid Case JSON preservation payload: ${errors.join('; ')}`);
398
+ throw new CompileRefusal('PRESERVATION_COLLISION', `invalid Case JSON preservation payload: ${errors.join('; ')}`);
398
399
  applyPreservedIdentities(result, preservation);
399
400
  Object.assign(result, preservation.topLevel ?? {});
400
401
  Object.assign(result.metadata, preservation.metadata ?? {});
@@ -402,7 +403,7 @@ export function applyPreservation(result, preservation, options = {}) {
402
403
  if (caseAppConfig && preservation.caseAppConfigFields) {
403
404
  for (const [field, value] of Object.entries(preservation.caseAppConfigFields.fields ?? {})) {
404
405
  if (!options.overwrite && Object.prototype.hasOwnProperty.call(caseAppConfig, field))
405
- throw new Error(`preserved caseAppConfig field "${field}" collides with serializer-owned output.`);
406
+ throw new CompileRefusal('PRESERVATION_FIELD_COLLISION', `preserved caseAppConfig field "${field}" collides with serializer-owned output.`);
406
407
  caseAppConfig[field] = value;
407
408
  }
408
409
  for (const entry of preservation.caseAppConfigFields.sections ?? []) {
@@ -411,7 +412,7 @@ export function applyPreservation(result, preservation, options = {}) {
411
412
  continue;
412
413
  for (const [field, value] of Object.entries(entry.fields)) {
413
414
  if (!options.overwrite && Object.prototype.hasOwnProperty.call(section, field))
414
- throw new Error(`preserved Case App section "${entry.id}" field "${field}" collides with serializer-owned output.`);
415
+ throw new CompileRefusal('PRESERVATION_FIELD_COLLISION', `preserved Case App section "${entry.id}" field "${field}" collides with serializer-owned output.`);
415
416
  section[field] = value;
416
417
  }
417
418
  }
@@ -431,10 +432,10 @@ export function applyPreservation(result, preservation, options = {}) {
431
432
  if (!row && options.overwrite)
432
433
  continue;
433
434
  if (!row)
434
- throw new Error(`preserved variable "${name}" was not regenerated in variables.${lane}.`);
435
+ throw new CompileRefusal('PRESERVATION_TARGET_MISSING', `preserved variable "${name}" was not regenerated in variables.${lane}.`);
435
436
  for (const [field, value] of Object.entries(fields)) {
436
437
  if (!options.overwrite && Object.prototype.hasOwnProperty.call(row, field))
437
- throw new Error(`preserved variable "${name}" field "${field}" collides with serializer-owned output.`);
438
+ throw new CompileRefusal('PRESERVATION_FIELD_COLLISION', `preserved variable "${name}" field "${field}" collides with serializer-owned output.`);
438
439
  row[field] = value;
439
440
  }
440
441
  }
@@ -446,7 +447,7 @@ export function applyPreservation(result, preservation, options = {}) {
446
447
  if (!row && options.overwrite)
447
448
  continue;
448
449
  if (!row)
449
- throw new Error(`legacy variable default "${name}" was not regenerated in variables.${lane}.`);
450
+ throw new CompileRefusal('PRESERVATION_TARGET_MISSING', `legacy variable default "${name}" was not regenerated in variables.${lane}.`);
450
451
  const encoded = JSON.stringify(value);
451
452
  // The legacy row exists to carry the NON-STRING spelling of a value the
452
453
  // serializer now writes as a string, so it is only applicable while the two
@@ -457,7 +458,7 @@ export function applyPreservation(result, preservation, options = {}) {
457
458
  if (row.default !== encoded) {
458
459
  if (options.overwrite)
459
460
  continue;
460
- throw new Error(`legacy variable default "${name}" does not match the serializer-owned default.`);
461
+ throw new CompileRefusal('PRESERVATION_LEGACY_DEFAULT_MISMATCH', `legacy variable default "${name}" does not match the serializer-owned default.`);
461
462
  }
462
463
  row.default = value;
463
464
  }
@@ -485,7 +486,7 @@ export function applyPreservation(result, preservation, options = {}) {
485
486
  if (task[field] === value)
486
487
  continue;
487
488
  if (!options.overwrite)
488
- throw new Error(`preserved task "${entry.stage}/${entry.task}" field "${field}" collides with serializer-owned output.`);
489
+ throw new CompileRefusal('PRESERVATION_FIELD_COLLISION', `preserved task "${entry.stage}/${entry.task}" field "${field}" collides with serializer-owned output.`);
489
490
  }
490
491
  task[field] = value;
491
492
  }
@@ -509,7 +510,7 @@ export function applyPreservation(result, preservation, options = {}) {
509
510
  if (!trigger && options.overwrite)
510
511
  continue;
511
512
  if (!trigger)
512
- throw new Error(`trigger ${index + 1} was not regenerated for preserved data fields.`);
513
+ throw new CompileRefusal('PRESERVATION_TARGET_MISSING', `trigger ${index + 1} was not regenerated for preserved data fields.`);
513
514
  Object.assign(trigger.data, fields);
514
515
  }
515
516
  if (preservation.nodes?.length) {
@@ -721,7 +722,7 @@ function emitVariables(vars, ids, triggerNodeByRef) {
721
722
  return { default: vd.default };
722
723
  const value = JSON.stringify(vd.default);
723
724
  if (value === undefined)
724
- throw new TypeError(`Case variable "${vd.name}" has a default that cannot be JSON encoded.`);
725
+ throw new CompileRefusal('CASE_VAR_DEFAULT_NOT_JSON', `Case variable "${vd.name}" has a default that cannot be JSON encoded.`);
725
726
  return { default: value };
726
727
  };
727
728
  const readableCompanion = (vd) => ({
@@ -748,7 +749,7 @@ function emitVariables(vars, ids, triggerNodeByRef) {
748
749
  // to the trigger node, so it does not also take a root companion.
749
750
  const node = triggerNodeByRef.get(vd.sourceTrigger);
750
751
  if (!node)
751
- throw new Error(`In-arg "${vd.name}" is bound via { from } to a trigger that was not added with .trigger(...).`);
752
+ throw new CompileRefusal('CASE_IN_ARG_TRIGGER_NOT_ADDED', `In-arg "${vd.name}" is bound via { from } to a trigger that was not added with .trigger(...).`);
752
753
  entry.default = vd.default === undefined ? '' : encodedDefault(vd).default;
753
754
  entry.elementId = node.id;
754
755
  inputs.push(entry);
@@ -1038,7 +1039,7 @@ function withOutputExtracts(data, task, ids, resolver) {
1038
1039
  const own = Array.isArray(data.outputs) ? data.outputs : [];
1039
1040
  const elementId = own.find((row) => typeof row.elementId === 'string')?.elementId;
1040
1041
  if (!elementId)
1041
- throw new Error(`task "${task.name}": output extracts need the task's resolved response output.`);
1042
+ throw new CompileRefusal('TASK_OUTPUTS_NO_RESPONSE', `task "${task.name}": output extracts need the task's resolved response output.`);
1042
1043
  data.outputs = [
1043
1044
  ...own,
1044
1045
  ...task.outputs.map((o, i) => emitTaskOutput(o, elementId, ids, `${elementId}:out:${i}:${o.var}`, resolver)),
@@ -1076,7 +1077,7 @@ function isResolvedSubscription(spec) {
1076
1077
  * by the archived v1 artifacts. Placeholder bags never enter this path. */
1077
1078
  function resolvedSubscriptionBag(kind, spec, resolver, elementId, where) {
1078
1079
  if (!resolver.library)
1079
- throw new Error(`${kind === 'trigger' ? 'event trigger' : 'wait-for-connector'} needs the connector library — `
1080
+ throw new CompileRefusal('LIBRARY_REQUIRED', `${kind === 'trigger' ? 'event trigger' : 'wait-for-connector'} needs the connector library — `
1080
1081
  + `pass { library } to serialize or use uip maestro case compile --library.`);
1081
1082
  const rc = resolveCaseSubscription(resolver.library, kind, spec, { where, bindings: resolver.bindings });
1082
1083
  const op = rc.entry.operation ?? {};
@@ -1121,12 +1122,13 @@ function subscriptionMetadata(rc, op, filters, elementId) {
1121
1122
  filters: filters.map((entry, index) => {
1122
1123
  // Same authored shape as a flow's event filter, same designer grammar (see
1123
1124
  // `event-filters.ts`); a malformed leaf is refused rather than dropped.
1124
- const { read } = readEventFilter(entry);
1125
- if (!read)
1126
- throw new Error(`event filter ${index} must carry a field and exactly one operator — got ${JSON.stringify(entry)}.`);
1127
- const problem = eventFilterProblem(read);
1128
- if (problem)
1129
- throw new Error(`event filter ${index} on \`${read.field}\`: ${problem}`);
1125
+ // The code `check` gives the same leaf (`core/event-checks.ts`).
1126
+ const judged = readEventFilterLeaf(entry);
1127
+ if (judged.code !== undefined)
1128
+ throw new CompileRefusal(judged.code, judged.problem !== undefined
1129
+ ? `event filter ${index} on \`${judged.read?.field}\`: ${judged.problem}`
1130
+ : `event filter ${index} must carry a field and exactly one operator — got ${JSON.stringify(entry)}.`);
1131
+ const { read } = judged;
1130
1132
  return {
1131
1133
  ...eventFilterTreeLeaf(read),
1132
1134
  uiId: stableUuid(`${elementId}:${read.field}:${String(read.value)}:${index}`),
@@ -1393,14 +1395,14 @@ function referenceBindings(kind, authored, resolver, ids, taskName) {
1393
1395
  // literal value, so the task binds exactly as the literal form does.
1394
1396
  const ref = resolveResourceRef(authored, resolver.bindings, `Task "${taskName}"`);
1395
1397
  if (isResourceLabelRefusal(ref))
1396
- throw new Error(`[${ref.code}] ${ref.message}`);
1398
+ throw new CompileRefusal(ref.code, ref.message);
1397
1399
  const binding = kind === 'business-rule'
1398
1400
  ? { resource: BUSINESS_RULE_RESOURCE }
1399
1401
  : kind === 'action'
1400
1402
  ? { resource: ACTION_APP_RESOURCE }
1401
1403
  : ownEntry(REFERENCE_BINDING, kind);
1402
1404
  if (!binding)
1403
- throw new Error(`task kind "${kind}" is not a published-resource task.`);
1405
+ throw new Error(`internal SDK error: task kind "${kind}" is not a published-resource task.`);
1404
1406
  // An in-solution resource (`folder: 'solution_folder'`) keeps the sentinel
1405
1407
  // in its key and binds an empty folderPath — see resource-identity.ts.
1406
1408
  const { resourceKey, folderPath } = resourceIdentity(ref.name, ref.folderPath);
@@ -1459,7 +1461,7 @@ function businessRuleTaskData(task, resolver, elementId, ids) {
1459
1461
  */
1460
1462
  function connectorTaskData(spec, resolver, elementId, where, executionType) {
1461
1463
  if (!resolver.library)
1462
- throw new Error(`connector task needs the connector library — pass { library } to serialize or use uip maestro case compile --library.`);
1464
+ throw new CompileRefusal('LIBRARY_REQUIRED', `connector task needs the connector library — pass { library } to serialize or use uip maestro case compile --library.`);
1463
1465
  const rc = resolver.library.resolve(spec.key, spec.action, spec.version, spec.object);
1464
1466
  const op = rc.entry.operation ?? {};
1465
1467
  const context = [
@@ -1527,17 +1529,17 @@ function connectorTaskData(spec, resolver, elementId, where, executionType) {
1527
1529
  */
1528
1530
  function externalTaskData(kind, spec, resolver, elementId, where) {
1529
1531
  if (!resolver.library)
1530
- throw new Error(`${kind} task needs the connector library — pass { library } to serialize or use uip maestro case compile --library.`);
1532
+ throw new CompileRefusal('LIBRARY_REQUIRED', `${kind} task needs the connector library — pass { library } to serialize or use uip maestro case compile --library.`);
1531
1533
  const rc = resolver.library.resolve(spec.connector.key, spec.connector.action, spec.connector.version, spec.connector.object);
1532
1534
  const refused = externalDescriptorRefusal(kind, rc);
1533
1535
  if (refused)
1534
- throw new Error(refused);
1536
+ throw new CompileRefusal(EXTERNAL_TASK_DESCRIPTOR_CONTRACT, refused);
1535
1537
  if (!isExternalExecutionMode(spec.mode))
1536
- throw new Error(`${kind} task mode must be the connector object's executionType: "sync", "async" or "hybrid".`);
1538
+ throw new CompileRefusal('EXTERNAL_TASK_MODE', `${kind} task mode must be the connector object's executionType: "sync", "async" or "hybrid".`);
1537
1539
  if (spec.mode === 'hybrid')
1538
- throw new Error(externalHybridRefusal(where));
1540
+ throw new CompileRefusal(EXTERNAL_TASK_HYBRID_UNSUPPORTED, externalHybridRefusal(where));
1539
1541
  if (!spec.connector.connection || !spec.connector.folder)
1540
- throw new Error(`${kind} task requires non-empty connection and folder binding names.`);
1542
+ throw new CompileRefusal(spec.connector.connection ? 'EXTERNAL_TASK_FOLDER' : 'EXTERNAL_TASK_CONNECTION', `${kind} task requires non-empty connection and folder binding names.`);
1541
1543
  const data = connectorTaskData(spec.connector, resolver, elementId, where, spec.mode);
1542
1544
  const context = data.context;
1543
1545
  const metadataAt = context.findIndex((row) => row.name === 'metadata');
@@ -1642,7 +1644,7 @@ function folderBindingRef(resolver, name, connection, site) {
1642
1644
  for (const row of resolver.bindingSink.values()) {
1643
1645
  if (row.resourceKey === connection.resourceKey && row.propertyAttribute === 'folderKey'
1644
1646
  && row.default !== folderKey)
1645
- throw new Error(`${site.where}: connection "${connection.resourceKey}" is bound to two folders: `
1647
+ throw new CompileRefusal('BINDING_CONNECTION_TWO_FOLDERS', `${site.where}: connection "${connection.resourceKey}" is bound to two folders: `
1646
1648
  + `"${String(row.name)}" (${String(row.default)}) and "${name}" (${folderKey}). `
1647
1649
  + `A connection lives in one folder; use the same folder binding for every task on it.`);
1648
1650
  }
@@ -1826,13 +1828,13 @@ function emitRules(rules, seed, ownerId, ids, resolver, site) {
1826
1828
  function emitRule(r, seed, ownerId, ids, resolver, site) {
1827
1829
  const [shape] = ruleShapeProblems(r);
1828
1830
  if (shape)
1829
- throw new Error(`${shape.message.replace(/^Rule /, 'rule ')} (in ${site})`);
1831
+ throw new CompileRefusal(shape.code, `${shape.message.replace(/^Rule /, 'rule ')} (in ${site})`);
1830
1832
  const ruleId = ids.ruleId(seed);
1831
1833
  const out = { rule: r.rule, id: ruleId };
1832
1834
  if (r.rule === 'selected-stage-completed' || r.rule === 'selected-stage-exited') {
1833
1835
  const sid = r.stage && resolver.stageIdByLabel.get(r.stage);
1834
1836
  if (!sid)
1835
- throw new Error(`rule "${r.rule}" references unknown stage "${r.stage}".`);
1837
+ throw new CompileRefusal('UNKNOWN_STAGE_REF', `rule "${r.rule}" references unknown stage "${r.stage}".`);
1836
1838
  out[CASE_FORMAT_PROFILE.selectedStageField] = CASE_FORMAT_PROFILE.selectedStageCardinality === 'singular' ? sid : [sid];
1837
1839
  }
1838
1840
  if (r.rule === 'selected-tasks-completed') {
@@ -1840,7 +1842,7 @@ function emitRule(r, seed, ownerId, ids, resolver, site) {
1840
1842
  out.selectedTasksIds = names.map((n) => {
1841
1843
  const tid = resolver.taskIdByKey.get(n);
1842
1844
  if (!tid)
1843
- throw new Error(`rule "selected-tasks-completed" references unknown task "${n}".`);
1845
+ throw new CompileRefusal('UNKNOWN_TASK_REF', `rule "selected-tasks-completed" references unknown task "${n}".`);
1844
1846
  return tid;
1845
1847
  });
1846
1848
  }
@@ -1853,12 +1855,12 @@ function emitRule(r, seed, ownerId, ids, resolver, site) {
1853
1855
  // preallocated id. `slaId` alone = a breach rule; adding `escalationId` = at-risk.
1854
1856
  const slaId = r.sla && resolver.slaIdByName.get(r.sla);
1855
1857
  if (!slaId)
1856
- throw new Error(`rule "sla-status-change" references unknown SLA "${r.sla}" — declare it with .sla({ displayName: "${r.sla}", … }).`);
1858
+ throw new CompileRefusal('UNKNOWN_SLA_REF', `rule "sla-status-change" references unknown SLA "${r.sla}" — declare it with .sla({ displayName: "${r.sla}", … }).`);
1857
1859
  out.slaId = slaId;
1858
1860
  if (r.escalation) {
1859
1861
  const escId = resolver.escalationIdByName.get(r.escalation);
1860
1862
  if (!escId)
1861
- throw new Error(`rule "sla-status-change" references unknown escalation "${r.escalation}".`);
1863
+ throw new CompileRefusal('UNKNOWN_ESCALATION_REF', `rule "sla-status-change" references unknown escalation "${r.escalation}".`);
1862
1864
  out.escalationId = escId;
1863
1865
  }
1864
1866
  }
@@ -1866,9 +1868,9 @@ function emitRule(r, seed, ownerId, ids, resolver, site) {
1866
1868
  // The event is matched by its exact name at run time; a rule without one
1867
1869
  // listens for nothing, so both a missing and an undeclared name refuse.
1868
1870
  if (!r.businessEvent)
1869
- throw new Error(`rule "api-event" in ${site} names no business event — write rule('api-event', { businessEvent: '<name>' }) with a name declared by .businessEvent('<name>').`);
1871
+ throw new CompileRefusal('API_EVENT_NO_BUSINESS_EVENT', `rule "api-event" in ${site} names no business event — write rule('api-event', { businessEvent: '<name>' }) with a name declared by .businessEvent('<name>').`);
1870
1872
  if (!resolver.businessEvents.has(r.businessEvent))
1871
- throw new Error(`rule "api-event" in ${site} listens for business event "${r.businessEvent}", which the case does not declare — add .businessEvent(${JSON.stringify(r.businessEvent)}).`);
1873
+ throw new CompileRefusal('UNKNOWN_BUSINESS_EVENT', `rule "api-event" in ${site} listens for business event "${r.businessEvent}", which the case does not declare — add .businessEvent(${JSON.stringify(r.businessEvent)}).`);
1872
1874
  out.businessEventType = r.businessEvent;
1873
1875
  }
1874
1876
  if (r.expression)
@@ -1939,7 +1941,7 @@ function emitExit(c, seed, ownerId, label, ids, resolver) {
1939
1941
  if (c.exitToStage) {
1940
1942
  const sid = resolver.stageIdByLabel.get(c.exitToStage);
1941
1943
  if (!sid)
1942
- throw new Error(`exit condition routes to unknown stage "${c.exitToStage}".`);
1944
+ throw new CompileRefusal('UNKNOWN_EXIT_STAGE', `exit condition routes to unknown stage "${c.exitToStage}".`);
1943
1945
  out.exitToStageId = sid;
1944
1946
  }
1945
1947
  return out;
@@ -23,14 +23,16 @@ export function registerFlowCommands(program) {
23
23
  .option('--library <dir>', 'connector library directory')
24
24
  .option('--bindings <file>', 'bindings.json path')
25
25
  .option('--connectors-local <dir>', 'connection-resolved connector overlay')
26
- .option('--no-check', 'skip the source-level check')
26
+ .option('--no-check', 'deprecated, does nothing: compile no longer runs check')
27
27
  .action(async (source, options) => {
28
28
  const argv = [source];
29
29
  addValue(argv, '-o', options.output);
30
30
  addValue(argv, '--library', options.library);
31
31
  addValue(argv, '--bindings', options.bindings);
32
32
  addValue(argv, '--connectors-local', options.connectorsLocal);
33
- if (options.check === false || emitOnlyEnabled())
33
+ // Passed on only when given, so compile prints its deprecation warning.
34
+ // Emit-only mode needs nothing here: compile does not run check.
35
+ if (options.check === false)
34
36
  argv.push('--no-check');
35
37
  await invoke('flow compile', () => import('../../compile-cli.js'), argv);
36
38
  });
@@ -10,12 +10,17 @@
10
10
  * --library defaults to $FLOW_SDK_LIBRARY_JSON; --bindings defaults to
11
11
  * .flow-sdk/bindings.json (or a legacy ./bindings.json) when present. Connector actions require a library.
12
12
  *
13
- * `--no-check` skips the SDK's static `check()` pass and emits whatever
14
- * `.build()` produced. It exists for loops whose verification is somewhere else
15
- * entirely — e.g. authoring against the product CLI (`uip maestro flow
16
- * validate` / `debug`), where a pre-emit static pass would answer questions the
17
- * product's own gates are supposed to answer. `.build()` still throws on
18
- * structural problems; only the diagnostic pass is skipped.
13
+ * `compile` only serializes (docs/CHECK_COMPILE_CONTRACT_DESIGN.md D1, D2). It
14
+ * does not run `check()`'s lint rules: run `flow check` first for those. It
15
+ * stops only when it cannot write a correct `.flow`, a refusal the serializer
16
+ * throws. A draft node (a connector with no connection, an empty required
17
+ * input) the canvas saved is written, with a warning: decompile marks its step
18
+ * `{ draft: true }` and carries its definition in `.definitions([...])`. Any
19
+ * other draft, such as one an agent just wrote (even of an operation the canvas
20
+ * has a draft of), is refused.
21
+ *
22
+ * `--no-check` is a deprecated no-op: it is accepted for one minor version, with
23
+ * a warning, and then removed.
19
24
  */
20
25
  import { diagnosticLine, reportDiagnostics, reportResult, thrownDiagnostic } from './cli-result.js';
21
26
  import { writeJsonFile } from './core/json-file.js';
@@ -24,8 +29,7 @@ import { basename, dirname, join, relative, resolve } from 'node:path';
24
29
  import { resolveFlowFile, loadBuiltFlow } from './load.js';
25
30
  import { serializeWithSidecars } from './serialize.js';
26
31
  import { compileOwnsResource } from './core/agent-tool-resources.js';
27
- import { check } from './check.js';
28
- import { flowCheckOpts, openCliLibrary } from './cli-library.js';
32
+ import { openCliLibrary } from './cli-library.js';
29
33
  import { Bindings } from './bindings.js';
30
34
  import { flagValue, runWhenInvokedDirectly } from './cli-run.js';
31
35
  import { connectionResources, inlineAgentResources, mergeBindingsV2, refreshInlineAgentRows } from './bindings-v2.js';
@@ -35,8 +39,8 @@ import { defaultBindingsFile } from './workdir.js';
35
39
  import { isWithin } from './path-within.js';
36
40
  import { flowSources, resourceSourcesOfOtherFlows } from './project-resource-users.js';
37
41
  import { fnv1aX4Uuid } from './core/stable-id.js';
38
- import { BindingRefusal } from './core/binding-messages.js';
39
- import { ByoaRefusal } from './core/byoa.js';
42
+ import { CompileRefusal } from './core/compile-refusal.js';
43
+ import { cliSpelling } from './core/cli-spelling.js';
40
44
  /** An agent folder name: a uuid, any case. */
41
45
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
42
46
  export async function run(argv) {
@@ -45,7 +49,9 @@ export async function run(argv) {
45
49
  return 2;
46
50
  }
47
51
  const flags = new Set(['-o', '--library', '--bindings', '--connectors-local']);
48
- const noCheck = argv.includes('--no-check');
52
+ if (argv.includes('--no-check')) {
53
+ console.error(`compile: --no-check is deprecated and does nothing: compile no longer runs check. Run \`${cliSpelling()} flow check\` before \`compile\`; the flag will be removed.`);
54
+ }
49
55
  const pos = argv.find((a, i) => !flags.has(a) && !flags.has(argv[i - 1]));
50
56
  if (!pos) {
51
57
  console.error('compile: missing <flow.ts | BaseName>');
@@ -66,37 +72,20 @@ export async function run(argv) {
66
72
  // (e.g. HTTP-only) then compile fine even if the connector cache isn't built.
67
73
  // A connector action with no library resolves to a clear error in serialize().
68
74
  //
69
- // Loaded BEFORE check() on purpose: one of check's rules (whether a connector
70
- // event operation declares any event parameters) can only be answered from the
71
- // library, and answering it from an assumption instead is what made
72
- // EVENT_NO_SCOPE an error for HTTP Webhook, whose only correct `where` is empty.
73
75
  const library = openCliLibrary(argv);
74
76
  const bindFile = flagValue(argv, '--bindings') ?? defaultBindingsFile();
75
77
  const bindings = new Bindings(bindFile);
76
78
  // One list per run; every exit path reports it (reportResult overwrites keys).
77
79
  const diagnostics = [];
78
- if (!noCheck) {
79
- // Same options as `flow check`, so the two cannot disagree.
80
- const diags = check(built, flowCheckOpts(library, bindings));
81
- const errors = diags.filter((d) => d.level === 'error');
82
- for (const d of diags)
83
- console.error(` ${diagnosticLine(d)}`);
84
- diagnostics.push(...diags);
85
- if (errors.length) {
86
- reportDiagnostics(diagnostics);
87
- console.error(`compile: ${errors.length} error(s); not emitting.`);
88
- return 1;
89
- }
90
- }
91
80
  // The serializer's own observations — a connection or folder value that can
92
- // never be a tenant resource key — printed in the same `⚠ [CODE]` shape as
93
- // check()'s. Warnings are never fatal: the .flow is still written, exit stays 0.
94
- // A binding LABEL the serializer refuses (its own name as the resourceKey, or a
95
- // label a populated bindings.json does not declare) is an error, not a
96
- // warning, and so is a BYOA-only event on a connection `prepare` never checked
97
- // (EVENT_BYOA_UNVERIFIED): the check pass above reports each first, and this is
98
- // the same refusal in the same `✗ [CODE]` shape when that pass was skipped
99
- // (`--no-check`, or an emit-only project). Nothing is written.
81
+ // never be a tenant resource key, a draft node written from its carried
82
+ // definition — printed in the same `⚠ [CODE]` shape as check()'s. Warnings are
83
+ // never fatal: the .flow is still written, exit stays 0. A binding LABEL the
84
+ // serializer refuses (its own name as the resourceKey, or a label a populated
85
+ // bindings.json does not declare) is an error, not a warning, and so is a
86
+ // BYOA-only event on a connection `prepare` never checked
87
+ // (EVENT_BYOA_UNVERIFIED): printed in the same `✗ [CODE]` shape `check` uses
88
+ // for it. Nothing is written.
100
89
  const out = flagValue(argv, '-o') ?? `${built.name || basename(file).replace(/\.flow\.ts$|\.ts$/, '')}.flow`;
101
90
  const outDir = dirname(resolve(out));
102
91
  // ONE FOLDER, ONE FLOW (#1184). An agent's `source` is the name of the folder
@@ -147,28 +136,37 @@ export async function run(argv) {
147
136
  const warnings = [];
148
137
  let emitted;
149
138
  try {
150
- emitted = serializeWithSidecars(built, { library, bindings, warn: (w) => warnings.push(w), agentSource });
139
+ emitted = serializeWithSidecars(built, {
140
+ library, bindings, warn: (w) => warnings.push(w), agentSource,
141
+ carriedDrafts: (d) => warnings.push(draftWarning(d)),
142
+ });
151
143
  }
152
144
  catch (error) {
153
145
  for (const w of warnings)
154
- console.error(` ⚠ [${w.code}] ${w.message}`);
146
+ console.error(` ${diagnosticLine(serializeWarningDiagnostic(w))}`);
155
147
  diagnostics.push(...warnings.map(serializeWarningDiagnostic));
156
- if (!(error instanceof BindingRefusal || error instanceof ByoaRefusal)) {
157
- // Any other throw still ends the run through the caller (exit 1, `--json`
158
- // `error`); the list is reported first.
148
+ if (!(error instanceof CompileRefusal)) {
149
+ // Any other throw (an internal SDK error) still ends the run through the
150
+ // caller (exit 1, `--json` `error`); the list is reported first.
159
151
  diagnostics.push(thrownDiagnostic(error));
160
152
  reportDiagnostics(diagnostics);
161
153
  throw error;
162
154
  }
163
- console.error(` ✗ [${error.code}] ${error.message}`);
164
- diagnostics.push({ level: 'error', code: error.code, message: error.message });
155
+ // A coded refusal: the serializer cannot write a correct `.flow`. Printed in
156
+ // `check`'s `✗ [CODE]` shape, which `check` reports for it too.
157
+ const refused = {
158
+ level: 'error', code: error.code, message: error.message,
159
+ ...(error.suggestion !== undefined ? { suggestion: error.suggestion } : {}),
160
+ };
161
+ console.error(` ${diagnosticLine(refused)}`);
162
+ diagnostics.push(refused);
165
163
  reportDiagnostics(diagnostics);
166
164
  console.error('compile: 1 error(s); not emitting.');
167
165
  return 1;
168
166
  }
169
167
  const { flow: flowJson, sidecars } = emitted;
170
168
  for (const w of warnings)
171
- console.error(` ⚠ [${w.code}] ${w.message}`);
169
+ console.error(` ${diagnosticLine(serializeWarningDiagnostic(w))}`);
172
170
  diagnostics.push(...warnings.map(serializeWarningDiagnostic));
173
171
  // An inline agent is TWO artifacts: the node, and the agent's own configuration
174
172
  // at `<source>/agent.json` — which is where flow-v1's serializer points the
@@ -325,7 +323,19 @@ export async function run(argv) {
325
323
  return 0;
326
324
  }
327
325
  function serializeWarningDiagnostic(w) {
328
- return { level: 'warning', code: w.code, message: w.message };
326
+ return { level: 'warning', code: w.code, message: w.message, ...(w.step !== undefined ? { step: w.step } : {}) };
327
+ }
328
+ /**
329
+ * A draft node written from its carried definition, as a warning: the node is
330
+ * in the `.flow` as the canvas saved it, unfinished. For instance
331
+ * `⚠ [BINDING_MISSING] … written as a draft from its carried definition`.
332
+ */
333
+ function draftWarning(d) {
334
+ return {
335
+ code: String(d.code),
336
+ message: `${d.message} Written as a draft, with the definition the source carries; finish the node before the flow runs.`,
337
+ ...(d.step !== undefined ? { step: d.step } : {}),
338
+ };
329
339
  }
330
340
  runWhenInvokedDirectly(import.meta.url, 'compile', run);
331
341
  /** A JSON file's contents, or `undefined` when it is missing or unreadable. */
@@ -98,6 +98,26 @@ export declare function readEventFilter(leaf: unknown): {
98
98
  keys: EventFilterOperatorKey[];
99
99
  read?: ReadEventFilter;
100
100
  };
101
+ /** A code `check` gives a filter leaf it refuses (`core/event-checks.ts`). */
102
+ export type EventFilterLeafCode = 'EVENT_FILTER_NO_FIELD' | 'EVENT_FILTER_MANY_OPERATORS' | 'EVENT_FILTER_NO_VALUE' | 'EVENT_FILTER_BACKSLASH' | 'EVENT_FILTER_BAD_VALUE';
103
+ /**
104
+ * Read an authored filter leaf and judge it the way `check` does: the first
105
+ * code `core/event-checks.ts` gives the leaf, in the same order, or the read
106
+ * condition when the leaf lowers. A serializer that refuses a bad leaf with one
107
+ * message calls this, so its code cannot drift from check's.
108
+ *
109
+ * @example
110
+ * readEventFilterLeaf({ field: 'subject', contains: 'Report' }) // { read: { field: 'subject', operator: 'contains', value: 'Report' } }
111
+ * readEventFilterLeaf({ contains: 'Report' }) // { code: 'EVENT_FILTER_NO_FIELD', read: { field: '', ... } }
112
+ */
113
+ export declare function readEventFilterLeaf(leaf: unknown): {
114
+ code?: undefined;
115
+ read: ReadEventFilter;
116
+ } | {
117
+ code: EventFilterLeafCode;
118
+ read?: ReadEventFilter;
119
+ problem?: string;
120
+ };
101
121
  export declare function isIsoDateString(value: unknown): value is string;
102
122
  /**
103
123
  * Why a filter cannot be lowered, or `undefined` when it can. Two shapes are
@@ -81,6 +81,33 @@ export function readEventFilter(leaf) {
81
81
  return { keys };
82
82
  return { keys, read: { field: String(f.field ?? ''), operator: keys[0], value } };
83
83
  }
84
+ /**
85
+ * Read an authored filter leaf and judge it the way `check` does: the first
86
+ * code `core/event-checks.ts` gives the leaf, in the same order, or the read
87
+ * condition when the leaf lowers. A serializer that refuses a bad leaf with one
88
+ * message calls this, so its code cannot drift from check's.
89
+ *
90
+ * @example
91
+ * readEventFilterLeaf({ field: 'subject', contains: 'Report' }) // { read: { field: 'subject', operator: 'contains', value: 'Report' } }
92
+ * readEventFilterLeaf({ contains: 'Report' }) // { code: 'EVENT_FILTER_NO_FIELD', read: { field: '', ... } }
93
+ */
94
+ export function readEventFilterLeaf(leaf) {
95
+ const { keys, read } = readEventFilter(leaf);
96
+ const field = leaf?.field;
97
+ if (typeof field !== 'string' || field === '')
98
+ return { code: 'EVENT_FILTER_NO_FIELD', read };
99
+ if (keys.length > 1)
100
+ return { code: 'EVENT_FILTER_MANY_OPERATORS' };
101
+ if (!read)
102
+ return { code: 'EVENT_FILTER_NO_VALUE' };
103
+ const problem = eventFilterProblem(read);
104
+ const ambiguous = typeof read.value === 'string' ? jmesStringProblem(read.value) : undefined;
105
+ if (ambiguous)
106
+ return { code: 'EVENT_FILTER_BACKSLASH', read, problem: problem ?? ambiguous };
107
+ if (problem)
108
+ return { code: 'EVENT_FILTER_BAD_VALUE', read, problem };
109
+ return { read };
110
+ }
84
111
  /** An ISO-8601 calendar date, optionally with a time part — what the designer serializes a date filter as. */
85
112
  const ISO_DATE = /^\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
86
113
  export function isIsoDateString(value) {