@synap-core/workspace-templates 0.7.2 → 0.7.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 +207 -8
- package/dist/index.js +2122 -1427
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/agent-fleet.yaml +666 -108
- package/src/blockchain-ecosystem.yaml +1 -0
- package/src/brand-library.yaml +127 -12
- package/src/builder.yaml +116 -12
- package/src/business-developer.yaml +102 -18
- package/src/catalog.ts +13 -3
- package/src/communication.yaml +23 -453
- package/src/composition-graph.test.ts +26 -0
- package/src/content-os.yaml +103 -10
- package/src/content-studio.yaml +1 -0
- package/src/crm.yaml +12 -0
- package/src/define.ts +8 -8
- package/src/dev-dashboard.yaml +43 -9
- package/src/domains.test.ts +117 -0
- package/src/domains.ts +102 -0
- package/src/ecosystem.yaml +21 -4
- package/src/enterprise-os.yaml +5 -0
- package/src/finance.yaml +46 -5
- package/src/foundation.yaml +13 -1
- package/src/grants.yaml +1 -0
- package/src/hr.yaml +61 -8
- package/src/index.ts +29 -0
- package/src/internal-runbook.yaml +36 -5
- package/src/legal.yaml +97 -12
- package/src/life-os.yaml +188 -27
- package/src/marketing.yaml +15 -2
- package/src/meetings-knowledge.yaml +7 -1
- package/src/operations.yaml +351 -0
- package/src/outreach-comms.yaml +6 -1
- package/src/project-management.yaml +840 -111
- package/src/research-base.yaml +516 -0
- package/src/research-decision.yaml +158 -0
- package/src/shared-role-ssot.test.ts +268 -0
- package/src/social.yaml +105 -37
- package/src/templates.ts +4573 -4302
- package/src/the-arch.yaml +11 -0
- package/src/types.ts +9 -0
- package/src/validate.test.ts +81 -0
- package/src/validate.ts +561 -0
- package/src/personal.yaml +0 -177
package/src/content-os.yaml
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
meta:
|
|
10
10
|
slug: content-os
|
|
11
|
+
domain: content
|
|
11
12
|
name: Content OS
|
|
12
13
|
description: "Your editorial operating system — pillars, posts, platforms, and newsletters. Turn raw thinking into a published content engine, all connected to your campaigns and brand voice."
|
|
13
14
|
icon: pen-tool
|
|
@@ -71,17 +72,51 @@ profiles:
|
|
|
71
72
|
label: Status
|
|
72
73
|
valueType: string
|
|
73
74
|
inputType: select
|
|
74
|
-
enumValues:
|
|
75
|
+
enumValues:
|
|
76
|
+
[
|
|
77
|
+
"Idea",
|
|
78
|
+
"Briefed",
|
|
79
|
+
"In Draft",
|
|
80
|
+
"Review",
|
|
81
|
+
"Scheduled",
|
|
82
|
+
"Published",
|
|
83
|
+
"Archived",
|
|
84
|
+
]
|
|
75
85
|
- slug: post-format
|
|
76
86
|
label: Format
|
|
77
87
|
valueType: string
|
|
78
88
|
inputType: select
|
|
79
|
-
enumValues:
|
|
89
|
+
enumValues:
|
|
90
|
+
[
|
|
91
|
+
"Article",
|
|
92
|
+
"Thread",
|
|
93
|
+
"Short Video",
|
|
94
|
+
"Long Video",
|
|
95
|
+
"Carousel",
|
|
96
|
+
"Image Post",
|
|
97
|
+
"Story",
|
|
98
|
+
"Newsletter Section",
|
|
99
|
+
"Podcast Episode",
|
|
100
|
+
"Other",
|
|
101
|
+
]
|
|
80
102
|
- slug: post-platform
|
|
81
103
|
label: Platform
|
|
82
104
|
valueType: string
|
|
83
105
|
inputType: select
|
|
84
|
-
enumValues:
|
|
106
|
+
enumValues:
|
|
107
|
+
[
|
|
108
|
+
"LinkedIn",
|
|
109
|
+
"Instagram",
|
|
110
|
+
"TikTok",
|
|
111
|
+
"YouTube",
|
|
112
|
+
"Twitter / X",
|
|
113
|
+
"Threads",
|
|
114
|
+
"Bluesky",
|
|
115
|
+
"Newsletter",
|
|
116
|
+
"Podcast",
|
|
117
|
+
"Blog",
|
|
118
|
+
"Other",
|
|
119
|
+
]
|
|
85
120
|
- slug: post-pillar
|
|
86
121
|
label: Pillar
|
|
87
122
|
valueType: entity_id
|
|
@@ -160,7 +195,16 @@ profiles:
|
|
|
160
195
|
label: Primary Formats
|
|
161
196
|
valueType: string
|
|
162
197
|
inputType: select
|
|
163
|
-
enumValues:
|
|
198
|
+
enumValues:
|
|
199
|
+
[
|
|
200
|
+
"Articles + Threads",
|
|
201
|
+
"Short Video",
|
|
202
|
+
"Long Video",
|
|
203
|
+
"Newsletter",
|
|
204
|
+
"Podcast",
|
|
205
|
+
"Carousel",
|
|
206
|
+
"Mixed",
|
|
207
|
+
]
|
|
164
208
|
- slug: post-count-pillar
|
|
165
209
|
label: Total Posts Published
|
|
166
210
|
valueType: number
|
|
@@ -178,23 +222,61 @@ profiles:
|
|
|
178
222
|
label: Target Frequency
|
|
179
223
|
valueType: string
|
|
180
224
|
inputType: select
|
|
181
|
-
enumValues:
|
|
225
|
+
enumValues:
|
|
226
|
+
[
|
|
227
|
+
"Daily",
|
|
228
|
+
"3x / week",
|
|
229
|
+
"2x / week",
|
|
230
|
+
"Weekly",
|
|
231
|
+
"Bi-weekly",
|
|
232
|
+
"Monthly",
|
|
233
|
+
"Ad hoc",
|
|
234
|
+
]
|
|
182
235
|
- slug: pillar-tags
|
|
183
236
|
label: Tags
|
|
184
237
|
valueType: array
|
|
185
238
|
|
|
239
|
+
# `platform` is the ONE pod-wide publishing-account *vocabulary*. `scope: shared`
|
|
240
|
+
# makes it a single pod-wide profile row that a `require`-consumer (e.g.
|
|
241
|
+
# social.yaml) can adopt by RE-DECLARING this slug — the resolver writes the
|
|
242
|
+
# grant on declare, giving that workspace the same vocabulary instead of forking
|
|
243
|
+
# its own `channel` kind. (This shares the profile DEFINITION, not the account
|
|
244
|
+
# entities — `platform` has no `entityScope: pod`, so each workspace's platform
|
|
245
|
+
# rows stay its own.) This is the deterministic form of the campaign reuse
|
|
246
|
+
# pattern (there, a workspace kind is promoted to shared at apply; here the
|
|
247
|
+
# SSOT is shared from the start). The re-declared body in social must not
|
|
248
|
+
# conflict with this one — guarded locally by the shared-role SSOT check, not a
|
|
249
|
+
# CI gate.
|
|
186
250
|
- slug: platform
|
|
187
251
|
displayName: Platform
|
|
188
252
|
icon: share-2
|
|
189
253
|
color: "#06B6D4"
|
|
190
254
|
description: "A social account, newsletter publication, or podcast channel. Each platform has its own audience metrics and publishing rhythm."
|
|
191
|
-
scope:
|
|
255
|
+
scope: shared
|
|
192
256
|
properties:
|
|
193
257
|
- slug: platform-type
|
|
194
258
|
label: Type
|
|
195
259
|
valueType: string
|
|
196
260
|
inputType: select
|
|
197
|
-
enumValues:
|
|
261
|
+
enumValues:
|
|
262
|
+
[
|
|
263
|
+
"LinkedIn",
|
|
264
|
+
"Instagram",
|
|
265
|
+
"TikTok",
|
|
266
|
+
"YouTube",
|
|
267
|
+
"Twitter / X",
|
|
268
|
+
"Threads",
|
|
269
|
+
"Bluesky",
|
|
270
|
+
"Newsletter",
|
|
271
|
+
"Podcast",
|
|
272
|
+
"Blog / Website",
|
|
273
|
+
"Other",
|
|
274
|
+
]
|
|
275
|
+
- slug: handle
|
|
276
|
+
label: Handle
|
|
277
|
+
valueType: string
|
|
278
|
+
inputType: text
|
|
279
|
+
placeholder: "@username"
|
|
198
280
|
- slug: platform-status
|
|
199
281
|
label: Status
|
|
200
282
|
valueType: string
|
|
@@ -222,7 +304,16 @@ profiles:
|
|
|
222
304
|
label: Posting Frequency
|
|
223
305
|
valueType: string
|
|
224
306
|
inputType: select
|
|
225
|
-
enumValues:
|
|
307
|
+
enumValues:
|
|
308
|
+
[
|
|
309
|
+
"Daily",
|
|
310
|
+
"3x / week",
|
|
311
|
+
"2x / week",
|
|
312
|
+
"Weekly",
|
|
313
|
+
"Bi-weekly",
|
|
314
|
+
"Monthly",
|
|
315
|
+
"As Needed",
|
|
316
|
+
]
|
|
226
317
|
- slug: last-post-date
|
|
227
318
|
label: Last Post Date
|
|
228
319
|
valueType: date
|
|
@@ -246,7 +337,8 @@ profiles:
|
|
|
246
337
|
label: Status
|
|
247
338
|
valueType: string
|
|
248
339
|
inputType: select
|
|
249
|
-
enumValues:
|
|
340
|
+
enumValues:
|
|
341
|
+
["Idea", "Drafting", "Editing", "Scheduled", "Sent", "Archived"]
|
|
250
342
|
- slug: newsletter-series
|
|
251
343
|
label: Series
|
|
252
344
|
valueType: string
|
|
@@ -327,7 +419,8 @@ profiles:
|
|
|
327
419
|
label: Channel
|
|
328
420
|
valueType: string
|
|
329
421
|
inputType: select
|
|
330
|
-
enumValues:
|
|
422
|
+
enumValues:
|
|
423
|
+
[email, social, event, content, partnership, paid, organic, other]
|
|
331
424
|
- slug: campaign-goal
|
|
332
425
|
label: Goal
|
|
333
426
|
valueType: string
|
package/src/content-studio.yaml
CHANGED
package/src/crm.yaml
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
meta:
|
|
8
8
|
slug: crm
|
|
9
|
+
domain: revenue
|
|
9
10
|
name: CRM
|
|
10
11
|
description: Sales pipeline and contact management from first contact to handoff.
|
|
11
12
|
icon: handshake
|
|
@@ -177,6 +178,17 @@ profiles:
|
|
|
177
178
|
- won
|
|
178
179
|
- lost
|
|
179
180
|
- inactive
|
|
181
|
+
- slug: commercialStage
|
|
182
|
+
label: Commercial stage
|
|
183
|
+
valueType: string
|
|
184
|
+
inputType: select
|
|
185
|
+
enumValues:
|
|
186
|
+
- draft
|
|
187
|
+
- proposed
|
|
188
|
+
- negotiating
|
|
189
|
+
- won
|
|
190
|
+
- lost
|
|
191
|
+
constraints: { defaultValue: draft }
|
|
180
192
|
- slug: valueType
|
|
181
193
|
label: Value type
|
|
182
194
|
valueType: string
|
package/src/define.ts
CHANGED
|
@@ -404,6 +404,7 @@ export interface PackageDefinitionOutput {
|
|
|
404
404
|
slug: string;
|
|
405
405
|
icon?: string;
|
|
406
406
|
color?: string;
|
|
407
|
+
domain?: string;
|
|
407
408
|
tags?: string[];
|
|
408
409
|
isPublic?: boolean;
|
|
409
410
|
requiredTier?: string | null;
|
|
@@ -442,13 +443,11 @@ export interface PackageDefinitionOutput {
|
|
|
442
443
|
automations?: PackageAutomationWire[];
|
|
443
444
|
/**
|
|
444
445
|
* Entity-detail action placements — carried verbatim (authored form IS the
|
|
445
|
-
* wire form).
|
|
446
|
-
* `hub-protocol/rest/packages.ts`)
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
* `
|
|
450
|
-
* `ActionPlacementSchema` field to `PackageApplySchema` to close the Hub half
|
|
451
|
-
* (out of the T2 fence — flagged in the handoff).
|
|
446
|
+
* wire form). The Hub `PackageApplySchema` (synap-backend
|
|
447
|
+
* `hub-protocol/rest/packages.ts`) now declares `actionPlacements`
|
|
448
|
+
* (`z.array(ActionPlacementSchema).optional()`), so BOTH the tRPC
|
|
449
|
+
* `createFromDefinition` door and the CLI/Hub install path carry it through to
|
|
450
|
+
* `applyPackagePostWorkspace` — the parity is closed.
|
|
452
451
|
*/
|
|
453
452
|
actionPlacements?: WorkspaceYaml['actionPlacements'];
|
|
454
453
|
onboarding?: WorkspaceYaml['onboarding'];
|
|
@@ -481,6 +480,7 @@ export function toPackageDefinition(
|
|
|
481
480
|
slug: tpl.meta.slug,
|
|
482
481
|
icon: tpl.meta.icon,
|
|
483
482
|
color: tpl.meta.color,
|
|
483
|
+
domain: tpl.meta.domain,
|
|
484
484
|
tags: tpl.meta.tags,
|
|
485
485
|
isPublic: tpl.meta.isPublic ?? true,
|
|
486
486
|
},
|
|
@@ -535,7 +535,7 @@ type PackageDefinitionForwarded =
|
|
|
535
535
|
| 'integrations' // → capabilities
|
|
536
536
|
| 'playbooks'
|
|
537
537
|
| 'automations' // → mapped to the apply door's flowDefinition wire shape
|
|
538
|
-
| 'actionPlacements' // → carried verbatim (Hub inbound schema
|
|
538
|
+
| 'actionPlacements' // → carried verbatim (Hub inbound schema declares it — parity closed)
|
|
539
539
|
| 'onboarding'
|
|
540
540
|
| 'dependencies';
|
|
541
541
|
|
package/src/dev-dashboard.yaml
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
meta:
|
|
9
9
|
slug: dev-dashboard
|
|
10
|
+
domain: build
|
|
10
11
|
name: "Synap Dev — Infrastructure Command Center"
|
|
11
12
|
description: "Internal DevOps dashboard for monitoring Synap infrastructure. Tracks services, vendors, and environments with health status, architecture flow map, and quick access to admin panels."
|
|
12
13
|
icon: server
|
|
@@ -123,7 +124,8 @@ profiles:
|
|
|
123
124
|
label: Vendor Type
|
|
124
125
|
valueType: string
|
|
125
126
|
inputType: select
|
|
126
|
-
enumValues:
|
|
127
|
+
enumValues:
|
|
128
|
+
[Hosting, AI, Auth, Database, Monitoring, CI/CD, DNS, Email, Other]
|
|
127
129
|
- slug: vendor-url
|
|
128
130
|
label: Vendor URL
|
|
129
131
|
valueType: string
|
|
@@ -271,7 +273,8 @@ profiles:
|
|
|
271
273
|
label: Type
|
|
272
274
|
valueType: string
|
|
273
275
|
inputType: select
|
|
274
|
-
enumValues:
|
|
276
|
+
enumValues:
|
|
277
|
+
[api_key, ssh_key, password, token, certificate, env_variable, other]
|
|
275
278
|
placeholder: "Select credential type"
|
|
276
279
|
- slug: service-name
|
|
277
280
|
label: Service
|
|
@@ -418,7 +421,14 @@ views:
|
|
|
418
421
|
type: table
|
|
419
422
|
scopeProfileSlug: service
|
|
420
423
|
config:
|
|
421
|
-
visibleColumns:
|
|
424
|
+
visibleColumns:
|
|
425
|
+
[
|
|
426
|
+
service-type,
|
|
427
|
+
environment,
|
|
428
|
+
deployment-branch,
|
|
429
|
+
last-deploy,
|
|
430
|
+
health-status,
|
|
431
|
+
]
|
|
422
432
|
sortBy: last-deploy
|
|
423
433
|
sortDirection: desc
|
|
424
434
|
|
|
@@ -854,12 +864,36 @@ bento:
|
|
|
854
864
|
config:
|
|
855
865
|
title: Quick Links
|
|
856
866
|
links:
|
|
857
|
-
- {
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
- {
|
|
867
|
+
- {
|
|
868
|
+
label: GitHub,
|
|
869
|
+
url: "https://github.com/synapsh",
|
|
870
|
+
color: "#24292E",
|
|
871
|
+
}
|
|
872
|
+
- {
|
|
873
|
+
label: Hetzner Console,
|
|
874
|
+
url: "https://console.hetzner.cloud",
|
|
875
|
+
color: "#D50C2D",
|
|
876
|
+
}
|
|
877
|
+
- {
|
|
878
|
+
label: Vercel Dashboard,
|
|
879
|
+
url: "https://vercel.com/dashboard",
|
|
880
|
+
color: "#000000",
|
|
881
|
+
}
|
|
882
|
+
- {
|
|
883
|
+
label: Ory Console,
|
|
884
|
+
url: "https://console.ory.sh",
|
|
885
|
+
color: "#5528FF",
|
|
886
|
+
}
|
|
887
|
+
- {
|
|
888
|
+
label: Cloudflare,
|
|
889
|
+
url: "https://dash.cloudflare.com",
|
|
890
|
+
color: "#F38020",
|
|
891
|
+
}
|
|
892
|
+
- {
|
|
893
|
+
label: OpenRouter,
|
|
894
|
+
url: "https://openrouter.ai/dashboard",
|
|
895
|
+
color: "#6366F1",
|
|
896
|
+
}
|
|
863
897
|
- kind: widget
|
|
864
898
|
widgetType: entity-list
|
|
865
899
|
pos: { x: 0, y: 7, w: 6, h: 5 }
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the domain taxonomy — the "works for any template" contract.
|
|
3
|
+
* Run: `npx vitest run packages/workspace-templates` (from the repo root).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import {
|
|
8
|
+
groupByDomain,
|
|
9
|
+
domainLabel,
|
|
10
|
+
TEMPLATE_DOMAINS,
|
|
11
|
+
OTHER_DOMAIN_ID,
|
|
12
|
+
isKnownDomain,
|
|
13
|
+
} from "./domains.js";
|
|
14
|
+
|
|
15
|
+
type Row = { slug: string; domain?: string };
|
|
16
|
+
const domainOf = (r: Row) => r.domain;
|
|
17
|
+
|
|
18
|
+
describe("domainLabel", () => {
|
|
19
|
+
it("returns the registry label for a known domain", () => {
|
|
20
|
+
expect(domainLabel("revenue")).toBe("Sales & Marketing");
|
|
21
|
+
});
|
|
22
|
+
it("returns 'More' for a missing domain", () => {
|
|
23
|
+
expect(domainLabel(undefined)).toBe("More");
|
|
24
|
+
expect(domainLabel(null)).toBe("More");
|
|
25
|
+
expect(domainLabel("")).toBe("More");
|
|
26
|
+
});
|
|
27
|
+
it("title-cases an unknown-but-declared domain so it still reads", () => {
|
|
28
|
+
expect(domainLabel("supply-chain")).toBe("Supply Chain");
|
|
29
|
+
expect(domainLabel("bizops")).toBe("Bizops");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("groupByDomain — works for ANY template", () => {
|
|
34
|
+
it("orders known domains by the registry, never alphabetically", () => {
|
|
35
|
+
const rows: Row[] = [
|
|
36
|
+
{ slug: "a", domain: "knowledge" }, // registry order 5
|
|
37
|
+
{ slug: "b", domain: "strategy" }, // registry order 0
|
|
38
|
+
{ slug: "c", domain: "content" }, // registry order 2
|
|
39
|
+
];
|
|
40
|
+
const groups = groupByDomain(rows, domainOf);
|
|
41
|
+
expect(groups.map((g) => g.id)).toEqual(["strategy", "content", "knowledge"]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("preserves input order WITHIN a section", () => {
|
|
45
|
+
const rows: Row[] = [
|
|
46
|
+
{ slug: "z", domain: "content" },
|
|
47
|
+
{ slug: "a", domain: "content" },
|
|
48
|
+
];
|
|
49
|
+
const [content] = groupByDomain(rows, domainOf);
|
|
50
|
+
expect(content.items.map((r) => r.slug)).toEqual(["z", "a"]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("places an unknown-but-declared domain AFTER all known ones", () => {
|
|
54
|
+
const rows: Row[] = [
|
|
55
|
+
{ slug: "x", domain: "supply-chain" }, // unknown
|
|
56
|
+
{ slug: "y", domain: "build" }, // known
|
|
57
|
+
];
|
|
58
|
+
const groups = groupByDomain(rows, domainOf);
|
|
59
|
+
expect(groups.map((g) => g.id)).toEqual(["build", "supply-chain"]);
|
|
60
|
+
expect(groups[1].label).toBe("Supply Chain");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("sorts multiple unknown domains alphabetically, before 'other'", () => {
|
|
64
|
+
const rows: Row[] = [
|
|
65
|
+
{ slug: "1", domain: "zeta" },
|
|
66
|
+
{ slug: "2" }, // missing → other
|
|
67
|
+
{ slug: "3", domain: "alpha" },
|
|
68
|
+
{ slug: "4", domain: "revenue" }, // known
|
|
69
|
+
];
|
|
70
|
+
const groups = groupByDomain(rows, domainOf);
|
|
71
|
+
expect(groups.map((g) => g.id)).toEqual([
|
|
72
|
+
"revenue",
|
|
73
|
+
"alpha",
|
|
74
|
+
"zeta",
|
|
75
|
+
OTHER_DOMAIN_ID,
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("routes a MISSING domain into the 'More' bucket, always LAST", () => {
|
|
80
|
+
const rows: Row[] = [
|
|
81
|
+
{ slug: "1" },
|
|
82
|
+
{ slug: "2", domain: "strategy" },
|
|
83
|
+
];
|
|
84
|
+
const groups = groupByDomain(rows, domainOf);
|
|
85
|
+
expect(groups[groups.length - 1].id).toBe(OTHER_DOMAIN_ID);
|
|
86
|
+
expect(groups[groups.length - 1].label).toBe("More");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("NEVER drops an item — count in equals count out", () => {
|
|
90
|
+
const rows: Row[] = [
|
|
91
|
+
{ slug: "1", domain: "build" },
|
|
92
|
+
{ slug: "2", domain: "made-up" },
|
|
93
|
+
{ slug: "3" },
|
|
94
|
+
{ slug: "4", domain: "build" },
|
|
95
|
+
];
|
|
96
|
+
const total = groupByDomain(rows, domainOf).reduce(
|
|
97
|
+
(n, g) => n + g.items.length,
|
|
98
|
+
0
|
|
99
|
+
);
|
|
100
|
+
expect(total).toBe(rows.length);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("omits empty sections", () => {
|
|
104
|
+
const groups = groupByDomain([{ slug: "1", domain: "build" }], domainOf);
|
|
105
|
+
expect(groups).toHaveLength(1);
|
|
106
|
+
expect(groups[0].id).toBe("build");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("isKnownDomain", () => {
|
|
111
|
+
it("recognises every registry id and rejects the rest", () => {
|
|
112
|
+
for (const d of TEMPLATE_DOMAINS) expect(isKnownDomain(d.id)).toBe(true);
|
|
113
|
+
expect(isKnownDomain("made-up")).toBe(false);
|
|
114
|
+
expect(isKnownDomain(undefined)).toBe(false);
|
|
115
|
+
expect(isKnownDomain(OTHER_DOMAIN_ID)).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
});
|
package/src/domains.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template DOMAIN taxonomy — the authored, cross-surface grouping that sections
|
|
3
|
+
* the marketplace (CLI `market list`, browser New Space + catalog, marketplace).
|
|
4
|
+
*
|
|
5
|
+
* WHY A FIELD, NOT DERIVED FROM TAGS. Grouping is a property of the template, so
|
|
6
|
+
* it lives ON the template (`meta.domain`) — authored once, read by every
|
|
7
|
+
* surface. The alternative was a hardcoded slug→group map (or a tag heuristic)
|
|
8
|
+
* inside each consumer; that map drifts the instant a template is added, and no
|
|
9
|
+
* one authoring a template can see it. Same lesson as the vocabulary work: a
|
|
10
|
+
* declared relationship should live where it can be verified, not re-guessed.
|
|
11
|
+
*
|
|
12
|
+
* NOT `category`. The catalog already uses `category` for the PACKAGE TYPE
|
|
13
|
+
* (`workspace`/`capability`/`skill`/…). `domain` is the human subject area.
|
|
14
|
+
*
|
|
15
|
+
* WORKS FOR ANY TEMPLATE. A template may declare a domain absent from this
|
|
16
|
+
* registry, or none at all — `groupByDomain` never drops one. Known domains
|
|
17
|
+
* render first in registry order; an unknown-but-declared domain renders after
|
|
18
|
+
* them (its id title-cased); a missing domain falls into the `other` bucket,
|
|
19
|
+
* always last. So a NEW template appears correctly without touching this file —
|
|
20
|
+
* adding it here only controls a domain's ORDER and LABEL.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export interface TemplateDomain {
|
|
24
|
+
id: string;
|
|
25
|
+
label: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const OTHER_DOMAIN_ID = "other";
|
|
29
|
+
export const OTHER_DOMAIN_LABEL = "More";
|
|
30
|
+
|
|
31
|
+
/** Known domains, in display order (the order sections appear in a listing). */
|
|
32
|
+
export const TEMPLATE_DOMAINS: readonly TemplateDomain[] = [
|
|
33
|
+
{ id: "strategy", label: "Strategy & Market" },
|
|
34
|
+
{ id: "revenue", label: "Sales & Marketing" },
|
|
35
|
+
{ id: "content", label: "Content & Social" },
|
|
36
|
+
{ id: "company", label: "Company & Operations" },
|
|
37
|
+
{ id: "build", label: "Build & Operate" },
|
|
38
|
+
{ id: "knowledge", label: "Knowledge & Research" },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const KNOWN = new Map(
|
|
42
|
+
TEMPLATE_DOMAINS.map((d, i) => [d.id, { label: d.label, order: i }] as const)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
/** Every valid domain id the registry knows about (excludes `other`). */
|
|
46
|
+
export function isKnownDomain(id: string | undefined | null): boolean {
|
|
47
|
+
return id != null && KNOWN.has(id);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Display label for a domain id. Missing → "More"; unknown-declared → title-cased id. */
|
|
51
|
+
export function domainLabel(id: string | undefined | null): string {
|
|
52
|
+
if (!id) return OTHER_DOMAIN_LABEL;
|
|
53
|
+
const known = KNOWN.get(id);
|
|
54
|
+
if (known) return known.label;
|
|
55
|
+
return id
|
|
56
|
+
.replace(/[-_]/g, " ")
|
|
57
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface DomainGroup<T> {
|
|
61
|
+
id: string;
|
|
62
|
+
label: string;
|
|
63
|
+
items: T[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Partition `items` into ordered domain sections: known domains first (registry
|
|
68
|
+
* order), then unknown-but-declared domains (alphabetical), then the missing
|
|
69
|
+
* "More" bucket, always last. Empty sections are omitted; nothing is dropped.
|
|
70
|
+
* Item order WITHIN a section is preserved from the input.
|
|
71
|
+
*/
|
|
72
|
+
export function groupByDomain<T>(
|
|
73
|
+
items: readonly T[],
|
|
74
|
+
domainOf: (item: T) => string | undefined | null
|
|
75
|
+
): DomainGroup<T>[] {
|
|
76
|
+
const buckets = new Map<string, T[]>();
|
|
77
|
+
for (const item of items) {
|
|
78
|
+
const id = domainOf(item) || OTHER_DOMAIN_ID;
|
|
79
|
+
const bucket = buckets.get(id);
|
|
80
|
+
if (bucket) bucket.push(item);
|
|
81
|
+
else buckets.set(id, [item]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const rank = (id: string): number => {
|
|
85
|
+
if (id === OTHER_DOMAIN_ID) return Number.MAX_SAFE_INTEGER;
|
|
86
|
+
const known = KNOWN.get(id);
|
|
87
|
+
return known ? known.order : 1_000_000; // unknown-declared: after known, before "other"
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return [...buckets.entries()]
|
|
91
|
+
.sort(([a], [b]) => {
|
|
92
|
+
const ra = rank(a);
|
|
93
|
+
const rb = rank(b);
|
|
94
|
+
if (ra !== rb) return ra - rb;
|
|
95
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
96
|
+
})
|
|
97
|
+
.map(([id, groupItems]) => ({
|
|
98
|
+
id,
|
|
99
|
+
label: id === OTHER_DOMAIN_ID ? OTHER_DOMAIN_LABEL : domainLabel(id),
|
|
100
|
+
items: groupItems,
|
|
101
|
+
}));
|
|
102
|
+
}
|
package/src/ecosystem.yaml
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
meta:
|
|
10
10
|
slug: ecosystem
|
|
11
|
+
domain: strategy
|
|
11
12
|
name: Ecosystem
|
|
12
13
|
description: Map the market you operate in — the partners, competitors, and players, the segments and trends, and the relationships between them.
|
|
13
14
|
icon: radar
|
|
@@ -170,7 +171,8 @@ profiles:
|
|
|
170
171
|
label: Type
|
|
171
172
|
valueType: string
|
|
172
173
|
inputType: select
|
|
173
|
-
enumValues:
|
|
174
|
+
enumValues:
|
|
175
|
+
[raise, partnership, launch, acquisition, listing, conference, other]
|
|
174
176
|
- slug: event-date
|
|
175
177
|
label: Date
|
|
176
178
|
valueType: date
|
|
@@ -390,9 +392,19 @@ playbooks:
|
|
|
390
392
|
profileSlug: competitor
|
|
391
393
|
params:
|
|
392
394
|
- { name: competitor, type: entity, label: Competitor, required: true }
|
|
393
|
-
- {
|
|
395
|
+
- {
|
|
396
|
+
name: focus,
|
|
397
|
+
type: text,
|
|
398
|
+
label: Focus area,
|
|
399
|
+
defaultValue: "product, pricing, and positioning",
|
|
400
|
+
}
|
|
394
401
|
- { name: companyContext, type: text, label: Our company context }
|
|
395
|
-
- {
|
|
402
|
+
- {
|
|
403
|
+
name: outputTypes,
|
|
404
|
+
type: text,
|
|
405
|
+
label: Outputs,
|
|
406
|
+
defaultValue: "a head-to-head comparison summary and an updated competitor profile",
|
|
407
|
+
}
|
|
396
408
|
executor: is-agent
|
|
397
409
|
# A radar MONITOR: bound to the competitor KIND + a schedule → the backing
|
|
398
410
|
# cron automation fans out (query→loop→playbook_run) and runs this once per
|
|
@@ -423,7 +435,12 @@ playbooks:
|
|
|
423
435
|
angles you covered, and why each new one matters.
|
|
424
436
|
schedule: { cron: "0 9 * * 1", enabled: false }
|
|
425
437
|
params:
|
|
426
|
-
- {
|
|
438
|
+
- {
|
|
439
|
+
name: ourSpace,
|
|
440
|
+
type: text,
|
|
441
|
+
label: Our market / category,
|
|
442
|
+
required: true,
|
|
443
|
+
}
|
|
427
444
|
executor: is-agent
|
|
428
445
|
grants:
|
|
429
446
|
- exa_search
|
package/src/enterprise-os.yaml
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
meta:
|
|
22
22
|
slug: enterprise-os
|
|
23
|
+
domain: company
|
|
23
24
|
name: Enterprise OS
|
|
24
25
|
description: Stand up a company's public operating core in one install — strategy, market, operations, and pipeline — plus a command tower to track objectives across them.
|
|
25
26
|
icon: layout-dashboard
|
|
@@ -47,6 +48,10 @@ dependencies:
|
|
|
47
48
|
kind: workspace
|
|
48
49
|
relation: require
|
|
49
50
|
reason: "The pipeline — contacts, leads, clients, and deals."
|
|
51
|
+
- slug: business-developer
|
|
52
|
+
kind: workspace
|
|
53
|
+
relation: require
|
|
54
|
+
reason: "The lead-first sales motion (crm-lead/crm-client roles, commercial proposals, commission handoff) that the CRM app runs on. Composes onto the CRM workspace above — without it the CRM lens is provisioned with pod-wide lead/client only and the app's pipeline is empty."
|
|
50
55
|
- slug: content-os
|
|
51
56
|
kind: workspace
|
|
52
57
|
relation: require
|