@synap-core/workspace-templates 0.9.4 → 0.11.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/index.d.ts +154 -45
- package/dist/index.js +2462 -543
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__fixtures__/default-relation-defs.json +208 -0
- package/src/__fixtures__/report-automation-flow.json +393 -0
- package/src/agent-fleet.yaml +6 -1
- package/src/base.template.test.ts +160 -0
- package/src/base.yaml +839 -0
- package/src/blockchain-ecosystem.yaml +106 -0
- package/src/business-developer.template.test.ts +79 -10
- package/src/business-developer.yaml +61 -39
- package/src/catalog.ts +36 -0
- package/src/concept-guard.test.ts +47 -31
- package/src/concept-guard.ts +23 -19
- package/src/content-os.yaml +22 -0
- package/src/corpus-flow-validity.test.ts +130 -0
- package/src/crm.yaml +59 -8
- package/src/define.primary-surface.test.ts +2 -4
- package/src/define.ts +28 -0
- package/src/ecosystem.yaml +14 -0
- package/src/enterprise-os.yaml +1 -1
- package/src/foundation.yaml +8 -0
- package/src/grants.yaml +256 -1
- package/src/life-os.yaml +6 -1
- package/src/networking.yaml +14 -3
- package/src/operations.yaml +270 -0
- package/src/outreach-comms.yaml +196 -0
- package/src/project-management.yaml +6 -1
- package/src/templates.ts +1770 -129
- package/src/the-arch.yaml +4 -0
- package/src/types.ts +98 -34
- package/src/validate-flow.ts +186 -7
- package/src/validate.ts +26 -2
|
@@ -47,6 +47,10 @@ profiles:
|
|
|
47
47
|
description: A liquidity provider — makes markets and provides depth for a token.
|
|
48
48
|
scope: shared
|
|
49
49
|
profileKind: role
|
|
50
|
+
# Shared supply-role category → the intro-matcher's roster spans ALL roles
|
|
51
|
+
# tagged "provider" via entity.query { roleCategory: "provider" } (no per-role
|
|
52
|
+
# hardcoding). Keep this string in sync with networking.yaml's solution-provider.
|
|
53
|
+
roleCategory: provider
|
|
50
54
|
applicableKinds:
|
|
51
55
|
- company
|
|
52
56
|
- person
|
|
@@ -81,6 +85,10 @@ profiles:
|
|
|
81
85
|
description: A security firm or individual that audits smart contracts and protocols.
|
|
82
86
|
scope: shared
|
|
83
87
|
profileKind: role
|
|
88
|
+
# Shared supply-role category → the intro-matcher's roster spans ALL roles
|
|
89
|
+
# tagged "provider" via entity.query { roleCategory: "provider" } (no per-role
|
|
90
|
+
# hardcoding). Keep this string in sync with networking.yaml's solution-provider.
|
|
91
|
+
roleCategory: provider
|
|
84
92
|
applicableKinds:
|
|
85
93
|
- company
|
|
86
94
|
- person
|
|
@@ -115,6 +123,10 @@ profiles:
|
|
|
115
123
|
description: An outsourced engineering studio or build partner.
|
|
116
124
|
scope: shared
|
|
117
125
|
profileKind: role
|
|
126
|
+
# Shared supply-role category → the intro-matcher's roster spans ALL roles
|
|
127
|
+
# tagged "provider" via entity.query { roleCategory: "provider" } (no per-role
|
|
128
|
+
# hardcoding). Keep this string in sync with networking.yaml's solution-provider.
|
|
129
|
+
roleCategory: provider
|
|
118
130
|
applicableKinds:
|
|
119
131
|
- company
|
|
120
132
|
- person
|
|
@@ -149,6 +161,10 @@ profiles:
|
|
|
149
161
|
description: A brand, growth, or content marketing agency.
|
|
150
162
|
scope: shared
|
|
151
163
|
profileKind: role
|
|
164
|
+
# Shared supply-role category → the intro-matcher's roster spans ALL roles
|
|
165
|
+
# tagged "provider" via entity.query { roleCategory: "provider" } (no per-role
|
|
166
|
+
# hardcoding). Keep this string in sync with networking.yaml's solution-provider.
|
|
167
|
+
roleCategory: provider
|
|
152
168
|
applicableKinds:
|
|
153
169
|
- company
|
|
154
170
|
- person
|
|
@@ -183,6 +199,10 @@ profiles:
|
|
|
183
199
|
description: A performance / paid-media buying agency.
|
|
184
200
|
scope: shared
|
|
185
201
|
profileKind: role
|
|
202
|
+
# Shared supply-role category → the intro-matcher's roster spans ALL roles
|
|
203
|
+
# tagged "provider" via entity.query { roleCategory: "provider" } (no per-role
|
|
204
|
+
# hardcoding). Keep this string in sync with networking.yaml's solution-provider.
|
|
205
|
+
roleCategory: provider
|
|
186
206
|
applicableKinds:
|
|
187
207
|
- company
|
|
188
208
|
- person
|
|
@@ -217,6 +237,10 @@ profiles:
|
|
|
217
237
|
description: A key opinion leader or influencer who amplifies reach.
|
|
218
238
|
scope: shared
|
|
219
239
|
profileKind: role
|
|
240
|
+
# Shared supply-role category → the intro-matcher's roster spans ALL roles
|
|
241
|
+
# tagged "provider" via entity.query { roleCategory: "provider" } (no per-role
|
|
242
|
+
# hardcoding). Keep this string in sync with networking.yaml's solution-provider.
|
|
243
|
+
roleCategory: provider
|
|
220
244
|
applicableKinds:
|
|
221
245
|
- company
|
|
222
246
|
- person
|
|
@@ -243,6 +267,88 @@ profiles:
|
|
|
243
267
|
valueType: string
|
|
244
268
|
inputType: textarea
|
|
245
269
|
|
|
270
|
+
# ── Grant Program (the per-chain funding PROGRAM — parent of a round) ───────
|
|
271
|
+
# The 3-layer grants spine is PROGRAM ≠ ROUND ≠ APPLICATION. `round` (in
|
|
272
|
+
# grants.yaml) is the funding-cycle/deadline carrier; a program is the
|
|
273
|
+
# long-lived, chain-scoped funder above it (e.g. Stellar SCF, Optimism RetroPGF).
|
|
274
|
+
# It lives HERE, with the web3 vocabulary, because a program IS a blockchain-
|
|
275
|
+
# ecosystem thing (it carries a `blockchain` and program mechanics), and it is
|
|
276
|
+
# a real-world entity referenced from every workspace — hence pod-wide, mirroring
|
|
277
|
+
# company/person in ecosystem.yaml (`entityScope: pod`). `scope: shared` makes
|
|
278
|
+
# the profile DEF pod-wide too, so a REUSING template (grants.yaml `require`s this
|
|
279
|
+
# overlay) resolves `grant-program` for its `round → belongs_to → grant-program`
|
|
280
|
+
# edge and deadline view. `defaultPlaybook` is a text ref for now — Wave 2 will
|
|
281
|
+
# create the actual per-chain playbook and bind it.
|
|
282
|
+
- slug: grant-program
|
|
283
|
+
displayName: Grant Program
|
|
284
|
+
icon: landmark
|
|
285
|
+
color: emerald
|
|
286
|
+
description: A per-chain funding program (e.g. Stellar SCF, Optimism RetroPGF) — the long-lived funder above a round.
|
|
287
|
+
profileKind: kind
|
|
288
|
+
entityScope: pod
|
|
289
|
+
scope: shared
|
|
290
|
+
properties:
|
|
291
|
+
- slug: blockchain
|
|
292
|
+
label: Blockchain / Chain
|
|
293
|
+
valueType: string
|
|
294
|
+
inputType: text
|
|
295
|
+
- slug: programName
|
|
296
|
+
label: Program Name
|
|
297
|
+
valueType: string
|
|
298
|
+
inputType: text
|
|
299
|
+
- slug: programType
|
|
300
|
+
label: Program Type
|
|
301
|
+
valueType: string
|
|
302
|
+
inputType: select
|
|
303
|
+
enumValues:
|
|
304
|
+
- direct
|
|
305
|
+
- retroactive
|
|
306
|
+
- quadratic
|
|
307
|
+
- rfp
|
|
308
|
+
- slug: status
|
|
309
|
+
label: Status
|
|
310
|
+
valueType: string
|
|
311
|
+
inputType: select
|
|
312
|
+
enumValues:
|
|
313
|
+
- active
|
|
314
|
+
- paused
|
|
315
|
+
- retired
|
|
316
|
+
- slug: url
|
|
317
|
+
label: Program URL
|
|
318
|
+
valueType: string
|
|
319
|
+
inputType: url
|
|
320
|
+
- slug: cadence
|
|
321
|
+
label: Cadence
|
|
322
|
+
valueType: string
|
|
323
|
+
inputType: text
|
|
324
|
+
placeholder: every 6 weeks
|
|
325
|
+
- slug: nextDeadline
|
|
326
|
+
label: Next Deadline
|
|
327
|
+
valueType: date
|
|
328
|
+
inputType: date
|
|
329
|
+
- slug: submissionTracks
|
|
330
|
+
label: Submission Tracks
|
|
331
|
+
valueType: array
|
|
332
|
+
inputType: text
|
|
333
|
+
- slug: trancheModel
|
|
334
|
+
label: Tranche Model
|
|
335
|
+
valueType: string
|
|
336
|
+
inputType: text
|
|
337
|
+
placeholder: 4 tranches / 90 days
|
|
338
|
+
- slug: defaultPlaybook
|
|
339
|
+
label: Default Playbook
|
|
340
|
+
valueType: string
|
|
341
|
+
inputType: text
|
|
342
|
+
# Defaults to the BASE grant playbook (grants.yaml `Grant Process`), the
|
|
343
|
+
# data-driven process that reads THIS program's cadence/tracks/tranche
|
|
344
|
+
# model at runtime. Override per-program only for a differently-shaped chain.
|
|
345
|
+
constraints:
|
|
346
|
+
defaultValue: Grant Process
|
|
347
|
+
- slug: description
|
|
348
|
+
label: Description
|
|
349
|
+
valueType: string
|
|
350
|
+
inputType: textarea
|
|
351
|
+
|
|
246
352
|
# Web3 actor↔actor relations (company→company). `company` resolves from the
|
|
247
353
|
# Ecosystem base — not redeclared here.
|
|
248
354
|
entityLinks:
|
|
@@ -47,17 +47,30 @@ describe("Business Developer workspace overlay", () => {
|
|
|
47
47
|
expect(template.workspace.visibility).toBeUndefined();
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
it("
|
|
51
|
-
|
|
50
|
+
it("extends the SHARED lead/client roles instead of forking them", () => {
|
|
51
|
+
// The lifecycle roles are the pod-wide foundation SSOT, re-declared so this
|
|
52
|
+
// workspace receives the grant. Row-level fields must match foundation
|
|
53
|
+
// VERBATIM (one row, no overlay mechanism — shared-role-ssot rule (c)); the
|
|
54
|
+
// business-development fields ride along as workspace-overlay PROPERTIES.
|
|
55
|
+
for (const slug of ["lead", "client"]) {
|
|
52
56
|
const facet = profile(slug);
|
|
53
|
-
expect(facet.scope).toBe("
|
|
57
|
+
expect(facet.scope).toBe("shared");
|
|
54
58
|
expect(facet.profileKind).toBe("role");
|
|
55
|
-
expect(facet.applicableKinds).toEqual(["
|
|
59
|
+
expect(facet.applicableKinds).toEqual(["company", "person"]);
|
|
60
|
+
expect(facet.semanticSlug).toBeUndefined();
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
|
|
63
|
+
// The forked spellings are GONE — reintroducing either is the regression.
|
|
64
|
+
expect(
|
|
65
|
+
template.profiles.some((candidate) => ["crm-lead", "crm-client"].includes(candidate.slug)),
|
|
66
|
+
).toBe(false);
|
|
67
|
+
|
|
68
|
+
// Extras only: foundation's own `leadStage` / `clientStatus` / `notes` are
|
|
69
|
+
// NOT restated here (restating with a different definition is base drift).
|
|
70
|
+
expect([...propertySlugs("lead")]).not.toContain("leadStage");
|
|
71
|
+
expect([...propertySlugs("client")]).not.toContain("clientStatus");
|
|
59
72
|
|
|
60
|
-
expect([...propertySlugs("
|
|
73
|
+
expect([...propertySlugs("lead")].sort()).toEqual([
|
|
61
74
|
"assignedUserId",
|
|
62
75
|
"convertedAt",
|
|
63
76
|
"disqualificationReason",
|
|
@@ -70,6 +83,13 @@ describe("Business Developer workspace overlay", () => {
|
|
|
70
83
|
"qualificationNotes",
|
|
71
84
|
"source",
|
|
72
85
|
].sort());
|
|
86
|
+
|
|
87
|
+
expect([...propertySlugs("client")].sort()).toEqual([
|
|
88
|
+
"becameClientAt",
|
|
89
|
+
"handoffDestination",
|
|
90
|
+
"handoffNotes",
|
|
91
|
+
"handoffStatus",
|
|
92
|
+
].sort());
|
|
73
93
|
});
|
|
74
94
|
|
|
75
95
|
it("models proposals and auditable commission separately from leads", () => {
|
|
@@ -136,6 +156,10 @@ describe("Business Developer workspace overlay", () => {
|
|
|
136
156
|
});
|
|
137
157
|
|
|
138
158
|
it("relies on CRM's single generic counterparty relationship", () => {
|
|
159
|
+
// `linked_to_deal` stays the ONE generic counterparty edge the handoff
|
|
160
|
+
// flow's `find-party` node reads. The role-qualified edges beside it are
|
|
161
|
+
// ADDITIVE (a brokered deal has a client side and a partner side) and must
|
|
162
|
+
// never displace it — hence the exact-order assertion.
|
|
139
163
|
expect(
|
|
140
164
|
crmTemplate.entityLinks?.map(({ sourceProfileSlug, targetProfileSlug, type }) => ({
|
|
141
165
|
sourceProfileSlug,
|
|
@@ -145,6 +169,20 @@ describe("Business Developer workspace overlay", () => {
|
|
|
145
169
|
).toEqual([
|
|
146
170
|
{ sourceProfileSlug: "person", targetProfileSlug: "deal", type: "linked_to_deal" },
|
|
147
171
|
{ sourceProfileSlug: "company", targetProfileSlug: "deal", type: "linked_to_deal" },
|
|
172
|
+
{ sourceProfileSlug: "person", targetProfileSlug: "deal", type: "client_on_deal" },
|
|
173
|
+
{ sourceProfileSlug: "company", targetProfileSlug: "deal", type: "client_on_deal" },
|
|
174
|
+
{ sourceProfileSlug: "person", targetProfileSlug: "deal", type: "partner_on_deal" },
|
|
175
|
+
{ sourceProfileSlug: "company", targetProfileSlug: "deal", type: "partner_on_deal" },
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
// The handoff still reads ONLY the generic edge — a brokered deal's partner
|
|
179
|
+
// side must not be mistaken for the counterparty that becomes the client.
|
|
180
|
+
const handoff = template.automations?.find((a) => a.name === "Win and hand off");
|
|
181
|
+
const findParty = (handoff?.flow.nodes as Array<{ id?: string; data?: unknown }>).find(
|
|
182
|
+
(n) => n.id === "find-party",
|
|
183
|
+
);
|
|
184
|
+
expect((findParty?.data as { relationTypes?: string[] }).relationTypes).toEqual([
|
|
185
|
+
"linked_to_deal",
|
|
148
186
|
]);
|
|
149
187
|
});
|
|
150
188
|
|
|
@@ -152,7 +190,7 @@ describe("Business Developer workspace overlay", () => {
|
|
|
152
190
|
expect(
|
|
153
191
|
template.playbooks?.map(({ name, subjectProfile }) => ({ name, profileSlug: subjectProfile?.profileSlug })),
|
|
154
192
|
).toEqual([
|
|
155
|
-
{ name: "Qualify a CRM lead", profileSlug: "
|
|
193
|
+
{ name: "Qualify a CRM lead", profileSlug: "lead" },
|
|
156
194
|
{ name: "Advance a commercial proposal", profileSlug: "deal" },
|
|
157
195
|
]);
|
|
158
196
|
expect(template.playbooks?.[0]?.goalTemplate ?? "").toMatch(/Do not create a deal/);
|
|
@@ -215,18 +253,49 @@ describe("Business Developer workspace overlay", () => {
|
|
|
215
253
|
}),
|
|
216
254
|
);
|
|
217
255
|
|
|
256
|
+
// The handoff writes the SHARED role slugs — this is what makes the party
|
|
257
|
+
// a client pod-wide (and therefore visible to the Operations pickup).
|
|
258
|
+
expect(handoff?.flow.nodes).toContainEqual(
|
|
259
|
+
expect.objectContaining({
|
|
260
|
+
id: "attach-client",
|
|
261
|
+
data: expect.objectContaining({
|
|
262
|
+
outputType: "facet_attach",
|
|
263
|
+
config: expect.objectContaining({ facetSlug: "client" }),
|
|
264
|
+
}),
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
expect(handoff?.flow.nodes).toContainEqual(
|
|
268
|
+
expect.objectContaining({
|
|
269
|
+
id: "convert-lead",
|
|
270
|
+
data: expect.objectContaining({
|
|
271
|
+
outputType: "facet_update",
|
|
272
|
+
config: expect.objectContaining({ facetSlug: "lead" }),
|
|
273
|
+
}),
|
|
274
|
+
}),
|
|
275
|
+
);
|
|
276
|
+
expect(handoff?.flow.nodes).toContainEqual(
|
|
277
|
+
expect.objectContaining({
|
|
278
|
+
id: "guard-lead",
|
|
279
|
+
data: expect.objectContaining({
|
|
280
|
+
checks: expect.arrayContaining([
|
|
281
|
+
expect.objectContaining({ arrayIncludes: "lead" }),
|
|
282
|
+
]),
|
|
283
|
+
}),
|
|
284
|
+
}),
|
|
285
|
+
);
|
|
286
|
+
|
|
218
287
|
expect(template.actionPlacements).toEqual([
|
|
219
288
|
expect.objectContaining({
|
|
220
289
|
profileSlug: "person",
|
|
221
290
|
kind: "playbook",
|
|
222
291
|
ref: "Qualify a CRM lead",
|
|
223
|
-
when: { requiredFacetSlugs: ["
|
|
292
|
+
when: { requiredFacetSlugs: ["lead"] },
|
|
224
293
|
}),
|
|
225
294
|
expect.objectContaining({
|
|
226
295
|
profileSlug: "company",
|
|
227
296
|
kind: "playbook",
|
|
228
297
|
ref: "Qualify a CRM lead",
|
|
229
|
-
when: { requiredFacetSlugs: ["
|
|
298
|
+
when: { requiredFacetSlugs: ["lead"] },
|
|
230
299
|
}),
|
|
231
300
|
expect.objectContaining({
|
|
232
301
|
profileSlug: "deal",
|
|
@@ -256,7 +325,7 @@ describe("Business Developer workspace overlay", () => {
|
|
|
256
325
|
expect(playbooks).toHaveLength(2);
|
|
257
326
|
expect(playbooks[0]).toMatchObject({
|
|
258
327
|
name: "Qualify a CRM lead",
|
|
259
|
-
subjectProfile: { profileSlug: "
|
|
328
|
+
subjectProfile: { profileSlug: "lead" },
|
|
260
329
|
params: [{ name: "lead", type: "string", description: "CRM lead" }],
|
|
261
330
|
});
|
|
262
331
|
expect(playbooks[1]).toMatchObject({
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
# Business Developer — a composable, lead-first CRM operating model.
|
|
2
2
|
#
|
|
3
3
|
# This is an ADDITIVE overlay on the CRM workspace, not a second CRM and not a
|
|
4
|
-
# rewrite of shared identity profiles. It
|
|
5
|
-
# `
|
|
6
|
-
#
|
|
7
|
-
#
|
|
4
|
+
# rewrite of shared identity profiles. It REUSES the pod-wide `lead` and
|
|
5
|
+
# `client` roles from Foundation (`scope: shared`, `profileKind: role`) and adds
|
|
6
|
+
# its business-development properties to them as WORKSPACE OVERLAYS — the
|
|
7
|
+
# documented extension mechanism (`getEffectiveProperties`, and rule (b) of
|
|
8
|
+
# `shared-role-ssot.test.ts`). It used to fork them into workspace-scoped
|
|
9
|
+
# `crm-lead` / `crm-client`; that was a vocabulary fork of one real-world
|
|
10
|
+
# concept: the same company was a "lead" under one spelling and not under the
|
|
11
|
+
# other, so pod-wide surfaces, the Operations handoff, and the CRM app
|
|
12
|
+
# disagreed about who was a client. One concept, one slug, workspace-specific
|
|
13
|
+
# FIELDS.
|
|
8
14
|
#
|
|
9
15
|
# Lifecycle:
|
|
10
|
-
# person / company +
|
|
16
|
+
# person / company + lead → optional deal(s) → won deal → client
|
|
11
17
|
#
|
|
12
18
|
# Deals are commercial proposals attached through CRM's existing
|
|
13
19
|
# `linked_to_deal` relation. The CRM requires one counterparty before a deal
|
|
@@ -41,23 +47,33 @@ workspace:
|
|
|
41
47
|
- crm.business-development
|
|
42
48
|
|
|
43
49
|
profiles:
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
# ── Lead (reused shared role — SSOT is foundation.yaml) ───────────────────
|
|
51
|
+
# NOT a workspace-local copy: this RE-DECLARES foundation's pod-wide `lead`
|
|
52
|
+
# role so the CRM workspace receives the grant (shared profiles are
|
|
53
|
+
# grant-gated — a workspace only sees a shared role if it references the
|
|
54
|
+
# slug), and ADDS the business-development properties below.
|
|
55
|
+
#
|
|
56
|
+
# Row-level fields (displayName/icon/color/description/applicableKinds) are
|
|
57
|
+
# restated VERBATIM from foundation, exactly as crm.yaml does: there is ONE
|
|
58
|
+
# pod-wide row and no overlay mechanism for row-level fields, so a divergent
|
|
59
|
+
# value here would be silently false on every pod depending on apply order
|
|
60
|
+
# (shared-role-ssot rule (c)).
|
|
61
|
+
#
|
|
62
|
+
# The properties are EXTRAS ONLY. Foundation's own `leadStage` / `notes` are
|
|
63
|
+
# deliberately NOT restated — omitting is "no claim", and restating with a
|
|
64
|
+
# different definition is the base-drift rule (b) forbids. Extras land as
|
|
65
|
+
# WORKSPACE OVERLAYS via getEffectiveProperties, so this workspace's members
|
|
66
|
+
# see lifecycle/ownership fields that other workspaces don't.
|
|
67
|
+
- slug: lead
|
|
68
|
+
displayName: Lead
|
|
69
|
+
icon: user-plus
|
|
70
|
+
color: yellow
|
|
71
|
+
description: An early-pipeline prospect — not yet a client, being qualified or nurtured.
|
|
72
|
+
scope: shared
|
|
53
73
|
profileKind: role
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
# and is what the cross-template concept guard requires alongside its
|
|
58
|
-
# DECLARED_DIVERGENCES entry — see src/concept-guard.ts.
|
|
59
|
-
semanticSlug: crm.lead
|
|
60
|
-
applicableKinds: [person, company]
|
|
74
|
+
applicableKinds:
|
|
75
|
+
- company
|
|
76
|
+
- person
|
|
61
77
|
properties:
|
|
62
78
|
- slug: lifecycleStage
|
|
63
79
|
label: Lifecycle stage
|
|
@@ -129,19 +145,22 @@ profiles:
|
|
|
129
145
|
valueType: date
|
|
130
146
|
inputType: date
|
|
131
147
|
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
148
|
+
# ── Client (reused shared role — SSOT is foundation.yaml) ─────────────────
|
|
149
|
+
# Same pattern as `lead` above: foundation's pod-wide `client` role, restated
|
|
150
|
+
# verbatim at row level, EXTENDED with the handoff context this pre-sale
|
|
151
|
+
# addon owns. Client delivery, renewal, and service health belong to
|
|
152
|
+
# operations.yaml, which reads the SAME `client` role pod-wide — that shared
|
|
153
|
+
# slug is what makes the CRM → Operations handoff possible at all.
|
|
154
|
+
- slug: client
|
|
155
|
+
displayName: Client
|
|
156
|
+
icon: briefcase
|
|
138
157
|
color: emerald
|
|
139
|
-
description:
|
|
140
|
-
scope:
|
|
158
|
+
description: An active commercial relationship — created when a deal is won.
|
|
159
|
+
scope: shared
|
|
141
160
|
profileKind: role
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
161
|
+
applicableKinds:
|
|
162
|
+
- company
|
|
163
|
+
- person
|
|
145
164
|
properties:
|
|
146
165
|
- slug: becameClientAt
|
|
147
166
|
label: Became client at
|
|
@@ -404,7 +423,7 @@ playbooks:
|
|
|
404
423
|
deal simply because the lead is qualified. If a commercial proposal is
|
|
405
424
|
warranted, explain why and ask for or propose that next step separately.
|
|
406
425
|
subjectProfile:
|
|
407
|
-
profileSlug:
|
|
426
|
+
profileSlug: lead
|
|
408
427
|
params:
|
|
409
428
|
- { name: lead, type: entity, label: CRM lead, required: true }
|
|
410
429
|
executor: is-agent
|
|
@@ -477,8 +496,11 @@ automations:
|
|
|
477
496
|
- path: steps.read-deal.output.entity.properties.handoffStatus
|
|
478
497
|
notEquals: handed_off
|
|
479
498
|
message: This proposal has already been handed off.
|
|
499
|
+
# minLength, NOT exists: `exists` is a null check, so an empty
|
|
500
|
+
# string satisfies it and the handoff would proceed with no
|
|
501
|
+
# sourcer actually set. The intent here is "has a value".
|
|
480
502
|
- path: steps.read-deal.output.entity.properties.sourcedByUserId
|
|
481
|
-
|
|
503
|
+
minLength: 1
|
|
482
504
|
message: Set the business developer who sourced this proposal before handing it off.
|
|
483
505
|
- path: steps.read-deal.output.entity.properties.synapFeeAmount
|
|
484
506
|
numberGte: 0
|
|
@@ -518,7 +540,7 @@ automations:
|
|
|
518
540
|
label: Confirm qualified CRM lead
|
|
519
541
|
checks:
|
|
520
542
|
- path: steps.read-party.output.entity.facetSlugs
|
|
521
|
-
arrayIncludes:
|
|
543
|
+
arrayIncludes: lead
|
|
522
544
|
message: The commercial counterparty must still be a CRM lead before it becomes a CRM client.
|
|
523
545
|
|
|
524
546
|
# Source-deal claim is the durable idempotency boundary for the whole
|
|
@@ -699,7 +721,7 @@ automations:
|
|
|
699
721
|
outputType: facet_attach
|
|
700
722
|
config:
|
|
701
723
|
entityId: "{{steps.read-party.output.entity.id}}"
|
|
702
|
-
facetSlug:
|
|
724
|
+
facetSlug: client
|
|
703
725
|
properties:
|
|
704
726
|
becameClientAt: "{{steps.handoff-time.output.result}}"
|
|
705
727
|
handoffStatus: handed_off
|
|
@@ -712,7 +734,7 @@ automations:
|
|
|
712
734
|
outputType: facet_update
|
|
713
735
|
config:
|
|
714
736
|
entityId: "{{steps.read-party.output.entity.id}}"
|
|
715
|
-
facetSlug:
|
|
737
|
+
facetSlug: lead
|
|
716
738
|
properties:
|
|
717
739
|
lifecycleStage: converted
|
|
718
740
|
convertedAt: "{{steps.handoff-time.output.result}}"
|
|
@@ -809,14 +831,14 @@ actionPlacements:
|
|
|
809
831
|
kind: playbook
|
|
810
832
|
ref: Qualify a CRM lead
|
|
811
833
|
label: Qualify lead
|
|
812
|
-
when: { requiredFacetSlugs: [
|
|
834
|
+
when: { requiredFacetSlugs: [lead] }
|
|
813
835
|
|
|
814
836
|
- profileSlug: company
|
|
815
837
|
surface: entity-detail
|
|
816
838
|
kind: playbook
|
|
817
839
|
ref: Qualify a CRM lead
|
|
818
840
|
label: Qualify lead
|
|
819
|
-
when: { requiredFacetSlugs: [
|
|
841
|
+
when: { requiredFacetSlugs: [lead] }
|
|
820
842
|
|
|
821
843
|
- profileSlug: deal
|
|
822
844
|
surface: entity-detail
|
package/src/catalog.ts
CHANGED
|
@@ -33,6 +33,20 @@ import type { WorkspaceYaml } from "./types.js";
|
|
|
33
33
|
// Inputs
|
|
34
34
|
// ============================================================================
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* A lightweight, QUERY-TIME summary of one sub-object a package carries (a cell
|
|
38
|
+
* today; the shape generalizes to views/automations without change). Rides the
|
|
39
|
+
* catalog LIST projection so a surface can match/surface a sub-item WITHOUT the
|
|
40
|
+
* full `definition` — it carries `key + name` only, never the sub-object's code.
|
|
41
|
+
* `kind` is DERIVED (in the CP SELECT, from which sub-array the row came from),
|
|
42
|
+
* never a stored taxonomy.
|
|
43
|
+
*/
|
|
44
|
+
export interface PackageItemSummary {
|
|
45
|
+
kind: string;
|
|
46
|
+
key: string;
|
|
47
|
+
name: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
/**
|
|
37
51
|
* A bundled template — the canonical `WorkspaceYaml` this package ships. Callers
|
|
38
52
|
* pass `listPublicTemplates()` (18) or `listWorkspaceTemplates()` (21); the door
|
|
@@ -69,6 +83,13 @@ export interface RemoteEntry {
|
|
|
69
83
|
* never "unknown-so-hide".
|
|
70
84
|
*/
|
|
71
85
|
isPrivate?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Lightweight computed summary of the row's sub-objects (cells), carried on the
|
|
88
|
+
* LIST projection so a surface can surface a matching sub-item without the full
|
|
89
|
+
* `definition`. `undefined` on sources that don't compute it (bundled) — treated
|
|
90
|
+
* as "no known sub-items". See `PackageItemSummary`.
|
|
91
|
+
*/
|
|
92
|
+
items?: PackageItemSummary[];
|
|
72
93
|
/**
|
|
73
94
|
* The CP `definition` jsonb — opaque install payload. The door reads only
|
|
74
95
|
* `definition.sourcePackage.version` (the winner signal). Absent on browse rows
|
|
@@ -128,6 +149,11 @@ export interface CpPackageRow {
|
|
|
128
149
|
icon?: string | null;
|
|
129
150
|
/** Present only on `/mine` rows. `false` ⇒ private. Browse rows omit it. */
|
|
130
151
|
isPublic?: boolean;
|
|
152
|
+
/**
|
|
153
|
+
* Computed sub-object summary from `GET /api/packages` / `/mine` (query-time,
|
|
154
|
+
* not a stored column). Carries `key + name` per cell; never the cell code.
|
|
155
|
+
*/
|
|
156
|
+
items?: PackageItemSummary[];
|
|
131
157
|
}
|
|
132
158
|
|
|
133
159
|
/**
|
|
@@ -154,6 +180,7 @@ export function rowToRemoteEntry(
|
|
|
154
180
|
tags: row.tags ?? [],
|
|
155
181
|
icon: row.icon ?? null,
|
|
156
182
|
isPrivate,
|
|
183
|
+
items: row.items,
|
|
157
184
|
// Both list routes strip `definition`; absence ⇒ bundle wins a slug conflict,
|
|
158
185
|
// and the install path fetches the full definition per-slug when needed.
|
|
159
186
|
};
|
|
@@ -175,6 +202,12 @@ interface CatalogEntryBase {
|
|
|
175
202
|
icon?: string | null;
|
|
176
203
|
/** `true` ⇒ not publicly listed (internal bundled template, or private CP row). */
|
|
177
204
|
isPrivate: boolean;
|
|
205
|
+
/**
|
|
206
|
+
* The entry's sub-object summary (cells). Always present (defaults to `[]`) so a
|
|
207
|
+
* consumer can match sub-items uniformly. Bundled workspace templates carry no
|
|
208
|
+
* cells → `[]` (honest); a remote row carries its CP-computed `items`.
|
|
209
|
+
*/
|
|
210
|
+
items: PackageItemSummary[];
|
|
178
211
|
}
|
|
179
212
|
|
|
180
213
|
/** A catalog entry backed by a bundled template. Install via `toWorkspaceDefinition()` / `toPackageDefinition()` on `template`. */
|
|
@@ -256,6 +289,8 @@ function bundledToEntry(tpl: WorkspaceYaml): BundledCatalogEntry {
|
|
|
256
289
|
tags: tpl.meta.tags ?? [],
|
|
257
290
|
icon: tpl.meta.icon,
|
|
258
291
|
isPrivate: tpl.meta.isPublic === false,
|
|
292
|
+
// Bundled workspace templates carry no cells — be honest, not `undefined`.
|
|
293
|
+
items: [],
|
|
259
294
|
template: tpl,
|
|
260
295
|
};
|
|
261
296
|
}
|
|
@@ -271,6 +306,7 @@ function remoteToEntry(row: RemoteEntry): RemoteCatalogEntry {
|
|
|
271
306
|
tags: row.tags ?? [],
|
|
272
307
|
icon: row.icon ?? null,
|
|
273
308
|
isPrivate: row.isPrivate ?? false,
|
|
309
|
+
items: row.items ?? [],
|
|
274
310
|
remote: row,
|
|
275
311
|
};
|
|
276
312
|
}
|