@synap-core/workspace-templates 0.9.3 → 0.9.4
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 +42 -33
- package/dist/index.js +142 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/crm.yaml +66 -0
- package/src/templates.ts +60 -0
- package/src/types.ts +47 -37
- package/src/validate-onboarding.test.ts +88 -0
- package/src/validate.ts +122 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synap-core/workspace-templates",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "pnpm@9.15.4",
|
|
6
6
|
"description": "Canonical workspace definitions — YAML source + typed exports for browser, backend, CLI, and marketplace",
|
package/src/crm.yaml
CHANGED
|
@@ -464,6 +464,72 @@ entityLinks:
|
|
|
464
464
|
targetProfileSlug: deal
|
|
465
465
|
type: linked_to_deal
|
|
466
466
|
|
|
467
|
+
onboarding:
|
|
468
|
+
goal: >
|
|
469
|
+
Turn the user's real commercial motion into a usable pipeline — the right
|
|
470
|
+
companies and people, their active opportunities, and an explicit next
|
|
471
|
+
step for every deal — so CRM becomes the place sales work moves forward.
|
|
472
|
+
framing: >
|
|
473
|
+
THE REVENUE OPERATOR: Act as a pragmatic sales lead setting up a CRM people
|
|
474
|
+
will actually maintain. Start from the user's existing motion, vocabulary,
|
|
475
|
+
and active conversations. Resolve before creating so imported or Pod-wide
|
|
476
|
+
people are reused rather than duplicated. Push for a concrete next step and
|
|
477
|
+
date; never invent revenue, probability, or contact details.
|
|
478
|
+
expertise:
|
|
479
|
+
starters:
|
|
480
|
+
- >
|
|
481
|
+
A useful first pipeline is deliberately small. Start with the few stages
|
|
482
|
+
that change how the team acts — contacted, qualifying, proposal,
|
|
483
|
+
negotiating, won, and lost — and keep the template's default stages
|
|
484
|
+
unless the user's real process needs a different view.
|
|
485
|
+
- >
|
|
486
|
+
Populate the workspace with live work, not a perfect historical archive:
|
|
487
|
+
propose the three to five opportunities the user is actively trying to
|
|
488
|
+
move, then add the companies and people needed to understand those deals.
|
|
489
|
+
- >
|
|
490
|
+
Every active deal needs one observable next step with a date. If the next
|
|
491
|
+
step is "follow up" or "keep warm", help turn it into a specific action
|
|
492
|
+
such as "send the revised proposal Thursday" before capturing it.
|
|
493
|
+
blindSpots:
|
|
494
|
+
- >
|
|
495
|
+
A deal without a real person is usually an account-shaped hope. If an
|
|
496
|
+
opportunity has no stakeholder, surface the gap instead of fabricating a
|
|
497
|
+
placeholder contact.
|
|
498
|
+
- >
|
|
499
|
+
Pipelines become fiction when stages describe confidence rather than an
|
|
500
|
+
event the buyer completed. Ask what evidence moves a deal into its current
|
|
501
|
+
stage, especially around proposal and negotiation.
|
|
502
|
+
- >
|
|
503
|
+
More fields do not create better sales hygiene. Prioritize stage, value
|
|
504
|
+
when known, expected close date, and next step; leave unverifiable fields
|
|
505
|
+
empty and offer enrichment separately.
|
|
506
|
+
bar: >
|
|
507
|
+
A sales lead can open the pipeline and immediately see the few live
|
|
508
|
+
opportunities, the real company and stakeholder behind each one, its
|
|
509
|
+
evidence-based stage, and the dated next action — with no duplicate people
|
|
510
|
+
and no invented commercial data.
|
|
511
|
+
collect:
|
|
512
|
+
- profileSlug: company
|
|
513
|
+
what: The real prospect organisations behind current conversations.
|
|
514
|
+
cardinality: few
|
|
515
|
+
keyFields: [website, industry, companySize, source, nextStep]
|
|
516
|
+
- profileSlug: person
|
|
517
|
+
what: The known stakeholders for those organisations and opportunities.
|
|
518
|
+
cardinality: few
|
|
519
|
+
keyFields: [email, jobTitle, company, source, nextFollowUpDate]
|
|
520
|
+
- profileSlug: deal
|
|
521
|
+
what: The live opportunities worth actively moving through the pipeline.
|
|
522
|
+
cardinality: few
|
|
523
|
+
keyFields: [dealStage, value, closeDate, nextStep]
|
|
524
|
+
openingQuestions:
|
|
525
|
+
- "Which three to five opportunities are you actively trying to move right now?"
|
|
526
|
+
- "For the most important one, which company and real stakeholder are involved?"
|
|
527
|
+
- "What has the buyer actually done so far, and what is the next dated action?"
|
|
528
|
+
doneWhen: >
|
|
529
|
+
At least one real opportunity is linked to its company or stakeholder and
|
|
530
|
+
has an evidence-based stage plus a concrete next step; the user can see how
|
|
531
|
+
to add or import the rest without duplicating existing people.
|
|
532
|
+
|
|
467
533
|
# Playbook — enriches a lead/prospect from public sources (company site,
|
|
468
534
|
# LinkedIn, firmographic signal) so ICP scoring and qualification start from
|
|
469
535
|
# real data instead of a blank record. Bound to `lead` so a newly captured
|
package/src/templates.ts
CHANGED
|
@@ -10643,6 +10643,66 @@ const DATA = {
|
|
|
10643
10643
|
"type": "linked_to_deal"
|
|
10644
10644
|
}
|
|
10645
10645
|
],
|
|
10646
|
+
"onboarding": {
|
|
10647
|
+
"goal": "Turn the user's real commercial motion into a usable pipeline — the right companies and people, their active opportunities, and an explicit next step for every deal — so CRM becomes the place sales work moves forward.\n",
|
|
10648
|
+
"framing": "THE REVENUE OPERATOR: Act as a pragmatic sales lead setting up a CRM people will actually maintain. Start from the user's existing motion, vocabulary, and active conversations. Resolve before creating so imported or Pod-wide people are reused rather than duplicated. Push for a concrete next step and date; never invent revenue, probability, or contact details.\n",
|
|
10649
|
+
"expertise": {
|
|
10650
|
+
"starters": [
|
|
10651
|
+
"A useful first pipeline is deliberately small. Start with the few stages that change how the team acts — contacted, qualifying, proposal, negotiating, won, and lost — and keep the template's default stages unless the user's real process needs a different view.\n",
|
|
10652
|
+
"Populate the workspace with live work, not a perfect historical archive: propose the three to five opportunities the user is actively trying to move, then add the companies and people needed to understand those deals.\n",
|
|
10653
|
+
"Every active deal needs one observable next step with a date. If the next step is \"follow up\" or \"keep warm\", help turn it into a specific action such as \"send the revised proposal Thursday\" before capturing it.\n"
|
|
10654
|
+
],
|
|
10655
|
+
"blindSpots": [
|
|
10656
|
+
"A deal without a real person is usually an account-shaped hope. If an opportunity has no stakeholder, surface the gap instead of fabricating a placeholder contact.\n",
|
|
10657
|
+
"Pipelines become fiction when stages describe confidence rather than an event the buyer completed. Ask what evidence moves a deal into its current stage, especially around proposal and negotiation.\n",
|
|
10658
|
+
"More fields do not create better sales hygiene. Prioritize stage, value when known, expected close date, and next step; leave unverifiable fields empty and offer enrichment separately.\n"
|
|
10659
|
+
],
|
|
10660
|
+
"bar": "A sales lead can open the pipeline and immediately see the few live opportunities, the real company and stakeholder behind each one, its evidence-based stage, and the dated next action — with no duplicate people and no invented commercial data.\n"
|
|
10661
|
+
},
|
|
10662
|
+
"collect": [
|
|
10663
|
+
{
|
|
10664
|
+
"profileSlug": "company",
|
|
10665
|
+
"what": "The real prospect organisations behind current conversations.",
|
|
10666
|
+
"cardinality": "few",
|
|
10667
|
+
"keyFields": [
|
|
10668
|
+
"website",
|
|
10669
|
+
"industry",
|
|
10670
|
+
"companySize",
|
|
10671
|
+
"source",
|
|
10672
|
+
"nextStep"
|
|
10673
|
+
]
|
|
10674
|
+
},
|
|
10675
|
+
{
|
|
10676
|
+
"profileSlug": "person",
|
|
10677
|
+
"what": "The known stakeholders for those organisations and opportunities.",
|
|
10678
|
+
"cardinality": "few",
|
|
10679
|
+
"keyFields": [
|
|
10680
|
+
"email",
|
|
10681
|
+
"jobTitle",
|
|
10682
|
+
"company",
|
|
10683
|
+
"source",
|
|
10684
|
+
"nextFollowUpDate"
|
|
10685
|
+
]
|
|
10686
|
+
},
|
|
10687
|
+
{
|
|
10688
|
+
"profileSlug": "deal",
|
|
10689
|
+
"what": "The live opportunities worth actively moving through the pipeline.",
|
|
10690
|
+
"cardinality": "few",
|
|
10691
|
+
"keyFields": [
|
|
10692
|
+
"dealStage",
|
|
10693
|
+
"value",
|
|
10694
|
+
"closeDate",
|
|
10695
|
+
"nextStep"
|
|
10696
|
+
]
|
|
10697
|
+
}
|
|
10698
|
+
],
|
|
10699
|
+
"openingQuestions": [
|
|
10700
|
+
"Which three to five opportunities are you actively trying to move right now?",
|
|
10701
|
+
"For the most important one, which company and real stakeholder are involved?",
|
|
10702
|
+
"What has the buyer actually done so far, and what is the next dated action?"
|
|
10703
|
+
],
|
|
10704
|
+
"doneWhen": "At least one real opportunity is linked to its company or stakeholder and has an evidence-based stage plus a concrete next step; the user can see how to add or import the rest without duplicating existing people.\n"
|
|
10705
|
+
},
|
|
10646
10706
|
"playbooks": [
|
|
10647
10707
|
{
|
|
10648
10708
|
"name": "Enrich Lead/Company",
|
package/src/types.ts
CHANGED
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
// App Reference — what opens as first screen when entering this workspace
|
|
11
11
|
// ============================================================================
|
|
12
12
|
|
|
13
|
-
export type TemplateAppKind =
|
|
13
|
+
export type TemplateAppKind = "native" | "stored" | "url";
|
|
14
14
|
|
|
15
15
|
export interface TemplateAppRef {
|
|
16
16
|
kind: TemplateAppKind;
|
|
17
17
|
/** For kind='native': the appId registered in the browser manifest */
|
|
18
18
|
appId?: string;
|
|
19
19
|
/** For kind='native': how the platform renders the app */
|
|
20
|
-
rendererType?:
|
|
20
|
+
rendererType?: "native" | "external" | "iframe-srcdoc";
|
|
21
21
|
/** For kind='stored': the cell key registered in the cell registry */
|
|
22
22
|
cellKey?: string;
|
|
23
23
|
/** For kind='stored': default props passed to the cell */
|
|
@@ -42,28 +42,38 @@ export type WorkspaceJsonValue =
|
|
|
42
42
|
|
|
43
43
|
export type WorkspacePrimarySurface =
|
|
44
44
|
| {
|
|
45
|
-
kind:
|
|
45
|
+
kind: "app";
|
|
46
46
|
appId: string;
|
|
47
|
-
rendererType:
|
|
47
|
+
rendererType: "native";
|
|
48
48
|
title?: string;
|
|
49
49
|
props?: Record<string, WorkspaceJsonValue>;
|
|
50
50
|
}
|
|
51
51
|
| {
|
|
52
|
-
kind:
|
|
52
|
+
kind: "app";
|
|
53
53
|
appId: string;
|
|
54
|
-
rendererType:
|
|
54
|
+
rendererType: "external";
|
|
55
55
|
url: string;
|
|
56
56
|
title?: string;
|
|
57
57
|
props?: Record<string, WorkspaceJsonValue>;
|
|
58
58
|
}
|
|
59
59
|
| {
|
|
60
|
-
kind:
|
|
60
|
+
kind: "cell";
|
|
61
61
|
cellKey: string;
|
|
62
62
|
title?: string;
|
|
63
63
|
props?: Record<string, WorkspaceJsonValue>;
|
|
64
64
|
}
|
|
65
65
|
| {
|
|
66
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Ordinary website. Unlike an external `app`, this has no trusted app
|
|
68
|
+
* identity and is always opened through the Browser's unprivileged URL
|
|
69
|
+
* surface.
|
|
70
|
+
*/
|
|
71
|
+
kind: "url";
|
|
72
|
+
url: string;
|
|
73
|
+
title?: string;
|
|
74
|
+
}
|
|
75
|
+
| {
|
|
76
|
+
kind: "view";
|
|
67
77
|
viewId: string;
|
|
68
78
|
title?: string;
|
|
69
79
|
};
|
|
@@ -75,13 +85,13 @@ export type WorkspacePrimarySurface =
|
|
|
75
85
|
*/
|
|
76
86
|
export type WorkspacePrimarySurfaceViewReference =
|
|
77
87
|
| {
|
|
78
|
-
kind:
|
|
88
|
+
kind: "view";
|
|
79
89
|
viewName: string;
|
|
80
90
|
viewSlug?: string;
|
|
81
91
|
title?: string;
|
|
82
92
|
}
|
|
83
93
|
| {
|
|
84
|
-
kind:
|
|
94
|
+
kind: "view";
|
|
85
95
|
viewName?: string;
|
|
86
96
|
viewSlug: string;
|
|
87
97
|
title?: string;
|
|
@@ -134,11 +144,11 @@ export interface TemplateProfile {
|
|
|
134
144
|
* `entity_scope` of an existing profile (a divergence is reported as an
|
|
135
145
|
* advisory `scopeConflict` and the live row is left alone).
|
|
136
146
|
*/
|
|
137
|
-
entityScope?:
|
|
147
|
+
entityScope?: "pod" | "workspace";
|
|
138
148
|
/** Semantic slug for system profiles. */
|
|
139
149
|
semanticSlug?: string;
|
|
140
150
|
/** Kind vs role profile. Omit for default ('kind'). */
|
|
141
|
-
profileKind?:
|
|
151
|
+
profileKind?: "kind" | "role";
|
|
142
152
|
/** For profileKind='role': which base-kind profile slugs this role can attach to. */
|
|
143
153
|
applicableKinds?: string[];
|
|
144
154
|
properties: TemplateProperty[];
|
|
@@ -201,22 +211,22 @@ export interface TemplateDisplayTemplate {
|
|
|
201
211
|
// ============================================================================
|
|
202
212
|
|
|
203
213
|
export interface TemplateSidebarSurface {
|
|
204
|
-
kind:
|
|
214
|
+
kind: "cell" | "view" | "entity" | "document" | "channel" | "app" | "url";
|
|
205
215
|
cellKey?: string;
|
|
206
216
|
viewId?: string;
|
|
207
217
|
viewName?: string;
|
|
208
218
|
appId?: string;
|
|
209
219
|
url?: string;
|
|
210
|
-
rendererType?:
|
|
211
|
-
placement?:
|
|
212
|
-
displayMode?:
|
|
220
|
+
rendererType?: "native" | "external" | "iframe-srcdoc";
|
|
221
|
+
placement?: "main" | "side" | "floating" | "modal" | "popover" | "embed";
|
|
222
|
+
displayMode?: "compact" | "medium" | "full";
|
|
213
223
|
props?: Record<string, unknown>;
|
|
214
224
|
title?: string;
|
|
215
225
|
meta?: Record<string, unknown>;
|
|
216
226
|
}
|
|
217
227
|
|
|
218
228
|
export interface TemplateSidebarItem {
|
|
219
|
-
kind:
|
|
229
|
+
kind: "app" | "view" | "profile" | "external" | "cell";
|
|
220
230
|
appId?: string;
|
|
221
231
|
viewName?: string;
|
|
222
232
|
viewId?: string;
|
|
@@ -253,7 +263,7 @@ export interface TemplateLayoutConfig {
|
|
|
253
263
|
// ============================================================================
|
|
254
264
|
|
|
255
265
|
export interface TemplateBentoBlock {
|
|
256
|
-
kind:
|
|
266
|
+
kind: "view" | "widget";
|
|
257
267
|
viewName?: string;
|
|
258
268
|
widgetType?: string;
|
|
259
269
|
pos: { x: number; y: number; w: number; h: number };
|
|
@@ -272,7 +282,7 @@ export interface TemplateProfileEntityBentoTemplates {
|
|
|
272
282
|
// ============================================================================
|
|
273
283
|
|
|
274
284
|
/** Mirrors `WorkspaceSourceRole` in synap-backend `schema/workspaces.ts`. */
|
|
275
|
-
export type TemplateSourceRole =
|
|
285
|
+
export type TemplateSourceRole = "provider" | "consumer" | "provider-consumer";
|
|
276
286
|
|
|
277
287
|
/** Mirrors `WorkspaceDefaultSource` in synap-backend `schema/workspaces.ts`. */
|
|
278
288
|
export interface TemplateDefaultSource {
|
|
@@ -302,7 +312,7 @@ export interface TemplateOnboardingCollectTarget {
|
|
|
302
312
|
/** Human description of what to capture for this target. */
|
|
303
313
|
what: string;
|
|
304
314
|
/** Roughly how many to expect — guides interview depth. */
|
|
305
|
-
cardinality?:
|
|
315
|
+
cardinality?: "one" | "few" | "several";
|
|
306
316
|
/** Key fields the agent should make sure to fill. */
|
|
307
317
|
keyFields?: string[];
|
|
308
318
|
}
|
|
@@ -357,7 +367,7 @@ export interface TemplateOnboarding {
|
|
|
357
367
|
|
|
358
368
|
export interface TemplatePlaybookParam {
|
|
359
369
|
name: string;
|
|
360
|
-
type:
|
|
370
|
+
type: "text" | "number" | "entity" | "choice" | "boolean";
|
|
361
371
|
label?: string;
|
|
362
372
|
required?: boolean;
|
|
363
373
|
defaultValue?: unknown;
|
|
@@ -368,12 +378,12 @@ export interface TemplatePlaybook {
|
|
|
368
378
|
description?: string;
|
|
369
379
|
goalTemplate: string;
|
|
370
380
|
params?: TemplatePlaybookParam[];
|
|
371
|
-
executor?:
|
|
372
|
-
inputStrategy?:
|
|
381
|
+
executor?: "is-agent" | "external-agent" | "hybrid";
|
|
382
|
+
inputStrategy?: "none" | "static" | "rotating" | "query";
|
|
373
383
|
channelSpec?: {
|
|
374
|
-
type:
|
|
384
|
+
type: "GROUP" | "AGENT_COLLAB" | "THREAD";
|
|
375
385
|
members?: string[];
|
|
376
|
-
aiReactionMode?:
|
|
386
|
+
aiReactionMode?: "on_mention" | "always" | "never";
|
|
377
387
|
};
|
|
378
388
|
/**
|
|
379
389
|
* A cron schedule that maintains ONE backing cron automation (a single
|
|
@@ -399,7 +409,7 @@ export interface TemplatePlaybook {
|
|
|
399
409
|
* captured entity of this kind can auto-launch the playbook as a session.
|
|
400
410
|
*/
|
|
401
411
|
subjectProfile?: { profileSlug: string; filter?: Record<string, unknown> };
|
|
402
|
-
status?:
|
|
412
|
+
status?: "draft" | "active" | "paused";
|
|
403
413
|
}
|
|
404
414
|
|
|
405
415
|
// ============================================================================
|
|
@@ -429,10 +439,10 @@ export interface TemplateAutomation {
|
|
|
429
439
|
key?: string;
|
|
430
440
|
name: string;
|
|
431
441
|
description?: string;
|
|
432
|
-
triggerType:
|
|
442
|
+
triggerType: "event" | "cron" | "webhook" | "manual";
|
|
433
443
|
triggerConfig?: Record<string, unknown>;
|
|
434
444
|
flow: { nodes: unknown[]; edges: unknown[]; precondition?: string };
|
|
435
|
-
status?:
|
|
445
|
+
status?: "draft" | "active" | "paused";
|
|
436
446
|
}
|
|
437
447
|
|
|
438
448
|
// ============================================================================
|
|
@@ -464,9 +474,9 @@ export interface TemplateActionPlacement {
|
|
|
464
474
|
/** Entity kind whose detail surface this action appears on. */
|
|
465
475
|
profileSlug: string;
|
|
466
476
|
/** Where the action renders. Only "entity-detail" is consumed today. */
|
|
467
|
-
surface:
|
|
477
|
+
surface: "entity-detail" | string;
|
|
468
478
|
/** What the action does when clicked. */
|
|
469
|
-
kind:
|
|
479
|
+
kind: "capability" | "playbook" | "automation";
|
|
470
480
|
/**
|
|
471
481
|
* Reference to the thing to run: a capability verb key, or a playbook /
|
|
472
482
|
* automation NAME (resolved to a row id at apply time).
|
|
@@ -509,7 +519,7 @@ export interface TemplateIntegration {
|
|
|
509
519
|
/** Override default params with {{var}} interpolation. */
|
|
510
520
|
params?: Record<string, string>;
|
|
511
521
|
/** "apply" (create+update) or "install" (create only). */
|
|
512
|
-
mode?:
|
|
522
|
+
mode?: "apply" | "install";
|
|
513
523
|
}
|
|
514
524
|
|
|
515
525
|
// ============================================================================
|
|
@@ -583,10 +593,10 @@ export interface TemplateExtendsRef {
|
|
|
583
593
|
* 3. Once no payload carries `'automation'`, drop it from the wire enums and this tuple.
|
|
584
594
|
*/
|
|
585
595
|
export const PACKAGE_DEPENDENCY_KINDS = [
|
|
586
|
-
|
|
587
|
-
|
|
596
|
+
"workspace",
|
|
597
|
+
"capability",
|
|
588
598
|
/** @deprecated Retired by CP 0049 in favour of `workflow`. See note above. */
|
|
589
|
-
|
|
599
|
+
"automation",
|
|
590
600
|
] as const;
|
|
591
601
|
|
|
592
602
|
/** What a `TemplateDependency` points at. Derived — never re-declare the list. */
|
|
@@ -610,7 +620,7 @@ export interface TemplateDependency {
|
|
|
610
620
|
* - `'require'` (default): the dependency must be present on the pod as its
|
|
611
621
|
* own artifact, but this template does not merge into it.
|
|
612
622
|
*/
|
|
613
|
-
relation?:
|
|
623
|
+
relation?: "compose" | "require";
|
|
614
624
|
/**
|
|
615
625
|
* Short human reason shown in the install-time "this also installs…" prompt.
|
|
616
626
|
*/
|
|
@@ -654,7 +664,7 @@ export interface WorkspaceYaml {
|
|
|
654
664
|
* Workspace classifier — consumed by createFromDefinition (settings.workspaceType,
|
|
655
665
|
* creator-role + agent-workspace behavior). Mirrors the backend union.
|
|
656
666
|
*/
|
|
657
|
-
workspaceType?:
|
|
667
|
+
workspaceType?: "personal" | "agent" | "project" | "operational";
|
|
658
668
|
visibility?: string;
|
|
659
669
|
/** Capabilities this workspace provides (e.g. "brand.library", "content.pipeline") */
|
|
660
670
|
capabilities?: string[];
|
|
@@ -751,7 +761,7 @@ export interface WorkspaceYaml {
|
|
|
751
761
|
|
|
752
762
|
export interface ResolvedWorkspaceTemplate {
|
|
753
763
|
meta: TemplateMeta;
|
|
754
|
-
definition: WorkspaceYaml[
|
|
764
|
+
definition: WorkspaceYaml["workspace"] & {
|
|
755
765
|
profiles: TemplateProfile[];
|
|
756
766
|
views: TemplateView[];
|
|
757
767
|
entityLinks: TemplateEntityLink[];
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { validateTemplate } from "./validate.js";
|
|
3
|
+
import type { WorkspaceYaml } from "./types.js";
|
|
4
|
+
|
|
5
|
+
function templateWithOnboarding(onboarding: unknown): WorkspaceYaml {
|
|
6
|
+
return {
|
|
7
|
+
meta: {
|
|
8
|
+
slug: "onboarding-validation",
|
|
9
|
+
name: "Onboarding validation",
|
|
10
|
+
description: "Validation fixture",
|
|
11
|
+
icon: "circle",
|
|
12
|
+
color: "blue",
|
|
13
|
+
},
|
|
14
|
+
workspace: {
|
|
15
|
+
name: "Onboarding validation",
|
|
16
|
+
description: "Validation fixture",
|
|
17
|
+
},
|
|
18
|
+
profiles: [
|
|
19
|
+
{
|
|
20
|
+
slug: "record",
|
|
21
|
+
displayName: "Record",
|
|
22
|
+
properties: [],
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
onboarding,
|
|
26
|
+
} as WorkspaceYaml;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("validateTemplate onboarding contract", () => {
|
|
30
|
+
it("accepts a complete domain recipe", () => {
|
|
31
|
+
const result = validateTemplate(
|
|
32
|
+
templateWithOnboarding({
|
|
33
|
+
goal: "Create the first useful record.",
|
|
34
|
+
framing: "Act as a domain expert.",
|
|
35
|
+
expertise: {
|
|
36
|
+
starters: ["Begin with the live work."],
|
|
37
|
+
blindSpots: ["Do not collect records without an owner."],
|
|
38
|
+
bar: "Every record has a clear next step.",
|
|
39
|
+
},
|
|
40
|
+
collect: [
|
|
41
|
+
{
|
|
42
|
+
profileSlug: "record",
|
|
43
|
+
what: "The records currently in motion.",
|
|
44
|
+
cardinality: "few",
|
|
45
|
+
keyFields: ["title"],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
openingQuestions: ["What is currently in motion?"],
|
|
49
|
+
doneWhen: "At least one useful record exists.",
|
|
50
|
+
}),
|
|
51
|
+
{}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(result, JSON.stringify(result.errors)).toEqual({
|
|
55
|
+
ok: true,
|
|
56
|
+
errors: [],
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("rejects empty expertise prose and collection instructions", () => {
|
|
61
|
+
const result = validateTemplate(
|
|
62
|
+
templateWithOnboarding({
|
|
63
|
+
goal: "Set up this workspace.",
|
|
64
|
+
framing: "Act as a domain expert.",
|
|
65
|
+
expertise: {
|
|
66
|
+
starters: [""],
|
|
67
|
+
blindSpots: [" "],
|
|
68
|
+
bar: "",
|
|
69
|
+
},
|
|
70
|
+
collect: [{ profileSlug: "record", what: "", keyFields: [""] }],
|
|
71
|
+
openingQuestions: [""],
|
|
72
|
+
doneWhen: "",
|
|
73
|
+
}),
|
|
74
|
+
{}
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(result.ok).toBe(false);
|
|
78
|
+
expect(result.errors.map((error) => error.rule)).toEqual(
|
|
79
|
+
expect.arrayContaining([
|
|
80
|
+
"onboarding-expertise-invalid",
|
|
81
|
+
"onboarding-collect-what-empty",
|
|
82
|
+
"onboarding-collect-key-fields-invalid",
|
|
83
|
+
"onboarding-opening-questions-invalid",
|
|
84
|
+
"onboarding-done-when-invalid",
|
|
85
|
+
])
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
});
|
package/src/validate.ts
CHANGED
|
@@ -274,7 +274,8 @@ export function validateTemplate(
|
|
|
274
274
|
if (!tpl.meta) add("meta-required", "meta", "Missing required field: meta");
|
|
275
275
|
else {
|
|
276
276
|
for (const f of REQUIRED_META_FIELDS) {
|
|
277
|
-
if (!tpl.meta[f])
|
|
277
|
+
if (!tpl.meta[f])
|
|
278
|
+
add("meta-field-required", `meta.${f}`, `meta.${f} is required`);
|
|
278
279
|
}
|
|
279
280
|
}
|
|
280
281
|
if (!tpl.workspace)
|
|
@@ -283,7 +284,9 @@ export function validateTemplate(
|
|
|
283
284
|
for (const f of REQUIRED_WORKSPACE_FIELDS) {
|
|
284
285
|
if (!tpl.workspace[f])
|
|
285
286
|
add(
|
|
286
|
-
"workspace-field-required",
|
|
287
|
+
"workspace-field-required",
|
|
288
|
+
`workspace.${f}`,
|
|
289
|
+
`workspace.${f} is required`
|
|
287
290
|
);
|
|
288
291
|
}
|
|
289
292
|
}
|
|
@@ -434,7 +437,110 @@ export function validateTemplate(
|
|
|
434
437
|
}
|
|
435
438
|
}
|
|
436
439
|
|
|
437
|
-
|
|
440
|
+
const onboarding = tpl.onboarding;
|
|
441
|
+
if (onboarding) {
|
|
442
|
+
if (typeof onboarding.goal !== "string" || onboarding.goal.trim() === "") {
|
|
443
|
+
add(
|
|
444
|
+
"onboarding-goal-empty",
|
|
445
|
+
"onboarding.goal",
|
|
446
|
+
"onboarding.goal must be a non-empty string"
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
if (
|
|
450
|
+
typeof onboarding.framing !== "string" ||
|
|
451
|
+
onboarding.framing.trim() === ""
|
|
452
|
+
) {
|
|
453
|
+
add(
|
|
454
|
+
"onboarding-framing-empty",
|
|
455
|
+
"onboarding.framing",
|
|
456
|
+
"onboarding.framing must be a non-empty string"
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
if (!Array.isArray(onboarding.collect) || onboarding.collect.length === 0) {
|
|
460
|
+
add(
|
|
461
|
+
"onboarding-collect-empty",
|
|
462
|
+
"onboarding.collect",
|
|
463
|
+
"onboarding.collect must contain at least one structured target"
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const expertise = onboarding.expertise;
|
|
468
|
+
for (const [field, values] of [
|
|
469
|
+
["starters", expertise?.starters],
|
|
470
|
+
["blindSpots", expertise?.blindSpots],
|
|
471
|
+
] as const) {
|
|
472
|
+
if (
|
|
473
|
+
values !== undefined &&
|
|
474
|
+
(!Array.isArray(values) ||
|
|
475
|
+
values.some(
|
|
476
|
+
(value) => typeof value !== "string" || value.trim() === ""
|
|
477
|
+
))
|
|
478
|
+
) {
|
|
479
|
+
add(
|
|
480
|
+
"onboarding-expertise-invalid",
|
|
481
|
+
`onboarding.expertise.${field}`,
|
|
482
|
+
`onboarding.expertise.${field} must contain only non-empty strings`
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (
|
|
487
|
+
expertise?.bar !== undefined &&
|
|
488
|
+
(typeof expertise.bar !== "string" || expertise.bar.trim() === "")
|
|
489
|
+
) {
|
|
490
|
+
add(
|
|
491
|
+
"onboarding-expertise-invalid",
|
|
492
|
+
"onboarding.expertise.bar",
|
|
493
|
+
"onboarding.expertise.bar must be a non-empty string"
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (
|
|
498
|
+
onboarding.openingQuestions !== undefined &&
|
|
499
|
+
(!Array.isArray(onboarding.openingQuestions) ||
|
|
500
|
+
onboarding.openingQuestions.some(
|
|
501
|
+
(question) => typeof question !== "string" || question.trim() === ""
|
|
502
|
+
))
|
|
503
|
+
) {
|
|
504
|
+
add(
|
|
505
|
+
"onboarding-opening-questions-invalid",
|
|
506
|
+
"onboarding.openingQuestions",
|
|
507
|
+
"onboarding.openingQuestions must contain only non-empty strings"
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
if (
|
|
511
|
+
onboarding.doneWhen !== undefined &&
|
|
512
|
+
(typeof onboarding.doneWhen !== "string" ||
|
|
513
|
+
onboarding.doneWhen.trim() === "")
|
|
514
|
+
) {
|
|
515
|
+
add(
|
|
516
|
+
"onboarding-done-when-invalid",
|
|
517
|
+
"onboarding.doneWhen",
|
|
518
|
+
"onboarding.doneWhen must be a non-empty string"
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
for (const c of onboarding?.collect || []) {
|
|
524
|
+
if (typeof c.what !== "string" || c.what.trim() === "") {
|
|
525
|
+
add(
|
|
526
|
+
"onboarding-collect-what-empty",
|
|
527
|
+
`onboarding.collect.${c.profileSlug}.what`,
|
|
528
|
+
`onboarding.collect target "${c.profileSlug}" must describe what to capture`
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
if (
|
|
532
|
+
c.keyFields !== undefined &&
|
|
533
|
+
(!Array.isArray(c.keyFields) ||
|
|
534
|
+
c.keyFields.some(
|
|
535
|
+
(field) => typeof field !== "string" || field.trim() === ""
|
|
536
|
+
))
|
|
537
|
+
) {
|
|
538
|
+
add(
|
|
539
|
+
"onboarding-collect-key-fields-invalid",
|
|
540
|
+
`onboarding.collect.${c.profileSlug}.keyFields`,
|
|
541
|
+
`onboarding.collect target "${c.profileSlug}" keyFields must contain only non-empty strings`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
438
544
|
if (!known(c.profileSlug))
|
|
439
545
|
add(
|
|
440
546
|
"onboarding-ref-unknown",
|
|
@@ -457,14 +563,17 @@ export function validateTemplate(
|
|
|
457
563
|
for (const a of tpl.automations || []) {
|
|
458
564
|
const refs: Array<{ where: string; slug: string }> = [];
|
|
459
565
|
collectProfileSlugRefs(
|
|
460
|
-
a.triggerConfig,
|
|
566
|
+
a.triggerConfig,
|
|
567
|
+
`automation "${a.name}".triggerConfig`,
|
|
461
568
|
refs
|
|
462
569
|
);
|
|
463
570
|
collectProfileSlugRefs(a.flow, `automation "${a.name}".flow`, refs);
|
|
464
571
|
for (const r of refs)
|
|
465
572
|
if (!known(r.slug))
|
|
466
573
|
add(
|
|
467
|
-
"automation-ref-unknown",
|
|
574
|
+
"automation-ref-unknown",
|
|
575
|
+
r.where,
|
|
576
|
+
`${r.where} references unknown profile "${r.slug}"`
|
|
468
577
|
);
|
|
469
578
|
|
|
470
579
|
// Node-contract validation. Previously validate.ts checked automations for
|
|
@@ -476,9 +585,8 @@ export function validateTemplate(
|
|
|
476
585
|
// author time); the structural + per-node contract checks always run.
|
|
477
586
|
const flow = a.flow ?? (a as { flowDefinition?: unknown }).flowDefinition;
|
|
478
587
|
if (flow != null) {
|
|
479
|
-
const { errors: flowErrors
|
|
480
|
-
|
|
481
|
-
for (const fe of flowErrors) {
|
|
588
|
+
const { errors: flowErrors } = validateFlowDefinition(flow);
|
|
589
|
+
for (const fe of flowErrors) {
|
|
482
590
|
const loc = fe.nodeId
|
|
483
591
|
? `automation "${a.name}".flow.node "${fe.nodeId}"`
|
|
484
592
|
: fe.edgeId
|
|
@@ -602,11 +710,15 @@ export function validateTemplate(
|
|
|
602
710
|
|
|
603
711
|
const doors: Array<
|
|
604
712
|
[
|
|
605
|
-
string,
|
|
713
|
+
string,
|
|
714
|
+
Array<{ widgetType?: unknown }>,
|
|
715
|
+
Array<{ viewName?: unknown; kind?: unknown }>,
|
|
606
716
|
]
|
|
607
717
|
> = [
|
|
608
718
|
[
|
|
609
|
-
"tRPC createFromDefinition",
|
|
719
|
+
"tRPC createFromDefinition",
|
|
720
|
+
definition.bentoLayout ?? [],
|
|
721
|
+
definition.bentoViewBlocks ?? [],
|
|
610
722
|
],
|
|
611
723
|
["CP packages/apply", pkg.bentoLayout ?? [], pkg.bentoViewBlocks ?? []],
|
|
612
724
|
];
|