@synap-core/workspace-templates 0.9.2 → 0.9.3

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/index.js CHANGED
@@ -6841,14 +6841,13 @@ var DATA = {
6841
6841
  "valueType": "string",
6842
6842
  "inputType": "select",
6843
6843
  "enumValues": [
6844
- "manual",
6845
- "referral",
6844
+ "cold-outreach",
6846
6845
  "linkedin",
6847
- "email",
6846
+ "twitter",
6847
+ "landing-page",
6848
+ "inbound-call",
6849
+ "referral",
6848
6850
  "event",
6849
- "inbound",
6850
- "partner",
6851
- "import",
6852
6851
  "other"
6853
6852
  ]
6854
6853
  },
@@ -7218,6 +7217,37 @@ var DATA = {
7218
7217
  "label": "Notes",
7219
7218
  "valueType": "string",
7220
7219
  "inputType": "textarea"
7220
+ },
7221
+ {
7222
+ "slug": "campaign-type",
7223
+ "label": "Type",
7224
+ "valueType": "string",
7225
+ "inputType": "select",
7226
+ "enumValues": [
7227
+ "scoping",
7228
+ "outreach",
7229
+ "live",
7230
+ "partnership",
7231
+ "event",
7232
+ "other"
7233
+ ],
7234
+ "constraints": {
7235
+ "defaultValue": "outreach"
7236
+ }
7237
+ },
7238
+ {
7239
+ "slug": "campaign-color",
7240
+ "label": "Color",
7241
+ "valueType": "string",
7242
+ "inputType": "select",
7243
+ "enumValues": [
7244
+ "indigo",
7245
+ "violet",
7246
+ "amber",
7247
+ "cyan",
7248
+ "pink",
7249
+ "teal"
7250
+ ]
7221
7251
  }
7222
7252
  ]
7223
7253
  }
@@ -8922,6 +8952,37 @@ var DATA = {
8922
8952
  "label": "Notes",
8923
8953
  "valueType": "string",
8924
8954
  "inputType": "textarea"
8955
+ },
8956
+ {
8957
+ "slug": "campaign-type",
8958
+ "label": "Type",
8959
+ "valueType": "string",
8960
+ "inputType": "select",
8961
+ "enumValues": [
8962
+ "scoping",
8963
+ "outreach",
8964
+ "live",
8965
+ "partnership",
8966
+ "event",
8967
+ "other"
8968
+ ],
8969
+ "constraints": {
8970
+ "defaultValue": "outreach"
8971
+ }
8972
+ },
8973
+ {
8974
+ "slug": "campaign-color",
8975
+ "label": "Color",
8976
+ "valueType": "string",
8977
+ "inputType": "select",
8978
+ "enumValues": [
8979
+ "indigo",
8980
+ "violet",
8981
+ "amber",
8982
+ "cyan",
8983
+ "pink",
8984
+ "teal"
8985
+ ]
8925
8986
  }
8926
8987
  ]
8927
8988
  },
@@ -10024,6 +10085,12 @@ var DATA = {
10024
10085
  ],
10025
10086
  "isPublic": true
10026
10087
  },
10088
+ "app": {
10089
+ "kind": "url",
10090
+ "appId": "crm",
10091
+ "url": "https://crm.synap.live",
10092
+ "title": "CRM"
10093
+ },
10027
10094
  "dependencies": [
10028
10095
  {
10029
10096
  "slug": "foundation",
@@ -10094,7 +10161,17 @@ var DATA = {
10094
10161
  "slug": "source",
10095
10162
  "label": "Source",
10096
10163
  "valueType": "string",
10097
- "inputType": "text"
10164
+ "inputType": "select",
10165
+ "enumValues": [
10166
+ "cold-outreach",
10167
+ "linkedin",
10168
+ "twitter",
10169
+ "landing-page",
10170
+ "inbound-call",
10171
+ "referral",
10172
+ "event",
10173
+ "other"
10174
+ ]
10098
10175
  },
10099
10176
  {
10100
10177
  "slug": "icpScore",
@@ -10183,7 +10260,17 @@ var DATA = {
10183
10260
  "slug": "source",
10184
10261
  "label": "Source",
10185
10262
  "valueType": "string",
10186
- "inputType": "text"
10263
+ "inputType": "select",
10264
+ "enumValues": [
10265
+ "cold-outreach",
10266
+ "linkedin",
10267
+ "twitter",
10268
+ "landing-page",
10269
+ "inbound-call",
10270
+ "referral",
10271
+ "event",
10272
+ "other"
10273
+ ]
10187
10274
  },
10188
10275
  {
10189
10276
  "slug": "icpScore",
@@ -22039,6 +22126,37 @@ var DATA = {
22039
22126
  "label": "Notes",
22040
22127
  "valueType": "string",
22041
22128
  "inputType": "textarea"
22129
+ },
22130
+ {
22131
+ "slug": "campaign-type",
22132
+ "label": "Type",
22133
+ "valueType": "string",
22134
+ "inputType": "select",
22135
+ "enumValues": [
22136
+ "scoping",
22137
+ "outreach",
22138
+ "live",
22139
+ "partnership",
22140
+ "event",
22141
+ "other"
22142
+ ],
22143
+ "constraints": {
22144
+ "defaultValue": "outreach"
22145
+ }
22146
+ },
22147
+ {
22148
+ "slug": "campaign-color",
22149
+ "label": "Color",
22150
+ "valueType": "string",
22151
+ "inputType": "select",
22152
+ "enumValues": [
22153
+ "indigo",
22154
+ "violet",
22155
+ "amber",
22156
+ "cyan",
22157
+ "pink",
22158
+ "teal"
22159
+ ]
22042
22160
  }
22043
22161
  ]
22044
22162
  },
@@ -28861,6 +28979,339 @@ function groupByDomain(items, domainOf) {
28861
28979
  }));
28862
28980
  }
28863
28981
 
28982
+ // src/validate-flow.ts
28983
+ var FLOW_NODE_TYPES = [
28984
+ "trigger",
28985
+ "command",
28986
+ "condition",
28987
+ "delay",
28988
+ "output",
28989
+ "loop",
28990
+ "transform",
28991
+ "fetch",
28992
+ "query",
28993
+ "entity_read",
28994
+ "related_entities",
28995
+ "guard",
28996
+ "compute",
28997
+ "select",
28998
+ "claim",
28999
+ "messages_query",
29000
+ "switch",
29001
+ "skill",
29002
+ "capability",
29003
+ "sub_automation",
29004
+ "playbook_run"
29005
+ ];
29006
+ var FLOW_OUTPUT_TYPES = [
29007
+ "notification",
29008
+ "entity_create",
29009
+ "entity_update",
29010
+ "facet_attach",
29011
+ "facet_update",
29012
+ "facet_detach",
29013
+ "relation_create",
29014
+ "webhook",
29015
+ "channel_message",
29016
+ "session_update",
29017
+ "set_state"
29018
+ ];
29019
+ var CHANNEL_MESSAGE_CHANNEL_TYPES = [
29020
+ "personal_thread",
29021
+ "proactive",
29022
+ "subjectEntity"
29023
+ ];
29024
+ var NODE_TYPE_SET = new Set(FLOW_NODE_TYPES);
29025
+ var OUTPUT_TYPE_SET = new Set(FLOW_OUTPUT_TYPES);
29026
+ var CHANNEL_TYPE_SET = new Set(CHANNEL_MESSAGE_CHANNEL_TYPES);
29027
+ function isRecord(v) {
29028
+ return typeof v === "object" && v !== null && !Array.isArray(v);
29029
+ }
29030
+ function nonEmptyString(v) {
29031
+ return typeof v === "string" && v.trim().length > 0;
29032
+ }
29033
+ function validateFlowDefinition(flow, resolvers) {
29034
+ const errors = [];
29035
+ if (!isRecord(flow)) {
29036
+ return {
29037
+ valid: false,
29038
+ errors: [
29039
+ {
29040
+ code: "flow_not_object",
29041
+ message: "flowDefinition must be an object with `nodes` and `edges` arrays."
29042
+ }
29043
+ ]
29044
+ };
29045
+ }
29046
+ const f = flow;
29047
+ if (!Array.isArray(f.nodes)) {
29048
+ errors.push({
29049
+ code: "nodes_not_array",
29050
+ message: "flowDefinition.nodes must be an array."
29051
+ });
29052
+ }
29053
+ if (!Array.isArray(f.edges)) {
29054
+ errors.push({
29055
+ code: "edges_not_array",
29056
+ message: "flowDefinition.edges must be an array."
29057
+ });
29058
+ }
29059
+ if (errors.length > 0) return { valid: false, errors };
29060
+ const nodes = f.nodes.map(
29061
+ (n) => isRecord(n) ? n : {}
29062
+ );
29063
+ const edges = f.edges.map(
29064
+ (e) => isRecord(e) ? e : {}
29065
+ );
29066
+ const nodeIds = /* @__PURE__ */ new Set();
29067
+ const seenIds = /* @__PURE__ */ new Set();
29068
+ nodes.forEach((node, idx) => {
29069
+ if (!nonEmptyString(node.id)) {
29070
+ errors.push({
29071
+ code: "node_missing_id",
29072
+ message: `Node at index ${idx} is missing a non-empty string \`id\`.`
29073
+ });
29074
+ return;
29075
+ }
29076
+ if (seenIds.has(node.id)) {
29077
+ errors.push({
29078
+ nodeId: node.id,
29079
+ code: "duplicate_node_id",
29080
+ message: `Duplicate node id "${node.id}" \u2014 every node id must be unique.`
29081
+ });
29082
+ return;
29083
+ }
29084
+ seenIds.add(node.id);
29085
+ nodeIds.add(node.id);
29086
+ });
29087
+ const nodesById = /* @__PURE__ */ new Map();
29088
+ for (const node of nodes) {
29089
+ if (nonEmptyString(node.id)) nodesById.set(node.id, node);
29090
+ const nodeId = nonEmptyString(node.id) ? node.id : void 0;
29091
+ if (!nonEmptyString(node.type) || !NODE_TYPE_SET.has(node.type)) {
29092
+ errors.push({
29093
+ nodeId,
29094
+ code: "unknown_node_type",
29095
+ message: `Node ${nodeId ? `"${nodeId}"` : `at index ?`} has ${nonEmptyString(node.type) ? `unknown type "${node.type}"` : "no type"}. Must be one of: ${FLOW_NODE_TYPES.join(", ")}.`
29096
+ });
29097
+ continue;
29098
+ }
29099
+ const type = node.type;
29100
+ const data = isRecord(node.data) ? node.data : {};
29101
+ switch (type) {
29102
+ case "capability": {
29103
+ if (!nonEmptyString(data.verbId)) {
29104
+ errors.push({
29105
+ nodeId,
29106
+ code: "capability_missing_verbId",
29107
+ message: `Capability node "${nodeId}" has no \`verbId\`. Pick a verb on the selected tool.`
29108
+ });
29109
+ } else if (resolvers?.verbExists && !resolvers.verbExists(data.verbId)) {
29110
+ errors.push({
29111
+ nodeId,
29112
+ code: "capability_unknown_verbId",
29113
+ message: `Capability node "${nodeId}" references verb "${data.verbId}", which is not in the capability catalog.`
29114
+ });
29115
+ }
29116
+ break;
29117
+ }
29118
+ case "skill": {
29119
+ const skillId = nonEmptyString(data.skillId) ? data.skillId : void 0;
29120
+ const skillName = nonEmptyString(data.skillName) ? data.skillName : void 0;
29121
+ if (!skillId && !skillName) {
29122
+ errors.push({
29123
+ nodeId,
29124
+ code: "skill_missing_ref",
29125
+ message: `Skill node "${nodeId}" has neither \`skillId\` nor \`skillName\`. One is required.`
29126
+ });
29127
+ } else if (resolvers?.skillExists && !resolvers.skillExists({ skillId, skillName })) {
29128
+ errors.push({
29129
+ nodeId,
29130
+ code: "skill_unknown_ref",
29131
+ message: `Skill node "${nodeId}" references a skill (${skillId ? `id "${skillId}"` : `name "${skillName}"`}) that does not exist.`
29132
+ });
29133
+ }
29134
+ break;
29135
+ }
29136
+ case "playbook_run": {
29137
+ const playbookId = nonEmptyString(data.playbookId) ? data.playbookId : void 0;
29138
+ const playbookName = nonEmptyString(data.playbookName) ? data.playbookName : void 0;
29139
+ if (!playbookId && !playbookName) {
29140
+ errors.push({
29141
+ nodeId,
29142
+ code: "playbook_run_missing_ref",
29143
+ message: `Playbook-run node "${nodeId}" has neither \`playbookId\` nor \`playbookName\`. One is required.`
29144
+ });
29145
+ } else if (resolvers?.playbookExists && !resolvers.playbookExists({ playbookId, playbookName })) {
29146
+ errors.push({
29147
+ nodeId,
29148
+ code: "playbook_run_unknown_ref",
29149
+ message: `Playbook-run node "${nodeId}" references a playbook (${playbookId ? `id "${playbookId}"` : `name "${playbookName}"`}) that does not exist.`
29150
+ });
29151
+ }
29152
+ break;
29153
+ }
29154
+ case "output": {
29155
+ const outputType = data.outputType;
29156
+ if (!nonEmptyString(outputType) || !OUTPUT_TYPE_SET.has(outputType)) {
29157
+ errors.push({
29158
+ nodeId,
29159
+ code: "output_bad_outputType",
29160
+ message: `Output node "${nodeId}" has ${nonEmptyString(outputType) ? `unknown outputType "${outputType}"` : "no outputType"}. Must be one of: ${FLOW_OUTPUT_TYPES.join(", ")}.`
29161
+ });
29162
+ break;
29163
+ }
29164
+ if (outputType === "channel_message") {
29165
+ const config = isRecord(data.config) ? data.config : {};
29166
+ const channelType = config.channelType;
29167
+ if (channelType !== void 0 && channelType !== null) {
29168
+ if (!nonEmptyString(channelType) || !CHANNEL_TYPE_SET.has(channelType)) {
29169
+ errors.push({
29170
+ nodeId,
29171
+ code: "channel_message_unknown_channelType",
29172
+ message: `Channel-message output "${nodeId}" has unknown channelType ${nonEmptyString(channelType) ? `"${channelType}"` : JSON.stringify(channelType)}. Use one of: ${CHANNEL_MESSAGE_CHANNEL_TYPES.join(
29173
+ ", "
29174
+ )}, or set \`config.channelEntityRef\`/\`config.channelId\`, or omit the target to post to the automation's own run channel.`
29175
+ });
29176
+ }
29177
+ }
29178
+ }
29179
+ break;
29180
+ }
29181
+ case "loop": {
29182
+ if (!nonEmptyString(data.iteratorExpression)) {
29183
+ errors.push({
29184
+ nodeId,
29185
+ code: "loop_missing_iterator",
29186
+ message: `Loop node "${nodeId}" has no \`iteratorExpression\` (the items to iterate, e.g. "steps.query.output.entities").`
29187
+ });
29188
+ }
29189
+ break;
29190
+ }
29191
+ case "condition": {
29192
+ if (!nonEmptyString(data.expression)) {
29193
+ errors.push({
29194
+ nodeId,
29195
+ code: "condition_missing_expression",
29196
+ message: `Condition node "${nodeId}" has no \`expression\` (the predicate to evaluate).`
29197
+ });
29198
+ }
29199
+ break;
29200
+ }
29201
+ case "switch": {
29202
+ if (!nonEmptyString(data.expression)) {
29203
+ errors.push({
29204
+ nodeId,
29205
+ code: "switch_missing_expression",
29206
+ message: `Switch node "${nodeId}" has no \`expression\` (the value to match against cases).`
29207
+ });
29208
+ }
29209
+ if (!Array.isArray(data.cases) || data.cases.length === 0) {
29210
+ errors.push({
29211
+ nodeId,
29212
+ code: "switch_missing_cases",
29213
+ message: `Switch node "${nodeId}" has no \`cases\`. Add at least one case with a \`value\`.`
29214
+ });
29215
+ }
29216
+ break;
29217
+ }
29218
+ }
29219
+ }
29220
+ edges.forEach((edge, idx) => {
29221
+ const edgeId = nonEmptyString(edge.id) ? edge.id : void 0;
29222
+ const label = edgeId ? `"${edgeId}"` : `at index ${idx}`;
29223
+ if (!nonEmptyString(edge.source) || !nodeIds.has(edge.source)) {
29224
+ errors.push({
29225
+ edgeId,
29226
+ code: "edge_bad_source",
29227
+ message: `Edge ${label} has ${nonEmptyString(edge.source) ? `source "${edge.source}" which is not a node in this flow` : "no source node"}.`
29228
+ });
29229
+ }
29230
+ if (!nonEmptyString(edge.target) || !nodeIds.has(edge.target)) {
29231
+ errors.push({
29232
+ edgeId,
29233
+ code: "edge_bad_target",
29234
+ message: `Edge ${label} has ${nonEmptyString(edge.target) ? `target "${edge.target}" which is not a node in this flow` : "no target node"}.`
29235
+ });
29236
+ }
29237
+ });
29238
+ for (const edge of edges) {
29239
+ if (!nonEmptyString(edge.source) || !nodesById.has(edge.source)) continue;
29240
+ if (edge.sourceHandle === void 0 || edge.sourceHandle === null) continue;
29241
+ if (!nonEmptyString(edge.sourceHandle)) continue;
29242
+ const src = nodesById.get(edge.source);
29243
+ const edgeId = nonEmptyString(edge.id) ? edge.id : void 0;
29244
+ if (src.type === "condition") {
29245
+ if (edge.sourceHandle !== "yes" && edge.sourceHandle !== "no") {
29246
+ errors.push({
29247
+ edgeId,
29248
+ nodeId: edge.source,
29249
+ code: "condition_bad_handle",
29250
+ message: `Edge from condition "${edge.source}" has sourceHandle "${edge.sourceHandle}" \u2014 must be "yes" or "no".`
29251
+ });
29252
+ }
29253
+ } else if (src.type === "switch") {
29254
+ const data = isRecord(src.data) ? src.data : {};
29255
+ const caseValues = Array.isArray(data.cases) ? data.cases.map((c) => isRecord(c) ? c.value : void 0).filter((v) => typeof v === "string") : [];
29256
+ if (!caseValues.includes(edge.sourceHandle)) {
29257
+ errors.push({
29258
+ edgeId,
29259
+ nodeId: edge.source,
29260
+ code: "switch_bad_handle",
29261
+ message: `Edge from switch "${edge.source}" has sourceHandle "${edge.sourceHandle}", which matches no declared case value (${caseValues.length ? caseValues.join(", ") : "none declared"}).`
29262
+ });
29263
+ }
29264
+ }
29265
+ }
29266
+ const cyclic = detectCycle(nodeIds, edges);
29267
+ if (cyclic.length > 0) {
29268
+ errors.push({
29269
+ code: "flow_has_cycle",
29270
+ message: `Flow has a cycle \u2014 these nodes cannot be ordered and would never run: ${cyclic.join(
29271
+ ", "
29272
+ )}. Remove the back-edge(s) so the graph is acyclic.`
29273
+ });
29274
+ }
29275
+ return { valid: errors.length === 0, errors };
29276
+ }
29277
+ function detectCycle(nodeIds, edges) {
29278
+ const inDegree = /* @__PURE__ */ new Map();
29279
+ const adjacency = /* @__PURE__ */ new Map();
29280
+ for (const id of nodeIds) {
29281
+ inDegree.set(id, 0);
29282
+ adjacency.set(id, []);
29283
+ }
29284
+ for (const edge of edges) {
29285
+ const source = nonEmptyString(edge.source) ? edge.source : void 0;
29286
+ const target = nonEmptyString(edge.target) ? edge.target : void 0;
29287
+ if (!source || !target) continue;
29288
+ if (!nodeIds.has(source) || !nodeIds.has(target)) continue;
29289
+ adjacency.get(source).push(target);
29290
+ inDegree.set(target, (inDegree.get(target) ?? 0) + 1);
29291
+ }
29292
+ const queue = [];
29293
+ for (const [id, degree] of inDegree) if (degree === 0) queue.push(id);
29294
+ let visited = 0;
29295
+ while (queue.length > 0) {
29296
+ const id = queue.shift();
29297
+ visited++;
29298
+ for (const target of adjacency.get(id) ?? []) {
29299
+ const next = (inDegree.get(target) ?? 1) - 1;
29300
+ inDegree.set(target, next);
29301
+ if (next === 0) queue.push(target);
29302
+ }
29303
+ }
29304
+ if (visited === nodeIds.size) return [];
29305
+ const cyclic = [];
29306
+ for (const [id, degree] of inDegree) if (degree > 0) cyclic.push(id);
29307
+ return cyclic;
29308
+ }
29309
+ function flowValidationErrorMessage(flow, resolvers) {
29310
+ const result = validateFlowDefinition(flow, resolvers);
29311
+ if (result.valid) return null;
29312
+ return "Invalid automation flow: " + result.errors.map((e) => e.nodeId ? `[${e.nodeId}] ${e.message}` : e.message).join(" ");
29313
+ }
29314
+
28864
29315
  // src/validate.ts
28865
29316
  var REQUIRED_META_FIELDS = [
28866
29317
  "slug",
@@ -28955,12 +29406,80 @@ function validateTemplate(tpl, opts = {}) {
28955
29406
  else {
28956
29407
  for (const f of REQUIRED_WORKSPACE_FIELDS) {
28957
29408
  if (!tpl.workspace[f])
28958
- add("workspace-field-required", `workspace.${f}`, `workspace.${f} is required`);
29409
+ add(
29410
+ "workspace-field-required",
29411
+ `workspace.${f}`,
29412
+ `workspace.${f} is required`
29413
+ );
28959
29414
  }
28960
29415
  }
28961
29416
  if (!tpl.profiles || tpl.profiles.length === 0) {
28962
29417
  add("profiles-required", "profiles", "At least one profile is required");
28963
29418
  }
29419
+ if (tpl.app && tpl.layout && Object.prototype.hasOwnProperty.call(tpl.layout, "primarySurface")) {
29420
+ add(
29421
+ "primary-surface-authoring-conflict",
29422
+ "layout.primarySurface",
29423
+ "Declare either top-level app or layout.primarySurface, not both"
29424
+ );
29425
+ }
29426
+ if (tpl.app?.kind === "native" && !tpl.app.appId) {
29427
+ add(
29428
+ "app-native-id-required",
29429
+ "app.appId",
29430
+ "A native workspace app requires appId"
29431
+ );
29432
+ }
29433
+ if (tpl.app?.kind === "url" && (!tpl.app.appId || !tpl.app.url)) {
29434
+ add(
29435
+ "app-url-identity-required",
29436
+ "app",
29437
+ "A hosted workspace app requires both appId and url"
29438
+ );
29439
+ }
29440
+ if (tpl.app?.kind === "stored" && !tpl.app.cellKey) {
29441
+ add(
29442
+ "app-cell-key-required",
29443
+ "app.cellKey",
29444
+ "A stored workspace app requires cellKey"
29445
+ );
29446
+ }
29447
+ const authoredPrimary = tpl.layout?.primarySurface;
29448
+ if (authoredPrimary?.kind === "view" && !("viewId" in authoredPrimary)) {
29449
+ const viewName = authoredPrimary.viewName;
29450
+ const viewSlug = authoredPrimary.viewSlug;
29451
+ if (!viewName && !viewSlug) {
29452
+ add(
29453
+ "primary-view-reference-required",
29454
+ "layout.primarySurface",
29455
+ "A view primary surface requires viewName or viewSlug"
29456
+ );
29457
+ } else {
29458
+ const declaredViews = [
29459
+ ...(tpl.views ?? []).map((view) => ({
29460
+ name: view.name,
29461
+ slug: view.slug
29462
+ })),
29463
+ ...tpl.bento?.viewName ? [{ name: tpl.bento.viewName, slug: void 0 }] : []
29464
+ ];
29465
+ const matches = declaredViews.filter(
29466
+ (view) => (!viewName || view.name === viewName) && (!viewSlug || view.slug === viewSlug)
29467
+ );
29468
+ if (matches.length === 0) {
29469
+ add(
29470
+ "primary-view-reference-unknown",
29471
+ "layout.primarySurface",
29472
+ `Primary surface references an unknown view (${viewName ?? viewSlug})`
29473
+ );
29474
+ } else if (matches.length > 1) {
29475
+ add(
29476
+ "primary-view-reference-ambiguous",
29477
+ "layout.primarySurface",
29478
+ `Primary surface view reference matches ${matches.length} authored views; add a unique viewSlug`
29479
+ );
29480
+ }
29481
+ }
29482
+ }
28964
29483
  const baseProfileSlugs = collectBaseProfileSlugs(tpl, resolveDepFrom(opts));
28965
29484
  const slugs = new Set((tpl.profiles || []).map((p) => p.slug));
28966
29485
  const known = (s) => slugs.has(s) || SYSTEM_PROFILES.has(s) || baseProfileSlugs.has(s);
@@ -29038,11 +29557,29 @@ function validateTemplate(tpl, opts = {}) {
29038
29557
  }
29039
29558
  for (const a of tpl.automations || []) {
29040
29559
  const refs = [];
29041
- collectProfileSlugRefs(a.triggerConfig, `automation "${a.name}".triggerConfig`, refs);
29560
+ collectProfileSlugRefs(
29561
+ a.triggerConfig,
29562
+ `automation "${a.name}".triggerConfig`,
29563
+ refs
29564
+ );
29042
29565
  collectProfileSlugRefs(a.flow, `automation "${a.name}".flow`, refs);
29043
29566
  for (const r of refs)
29044
29567
  if (!known(r.slug))
29045
- add("automation-ref-unknown", r.where, `${r.where} references unknown profile "${r.slug}"`);
29568
+ add(
29569
+ "automation-ref-unknown",
29570
+ r.where,
29571
+ `${r.where} references unknown profile "${r.slug}"`
29572
+ );
29573
+ const flow = a.flow ?? a.flowDefinition;
29574
+ if (flow != null) {
29575
+ const {
29576
+ errors: flowErrors
29577
+ } = validateFlowDefinition(flow);
29578
+ for (const fe of flowErrors) {
29579
+ const loc = fe.nodeId ? `automation "${a.name}".flow.node "${fe.nodeId}"` : fe.edgeId ? `automation "${a.name}".flow.edge "${fe.edgeId}"` : `automation "${a.name}".flow`;
29580
+ add(`automation-flow-${fe.code}`, loc, `${loc}: ${fe.message}`);
29581
+ }
29582
+ }
29046
29583
  }
29047
29584
  for (const pb of tpl.playbooks || []) {
29048
29585
  const ps = pb.subjectProfile?.profileSlug;
@@ -29088,7 +29625,9 @@ function validateTemplate(tpl, opts = {}) {
29088
29625
  `profileEntityBentoTemplates has an entry for unknown profile "${key}"`
29089
29626
  );
29090
29627
  }
29091
- const composeDeps = (tpl.dependencies || []).filter((d) => d.relation === "compose");
29628
+ const composeDeps = (tpl.dependencies || []).filter(
29629
+ (d) => d.relation === "compose"
29630
+ );
29092
29631
  for (const dep of composeDeps) {
29093
29632
  if (tpl.workspace?.visibility !== void 0) {
29094
29633
  add(
@@ -29108,7 +29647,9 @@ function validateTemplate(tpl, opts = {}) {
29108
29647
  const blocks = tpl.bento?.blocks ?? [];
29109
29648
  const authoredWidgets = blocks.filter((b) => b.kind === "widget");
29110
29649
  const authoredViews = blocks.filter((b) => b.kind === "view");
29111
- const unknownKind = blocks.filter((b) => b.kind !== "widget" && b.kind !== "view");
29650
+ const unknownKind = blocks.filter(
29651
+ (b) => b.kind !== "widget" && b.kind !== "view"
29652
+ );
29112
29653
  if (unknownKind.length) {
29113
29654
  add(
29114
29655
  "bento-unknown-kind",
@@ -29140,7 +29681,11 @@ function validateTemplate(tpl, opts = {}) {
29140
29681
  const expectW = authoredWidgets.length - badWidgets.length;
29141
29682
  const expectV = authoredViews.length - badViews.length;
29142
29683
  const doors = [
29143
- ["tRPC createFromDefinition", definition.bentoLayout ?? [], definition.bentoViewBlocks ?? []],
29684
+ [
29685
+ "tRPC createFromDefinition",
29686
+ definition.bentoLayout ?? [],
29687
+ definition.bentoViewBlocks ?? []
29688
+ ],
29144
29689
  ["CP packages/apply", pkg.bentoLayout ?? [], pkg.bentoViewBlocks ?? []]
29145
29690
  ];
29146
29691
  for (const [door, layout, viewBlocks] of doors) {
@@ -29151,7 +29696,9 @@ function validateTemplate(tpl, opts = {}) {
29151
29696
  `[${door}]: authored ${expectW} widget/${expectV} view \u2192 got ${layout.length} bentoLayout/${viewBlocks.length} bentoViewBlocks`
29152
29697
  );
29153
29698
  }
29154
- const layoutBad = layout.filter((b) => typeof b.widgetType !== "string");
29699
+ const layoutBad = layout.filter(
29700
+ (b) => typeof b.widgetType !== "string"
29701
+ );
29155
29702
  const viewsBad = viewBlocks.filter(
29156
29703
  (b) => typeof b.viewName !== "string" || b.kind !== "view"
29157
29704
  );
@@ -29221,6 +29768,79 @@ function toAutomationWire(automations) {
29221
29768
  status: a.status
29222
29769
  }));
29223
29770
  }
29771
+ var hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
29772
+ function appRefToPrimarySurface(app) {
29773
+ if (!app) return void 0;
29774
+ if (app.kind === "native") {
29775
+ if (!app.appId) {
29776
+ throw new Error("Workspace template native app is missing appId");
29777
+ }
29778
+ return {
29779
+ kind: "app",
29780
+ appId: app.appId,
29781
+ rendererType: "native",
29782
+ ...app.title ? { title: app.title } : {},
29783
+ ...app.props ? { props: app.props } : {}
29784
+ };
29785
+ }
29786
+ if (app.kind === "url") {
29787
+ if (!app.appId || !app.url) {
29788
+ throw new Error(
29789
+ "Workspace template URL app requires both appId and url"
29790
+ );
29791
+ }
29792
+ return {
29793
+ kind: "app",
29794
+ appId: app.appId,
29795
+ rendererType: "external",
29796
+ url: app.url,
29797
+ ...app.title ? { title: app.title } : {},
29798
+ ...app.props ? { props: app.props } : {}
29799
+ };
29800
+ }
29801
+ if (app.kind === "stored") {
29802
+ if (!app.cellKey) {
29803
+ throw new Error("Workspace template stored app is missing cellKey");
29804
+ }
29805
+ if (app.cellKey === "entity-detail" && !app.props?.entityId) {
29806
+ return void 0;
29807
+ }
29808
+ return {
29809
+ kind: "cell",
29810
+ cellKey: app.cellKey,
29811
+ ...app.title ? { title: app.title } : {},
29812
+ ...app.props ? { props: app.props } : {}
29813
+ };
29814
+ }
29815
+ return void 0;
29816
+ }
29817
+ function normalizeTemplateLayout(layout, app) {
29818
+ if (app && layout && hasOwn(layout, "primarySurface")) {
29819
+ throw new Error(
29820
+ "Workspace template declares both top-level app and layout.primarySurface; choose one authoring source"
29821
+ );
29822
+ }
29823
+ const primarySurface = (() => {
29824
+ if (layout && hasOwn(layout, "primarySurface")) {
29825
+ return layout.primarySurface;
29826
+ }
29827
+ const fromApp = appRefToPrimarySurface(app);
29828
+ if (fromApp) return fromApp;
29829
+ if (layout && hasOwn(layout, "defaultApp")) {
29830
+ return layout.defaultApp ? {
29831
+ kind: "app",
29832
+ appId: layout.defaultApp,
29833
+ rendererType: "native"
29834
+ } : null;
29835
+ }
29836
+ return void 0;
29837
+ })();
29838
+ if (!layout && primarySurface === void 0) return void 0;
29839
+ return {
29840
+ ...layout,
29841
+ ...primarySurface !== void 0 || layout && hasOwn(layout, "primarySurface") ? { primarySurface } : {}
29842
+ };
29843
+ }
29224
29844
  function resolveBlockKind(block) {
29225
29845
  if (block.kind === "widget" || block.kind === "view") return block.kind;
29226
29846
  if (block.widgetType) return "widget";
@@ -29281,7 +29901,7 @@ function toWorkspaceDefinition(slugOrYaml, nameOverride) {
29281
29901
  seedEntities: tpl.seedEntities ?? [],
29282
29902
  suggestedRelations: tpl.suggestedRelations,
29283
29903
  displayTemplates: tpl.displayTemplates,
29284
- layoutConfig: tpl.layout,
29904
+ layoutConfig: normalizeTemplateLayout(tpl.layout, tpl.app),
29285
29905
  bentoViewName: tpl.bento?.viewName,
29286
29906
  bentoLayout: bento.bentoLayout,
29287
29907
  bentoViewBlocks: bento.bentoViewBlocks,
@@ -29355,7 +29975,7 @@ function toPackageDefinition(slugOrYaml) {
29355
29975
  bentoViewBlocks: bento.bentoViewBlocks,
29356
29976
  bentoViewName: tpl.bento?.viewName,
29357
29977
  profileEntityBentoTemplates: tpl.profileEntityBentoTemplates,
29358
- layoutConfig: tpl.layout,
29978
+ layoutConfig: normalizeTemplateLayout(tpl.layout, tpl.app),
29359
29979
  capabilities: tpl.integrations,
29360
29980
  playbooks: tpl.playbooks,
29361
29981
  automations: toAutomationWire(tpl.automations),
@@ -29365,7 +29985,10 @@ function toPackageDefinition(slugOrYaml) {
29365
29985
  };
29366
29986
  }
29367
29987
  export {
29988
+ CHANNEL_MESSAGE_CHANNEL_TYPES,
29368
29989
  DECLARED_DIVERGENCES,
29990
+ FLOW_NODE_TYPES,
29991
+ FLOW_OUTPUT_TYPES,
29369
29992
  OTHER_DOMAIN_ID,
29370
29993
  OTHER_DOMAIN_LABEL,
29371
29994
  PACKAGE_DEPENDENCY_KINDS,
@@ -29381,6 +30004,7 @@ export {
29381
30004
  conceptTerm,
29382
30005
  domainLabel,
29383
30006
  findConceptViolations,
30007
+ flowValidationErrorMessage,
29384
30008
  formatConceptViolations,
29385
30009
  getWorkspaceTemplate,
29386
30010
  groupByDomain,
@@ -29390,10 +30014,12 @@ export {
29390
30014
  listPublicTemplates,
29391
30015
  listWorkspaceTemplates,
29392
30016
  mergeCatalog,
30017
+ normalizeTemplateLayout,
29393
30018
  rowToRemoteEntry,
29394
30019
  splitBentoBlocks,
29395
30020
  toPackageDefinition,
29396
30021
  toWorkspaceDefinition,
30022
+ validateFlowDefinition,
29397
30023
  validateTemplate
29398
30024
  };
29399
30025
  //# sourceMappingURL=index.js.map