@synap-core/workspace-templates 0.9.3 → 0.10.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/src/validate.ts CHANGED
@@ -152,6 +152,11 @@ export const SYSTEM_PROFILES = new Set([
152
152
  "research",
153
153
  "knowledge",
154
154
  "user_observation",
155
+ // Generated artifact — a workspace-scoped pod system profile
156
+ // (`ensure-system-profiles.ts`) written by the `base` report automation. It
157
+ // was simply missing from this hand-maintained list; the flow's
158
+ // `create-report` node references it, so referential integrity needs it here.
159
+ "report",
155
160
  ]);
156
161
 
157
162
  // ── Dependency-vocabulary closure (compose vs require semantics) ─────────────
@@ -274,7 +279,8 @@ export function validateTemplate(
274
279
  if (!tpl.meta) add("meta-required", "meta", "Missing required field: meta");
275
280
  else {
276
281
  for (const f of REQUIRED_META_FIELDS) {
277
- if (!tpl.meta[f]) add("meta-field-required", `meta.${f}`, `meta.${f} is required`);
282
+ if (!tpl.meta[f])
283
+ add("meta-field-required", `meta.${f}`, `meta.${f} is required`);
278
284
  }
279
285
  }
280
286
  if (!tpl.workspace)
@@ -283,12 +289,33 @@ export function validateTemplate(
283
289
  for (const f of REQUIRED_WORKSPACE_FIELDS) {
284
290
  if (!tpl.workspace[f])
285
291
  add(
286
- "workspace-field-required", `workspace.${f}`, `workspace.${f} is required`
292
+ "workspace-field-required",
293
+ `workspace.${f}`,
294
+ `workspace.${f} is required`
287
295
  );
288
296
  }
289
297
  }
290
- if (!tpl.profiles || tpl.profiles.length === 0) {
291
- add("profiles-required", "profiles", "At least one profile is required");
298
+ // A template earns validity by declaring a data-schema profile OR by carrying
299
+ // operational defaults (relation vocabulary / commands / automations). The
300
+ // `base` template is the latter: pod-wide operational config with no
301
+ // data-schema profile of its own (its report automation writes the `report`
302
+ // system profile and reads other system profiles, none declared here). Without
303
+ // this carve-out a legitimate operational-defaults template fails the gate,
304
+ // while a data template that forgot its profiles still does (it carries no
305
+ // operational config either).
306
+ const carriesOperationalDefaults =
307
+ (tpl.relationDefs && tpl.relationDefs.length > 0) ||
308
+ (tpl.commands && tpl.commands.length > 0) ||
309
+ (tpl.automations && tpl.automations.length > 0);
310
+ if (
311
+ (!tpl.profiles || tpl.profiles.length === 0) &&
312
+ !carriesOperationalDefaults
313
+ ) {
314
+ add(
315
+ "profiles-required",
316
+ "profiles",
317
+ "At least one profile is required (or operational defaults: relationDefs / commands / automations)"
318
+ );
292
319
  }
293
320
  if (
294
321
  tpl.app &&
@@ -434,7 +461,110 @@ export function validateTemplate(
434
461
  }
435
462
  }
436
463
 
437
- for (const c of tpl.onboarding?.collect || []) {
464
+ const onboarding = tpl.onboarding;
465
+ if (onboarding) {
466
+ if (typeof onboarding.goal !== "string" || onboarding.goal.trim() === "") {
467
+ add(
468
+ "onboarding-goal-empty",
469
+ "onboarding.goal",
470
+ "onboarding.goal must be a non-empty string"
471
+ );
472
+ }
473
+ if (
474
+ typeof onboarding.framing !== "string" ||
475
+ onboarding.framing.trim() === ""
476
+ ) {
477
+ add(
478
+ "onboarding-framing-empty",
479
+ "onboarding.framing",
480
+ "onboarding.framing must be a non-empty string"
481
+ );
482
+ }
483
+ if (!Array.isArray(onboarding.collect) || onboarding.collect.length === 0) {
484
+ add(
485
+ "onboarding-collect-empty",
486
+ "onboarding.collect",
487
+ "onboarding.collect must contain at least one structured target"
488
+ );
489
+ }
490
+
491
+ const expertise = onboarding.expertise;
492
+ for (const [field, values] of [
493
+ ["starters", expertise?.starters],
494
+ ["blindSpots", expertise?.blindSpots],
495
+ ] as const) {
496
+ if (
497
+ values !== undefined &&
498
+ (!Array.isArray(values) ||
499
+ values.some(
500
+ (value) => typeof value !== "string" || value.trim() === ""
501
+ ))
502
+ ) {
503
+ add(
504
+ "onboarding-expertise-invalid",
505
+ `onboarding.expertise.${field}`,
506
+ `onboarding.expertise.${field} must contain only non-empty strings`
507
+ );
508
+ }
509
+ }
510
+ if (
511
+ expertise?.bar !== undefined &&
512
+ (typeof expertise.bar !== "string" || expertise.bar.trim() === "")
513
+ ) {
514
+ add(
515
+ "onboarding-expertise-invalid",
516
+ "onboarding.expertise.bar",
517
+ "onboarding.expertise.bar must be a non-empty string"
518
+ );
519
+ }
520
+
521
+ if (
522
+ onboarding.openingQuestions !== undefined &&
523
+ (!Array.isArray(onboarding.openingQuestions) ||
524
+ onboarding.openingQuestions.some(
525
+ (question) => typeof question !== "string" || question.trim() === ""
526
+ ))
527
+ ) {
528
+ add(
529
+ "onboarding-opening-questions-invalid",
530
+ "onboarding.openingQuestions",
531
+ "onboarding.openingQuestions must contain only non-empty strings"
532
+ );
533
+ }
534
+ if (
535
+ onboarding.doneWhen !== undefined &&
536
+ (typeof onboarding.doneWhen !== "string" ||
537
+ onboarding.doneWhen.trim() === "")
538
+ ) {
539
+ add(
540
+ "onboarding-done-when-invalid",
541
+ "onboarding.doneWhen",
542
+ "onboarding.doneWhen must be a non-empty string"
543
+ );
544
+ }
545
+ }
546
+
547
+ for (const c of onboarding?.collect || []) {
548
+ if (typeof c.what !== "string" || c.what.trim() === "") {
549
+ add(
550
+ "onboarding-collect-what-empty",
551
+ `onboarding.collect.${c.profileSlug}.what`,
552
+ `onboarding.collect target "${c.profileSlug}" must describe what to capture`
553
+ );
554
+ }
555
+ if (
556
+ c.keyFields !== undefined &&
557
+ (!Array.isArray(c.keyFields) ||
558
+ c.keyFields.some(
559
+ (field) => typeof field !== "string" || field.trim() === ""
560
+ ))
561
+ ) {
562
+ add(
563
+ "onboarding-collect-key-fields-invalid",
564
+ `onboarding.collect.${c.profileSlug}.keyFields`,
565
+ `onboarding.collect target "${c.profileSlug}" keyFields must contain only non-empty strings`
566
+ );
567
+ }
438
568
  if (!known(c.profileSlug))
439
569
  add(
440
570
  "onboarding-ref-unknown",
@@ -457,14 +587,17 @@ export function validateTemplate(
457
587
  for (const a of tpl.automations || []) {
458
588
  const refs: Array<{ where: string; slug: string }> = [];
459
589
  collectProfileSlugRefs(
460
- a.triggerConfig, `automation "${a.name}".triggerConfig`,
590
+ a.triggerConfig,
591
+ `automation "${a.name}".triggerConfig`,
461
592
  refs
462
593
  );
463
594
  collectProfileSlugRefs(a.flow, `automation "${a.name}".flow`, refs);
464
595
  for (const r of refs)
465
596
  if (!known(r.slug))
466
597
  add(
467
- "automation-ref-unknown", r.where, `${r.where} references unknown profile "${r.slug}"`
598
+ "automation-ref-unknown",
599
+ r.where,
600
+ `${r.where} references unknown profile "${r.slug}"`
468
601
  );
469
602
 
470
603
  // Node-contract validation. Previously validate.ts checked automations for
@@ -476,9 +609,8 @@ export function validateTemplate(
476
609
  // author time); the structural + per-node contract checks always run.
477
610
  const flow = a.flow ?? (a as { flowDefinition?: unknown }).flowDefinition;
478
611
  if (flow != null) {
479
- const { errors: flowErrors
480
- } = validateFlowDefinition(flow);
481
- for (const fe of flowErrors) {
612
+ const { errors: flowErrors } = validateFlowDefinition(flow);
613
+ for (const fe of flowErrors) {
482
614
  const loc = fe.nodeId
483
615
  ? `automation "${a.name}".flow.node "${fe.nodeId}"`
484
616
  : fe.edgeId
@@ -602,11 +734,15 @@ export function validateTemplate(
602
734
 
603
735
  const doors: Array<
604
736
  [
605
- string, Array<{ widgetType?: unknown }>, Array<{ viewName?: unknown; kind?: unknown }>,
737
+ string,
738
+ Array<{ widgetType?: unknown }>,
739
+ Array<{ viewName?: unknown; kind?: unknown }>,
606
740
  ]
607
741
  > = [
608
742
  [
609
- "tRPC createFromDefinition", definition.bentoLayout ?? [], definition.bentoViewBlocks ?? [],
743
+ "tRPC createFromDefinition",
744
+ definition.bentoLayout ?? [],
745
+ definition.bentoViewBlocks ?? [],
610
746
  ],
611
747
  ["CP packages/apply", pkg.bentoLayout ?? [], pkg.bentoViewBlocks ?? []],
612
748
  ];