@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.d.ts +170 -8
- package/dist/index.js +643 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/business-developer.yaml +24 -6
- package/src/content-os.yaml +19 -0
- package/src/crm.yaml +28 -2
- package/src/define.primary-surface.test.ts +183 -0
- package/src/define.ts +142 -19
- package/src/index.ts +26 -3
- package/src/marketing.yaml +19 -0
- package/src/templates.ts +126 -8
- package/src/types.ts +74 -5
- package/src/validate-flow.drift.test.ts +83 -0
- package/src/validate-flow.ts +555 -0
- package/src/validate.test.ts +121 -0
- package/src/validate.ts +133 -10
package/src/validate.ts
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* • `scripts/check-converters.mjs` — injects the real converters, exits non-zero.
|
|
15
15
|
* • CP / pod / CLI (from dist) — import `validateTemplate` + `SYSTEM_PROFILES`.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
* Node's type-stripping BEFORE `tsup` builds `dist/`. A static
|
|
17
|
+
* NO RUNTIME IMPORT THAT FAILS AT STRIP-TIME. `build.mjs` loads this file
|
|
18
|
+
* directly under Node's type-stripping BEFORE `tsup` builds `dist/`. A static
|
|
19
19
|
* `import { toWorkspaceDefinition } from "./define.js"` would fail to resolve at
|
|
20
20
|
* that moment (define → templates.js does not exist pre-build, and Node's
|
|
21
21
|
* type-stripper does not remap a `.js` specifier to its `.ts` source). So the
|
|
@@ -23,6 +23,14 @@
|
|
|
23
23
|
* YAML/bento rules run everywhere; the converter-parity rules run wherever the
|
|
24
24
|
* caller can supply the compiled converters (check-converters.mjs, from dist).
|
|
25
25
|
* `import type` lines are fully erased by the type-stripper, so they are safe.
|
|
26
|
+
*
|
|
27
|
+
* THE ONE RUNTIME IMPORT: `./validate-flow.ts` (the pure automation-flow
|
|
28
|
+
* validator). It is imported with the `.ts` extension SPECIFICALLY so it resolves
|
|
29
|
+
* under Node's type-stripper at build.mjs time (a `.js` specifier would NOT — see
|
|
30
|
+
* above), and it has NO generated dependency (unlike `./define.js` → templates)
|
|
31
|
+
* so it is always present. It is DB-free (only `import type` internally), so it
|
|
32
|
+
* needs no injection: the flow-node contract check runs for every caller
|
|
33
|
+
* (build / check-converters / CLI / CP) automatically.
|
|
26
34
|
*/
|
|
27
35
|
|
|
28
36
|
import type {
|
|
@@ -31,6 +39,9 @@ import type {
|
|
|
31
39
|
TemplateProfile,
|
|
32
40
|
} from "./types.js";
|
|
33
41
|
|
|
42
|
+
// eslint-disable-next-line import/extensions -- .ts extension is intentional; see header.
|
|
43
|
+
import { validateFlowDefinition } from "./validate-flow.ts";
|
|
44
|
+
|
|
34
45
|
// ── Result shape ─────────────────────────────────────────────────────────────
|
|
35
46
|
|
|
36
47
|
export interface ValidationError {
|
|
@@ -271,12 +282,86 @@ export function validateTemplate(
|
|
|
271
282
|
else {
|
|
272
283
|
for (const f of REQUIRED_WORKSPACE_FIELDS) {
|
|
273
284
|
if (!tpl.workspace[f])
|
|
274
|
-
add(
|
|
285
|
+
add(
|
|
286
|
+
"workspace-field-required", `workspace.${f}`, `workspace.${f} is required`
|
|
287
|
+
);
|
|
275
288
|
}
|
|
276
289
|
}
|
|
277
290
|
if (!tpl.profiles || tpl.profiles.length === 0) {
|
|
278
291
|
add("profiles-required", "profiles", "At least one profile is required");
|
|
279
292
|
}
|
|
293
|
+
if (
|
|
294
|
+
tpl.app &&
|
|
295
|
+
tpl.layout &&
|
|
296
|
+
Object.prototype.hasOwnProperty.call(tpl.layout, "primarySurface")
|
|
297
|
+
) {
|
|
298
|
+
add(
|
|
299
|
+
"primary-surface-authoring-conflict",
|
|
300
|
+
"layout.primarySurface",
|
|
301
|
+
"Declare either top-level app or layout.primarySurface, not both"
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
if (tpl.app?.kind === "native" && !tpl.app.appId) {
|
|
305
|
+
add(
|
|
306
|
+
"app-native-id-required",
|
|
307
|
+
"app.appId",
|
|
308
|
+
"A native workspace app requires appId"
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
if (tpl.app?.kind === "url" && (!tpl.app.appId || !tpl.app.url)) {
|
|
312
|
+
add(
|
|
313
|
+
"app-url-identity-required",
|
|
314
|
+
"app",
|
|
315
|
+
"A hosted workspace app requires both appId and url"
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
if (tpl.app?.kind === "stored" && !tpl.app.cellKey) {
|
|
319
|
+
add(
|
|
320
|
+
"app-cell-key-required",
|
|
321
|
+
"app.cellKey",
|
|
322
|
+
"A stored workspace app requires cellKey"
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
const authoredPrimary = tpl.layout?.primarySurface;
|
|
326
|
+
if (authoredPrimary?.kind === "view" && !("viewId" in authoredPrimary)) {
|
|
327
|
+
const viewName = authoredPrimary.viewName;
|
|
328
|
+
const viewSlug = authoredPrimary.viewSlug;
|
|
329
|
+
if (!viewName && !viewSlug) {
|
|
330
|
+
add(
|
|
331
|
+
"primary-view-reference-required",
|
|
332
|
+
"layout.primarySurface",
|
|
333
|
+
"A view primary surface requires viewName or viewSlug"
|
|
334
|
+
);
|
|
335
|
+
} else {
|
|
336
|
+
const declaredViews = [
|
|
337
|
+
...(tpl.views ?? []).map((view) => ({
|
|
338
|
+
name: view.name,
|
|
339
|
+
slug: view.slug,
|
|
340
|
+
})),
|
|
341
|
+
...(tpl.bento?.viewName
|
|
342
|
+
? [{ name: tpl.bento.viewName, slug: undefined }]
|
|
343
|
+
: []),
|
|
344
|
+
];
|
|
345
|
+
const matches = declaredViews.filter(
|
|
346
|
+
(view) =>
|
|
347
|
+
(!viewName || view.name === viewName) &&
|
|
348
|
+
(!viewSlug || view.slug === viewSlug)
|
|
349
|
+
);
|
|
350
|
+
if (matches.length === 0) {
|
|
351
|
+
add(
|
|
352
|
+
"primary-view-reference-unknown",
|
|
353
|
+
"layout.primarySurface",
|
|
354
|
+
`Primary surface references an unknown view (${viewName ?? viewSlug})`
|
|
355
|
+
);
|
|
356
|
+
} else if (matches.length > 1) {
|
|
357
|
+
add(
|
|
358
|
+
"primary-view-reference-ambiguous",
|
|
359
|
+
"layout.primarySurface",
|
|
360
|
+
`Primary surface view reference matches ${matches.length} authored views; add a unique viewSlug`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
280
365
|
|
|
281
366
|
// ── Referential integrity: build the known-profile predicate ──────────────
|
|
282
367
|
const baseProfileSlugs = collectBaseProfileSlugs(tpl, resolveDepFrom(opts));
|
|
@@ -371,11 +456,37 @@ export function validateTemplate(
|
|
|
371
456
|
// SILENTLY at runtime.
|
|
372
457
|
for (const a of tpl.automations || []) {
|
|
373
458
|
const refs: Array<{ where: string; slug: string }> = [];
|
|
374
|
-
collectProfileSlugRefs(
|
|
459
|
+
collectProfileSlugRefs(
|
|
460
|
+
a.triggerConfig, `automation "${a.name}".triggerConfig`,
|
|
461
|
+
refs
|
|
462
|
+
);
|
|
375
463
|
collectProfileSlugRefs(a.flow, `automation "${a.name}".flow`, refs);
|
|
376
464
|
for (const r of refs)
|
|
377
465
|
if (!known(r.slug))
|
|
378
|
-
add(
|
|
466
|
+
add(
|
|
467
|
+
"automation-ref-unknown", r.where, `${r.where} references unknown profile "${r.slug}"`
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
// Node-contract validation. Previously validate.ts checked automations for
|
|
471
|
+
// profileSlug refs ONLY, so a bogus node type / dangling edge / cycle / typo'd
|
|
472
|
+
// channelType / verb-less capability node passed the template gate green and
|
|
473
|
+
// then SILENTLY never fired at runtime (validateFlowDefinition only ran
|
|
474
|
+
// server-side at install). Run the SAME contract validator here, at author
|
|
475
|
+
// time. Catalog-existence checks are resolver-gated and skipped (no catalog at
|
|
476
|
+
// author time); the structural + per-node contract checks always run.
|
|
477
|
+
const flow = a.flow ?? (a as { flowDefinition?: unknown }).flowDefinition;
|
|
478
|
+
if (flow != null) {
|
|
479
|
+
const { errors: flowErrors
|
|
480
|
+
} = validateFlowDefinition(flow);
|
|
481
|
+
for (const fe of flowErrors) {
|
|
482
|
+
const loc = fe.nodeId
|
|
483
|
+
? `automation "${a.name}".flow.node "${fe.nodeId}"`
|
|
484
|
+
: fe.edgeId
|
|
485
|
+
? `automation "${a.name}".flow.edge "${fe.edgeId}"`
|
|
486
|
+
: `automation "${a.name}".flow`;
|
|
487
|
+
add(`automation-flow-${fe.code}`, loc, `${loc}: ${fe.message}`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
379
490
|
}
|
|
380
491
|
for (const pb of tpl.playbooks || []) {
|
|
381
492
|
const ps = pb.subjectProfile?.profileSlug;
|
|
@@ -426,7 +537,9 @@ export function validateTemplate(
|
|
|
426
537
|
// its base's workspace. The additive reconcile OVERWRITES workspace-level
|
|
427
538
|
// settings, so an overlay MUST set workspace.subtype to its compose-base's slug
|
|
428
539
|
// and MUST OMIT workspace.visibility — else it clobbers the base's identity.
|
|
429
|
-
const composeDeps = (tpl.dependencies || []).filter(
|
|
540
|
+
const composeDeps = (tpl.dependencies || []).filter(
|
|
541
|
+
(d) => d.relation === "compose"
|
|
542
|
+
);
|
|
430
543
|
for (const dep of composeDeps) {
|
|
431
544
|
if (tpl.workspace?.visibility !== undefined) {
|
|
432
545
|
add(
|
|
@@ -448,7 +561,9 @@ export function validateTemplate(
|
|
|
448
561
|
const blocks: TemplateBentoBlock[] = tpl.bento?.blocks ?? [];
|
|
449
562
|
const authoredWidgets = blocks.filter((b) => b.kind === "widget");
|
|
450
563
|
const authoredViews = blocks.filter((b) => b.kind === "view");
|
|
451
|
-
const unknownKind = blocks.filter(
|
|
564
|
+
const unknownKind = blocks.filter(
|
|
565
|
+
(b) => b.kind !== "widget" && b.kind !== "view"
|
|
566
|
+
);
|
|
452
567
|
if (unknownKind.length) {
|
|
453
568
|
add(
|
|
454
569
|
"bento-unknown-kind",
|
|
@@ -485,8 +600,14 @@ export function validateTemplate(
|
|
|
485
600
|
const expectW = authoredWidgets.length - badWidgets.length;
|
|
486
601
|
const expectV = authoredViews.length - badViews.length;
|
|
487
602
|
|
|
488
|
-
const doors: Array<
|
|
489
|
-
[
|
|
603
|
+
const doors: Array<
|
|
604
|
+
[
|
|
605
|
+
string, Array<{ widgetType?: unknown }>, Array<{ viewName?: unknown; kind?: unknown }>,
|
|
606
|
+
]
|
|
607
|
+
> = [
|
|
608
|
+
[
|
|
609
|
+
"tRPC createFromDefinition", definition.bentoLayout ?? [], definition.bentoViewBlocks ?? [],
|
|
610
|
+
],
|
|
490
611
|
["CP packages/apply", pkg.bentoLayout ?? [], pkg.bentoViewBlocks ?? []],
|
|
491
612
|
];
|
|
492
613
|
for (const [door, layout, viewBlocks] of doors) {
|
|
@@ -497,7 +618,9 @@ export function validateTemplate(
|
|
|
497
618
|
`[${door}]: authored ${expectW} widget/${expectV} view → got ${layout.length} bentoLayout/${viewBlocks.length} bentoViewBlocks`
|
|
498
619
|
);
|
|
499
620
|
}
|
|
500
|
-
const layoutBad = layout.filter(
|
|
621
|
+
const layoutBad = layout.filter(
|
|
622
|
+
(b) => typeof b.widgetType !== "string"
|
|
623
|
+
);
|
|
501
624
|
const viewsBad = viewBlocks.filter(
|
|
502
625
|
(b) => typeof b.viewName !== "string" || b.kind !== "view"
|
|
503
626
|
);
|