@synap-core/workspace-templates 0.7.2 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/index.d.ts +207 -8
  2. package/dist/index.js +2148 -1480
  3. package/dist/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/src/agent-fleet.yaml +666 -108
  6. package/src/blockchain-ecosystem.yaml +1 -0
  7. package/src/brand-library.yaml +127 -12
  8. package/src/builder.yaml +116 -12
  9. package/src/business-developer.yaml +102 -18
  10. package/src/catalog.ts +13 -3
  11. package/src/communication.yaml +23 -453
  12. package/src/composition-graph.test.ts +26 -0
  13. package/src/content-os.yaml +103 -10
  14. package/src/content-studio.yaml +1 -0
  15. package/src/crm.yaml +1 -0
  16. package/src/define.ts +8 -8
  17. package/src/dev-dashboard.yaml +43 -9
  18. package/src/domains.test.ts +117 -0
  19. package/src/domains.ts +102 -0
  20. package/src/ecosystem.yaml +21 -4
  21. package/src/enterprise-os.yaml +1 -0
  22. package/src/finance.yaml +46 -5
  23. package/src/foundation.yaml +13 -1
  24. package/src/grants.yaml +1 -0
  25. package/src/hr.yaml +61 -8
  26. package/src/index.ts +29 -0
  27. package/src/internal-runbook.yaml +36 -5
  28. package/src/legal.yaml +97 -12
  29. package/src/life-os.yaml +188 -27
  30. package/src/marketing.yaml +15 -2
  31. package/src/meetings-knowledge.yaml +7 -1
  32. package/src/operations.yaml +347 -0
  33. package/src/outreach-comms.yaml +6 -1
  34. package/src/project-management.yaml +840 -111
  35. package/src/research-base.yaml +516 -0
  36. package/src/research-decision.yaml +158 -0
  37. package/src/shared-role-ssot.test.ts +268 -0
  38. package/src/social.yaml +105 -37
  39. package/src/templates.ts +4548 -4304
  40. package/src/the-arch.yaml +1 -0
  41. package/src/types.ts +9 -0
  42. package/src/validate.test.ts +81 -0
  43. package/src/validate.ts +561 -0
  44. package/src/personal.yaml +0 -177
@@ -0,0 +1,268 @@
1
+ /**
2
+ * TRIPWIRE — shared ecosystem roles are a single pod-wide SSOT.
3
+ *
4
+ * Ratified principle: the ecosystem role vocabulary (client, partner, sponsor,
5
+ * lead, churned, …) is defined ONCE (foundation.yaml) as `scope: shared`,
6
+ * `profileKind: role`, and merely REFERENCED by every other template. The
7
+ * template apply/dedup layer resolves a declared slug to the ONE pod-wide
8
+ * profile — so if two templates declare the same shared slug with DIFFERENT
9
+ * property bodies, they silently fight over one row (drift), and if a template
10
+ * declares one of these role slugs as a `kind` or workspace-scoped, it forks a
11
+ * second identity for a hat.
12
+ *
13
+ * WHY THIS LIVES HERE NOW (read before re-adding a backend copy):
14
+ * synap-backend once carried this guard at
15
+ * `packages/api/src/__tripwires__/template-shared-role-ssot.test.ts`, reaching
16
+ * ACROSS repos to read `synap-app/.../src/templates.ts` by relative path. That
17
+ * path is absent in synap-backend CI, so the guard was green locally and
18
+ * NON-FUNCTIONAL in CI — it guarded nothing where it mattered. The corpus it
19
+ * asserts against is authored HERE and the generated `templates.ts` (which
20
+ * `templates-freshness.test.ts` keeps in lockstep with the YAML) lives HERE, so
21
+ * this is where the guard belongs: it runs in THIS package's `pnpm test`,
22
+ * alongside `concept-guard.test.ts`, against the real bundled corpus.
23
+ * (synap-backend owns DELETING its now-redundant copy — see the coordination
24
+ * note in the handoff; this file does not touch it.)
25
+ *
26
+ * DERIVE, DON'T HARDCODE. The backend copy hardcoded
27
+ * `{client, partner, sponsor, lead, competitor}` — which had DRIFTED from the
28
+ * SSOT: foundation.yaml declares `churned`, not `competitor` (competitor is
29
+ * ecosystem.yaml's own shared role, not a foundation-owned one). We derive the
30
+ * shared-ROLE vocabulary from foundation.yaml itself, so the guarded set can
31
+ * never drift from the SSOT it guards. Shared KINDS are not centralized in
32
+ * foundation today (e.g. `platform` is owned by content-os), so the shared-kind
33
+ * set is derived from the corpus — still not a hardcoded list.
34
+ *
35
+ * (a) No template declares a foundation-owned shared-role slug as
36
+ * `profileKind: kind` or workspace-scoped.
37
+ * (a2) Mirror for shared KINDS: no template declares a shared-kind slug as a
38
+ * `role` or workspace-scoped.
39
+ * (b) No two templates declare the SAME property slug on the same `scope:
40
+ * shared` profile with DIFFERENT definitions. EXTRA properties are ALLOWED
41
+ * (they land as workspace overlays — the legitimate extension mechanism).
42
+ * (c) No two templates declare the same shared profile with CONFLICTING
43
+ * ROW-LEVEL fields (no overlay mechanism exists for these — one row, one
44
+ * value; absence = no claim).
45
+ * (d) Every shared ROLE slug has a non-empty `applicableKinds` declared by at
46
+ * least one template (else the pod-wide row accepts ANY kind as the role —
47
+ * `FacetRepository.attach` skips the gate when applicableKinds is empty).
48
+ */
49
+
50
+ import { describe, expect, it } from "vitest";
51
+ import { WORKSPACE_TEMPLATES } from "./templates.js";
52
+ import type { TemplateProfile, TemplateProperty, WorkspaceYaml } from "./types.js";
53
+
54
+ const templates: WorkspaceYaml[] = Object.values(WORKSPACE_TEMPLATES);
55
+
56
+ const profilesOf = (tpl: WorkspaceYaml): TemplateProfile[] => tpl.profiles ?? [];
57
+ const templateSlug = (tpl: WorkspaceYaml): string => tpl.meta?.slug ?? "(unknown)";
58
+
59
+ const isSharedScope = (scope?: string): boolean =>
60
+ (scope ?? "").toLowerCase() === "shared";
61
+ const isWorkspaceScope = (scope?: string): boolean =>
62
+ // Omitted scope defaults to workspace-scoped.
63
+ (scope ?? "workspace").toLowerCase() === "workspace";
64
+ const normalizeKind = (k?: string): "kind" | "role" => (k === "role" ? "role" : "kind");
65
+
66
+ // ── Derive the shared vocabulary (NOT a hardcoded list) ──────────────────────
67
+ // Shared ROLES: foundation.yaml is the SSOT — its `scope: shared` roles.
68
+ const foundation = WORKSPACE_TEMPLATES["foundation"];
69
+ const SHARED_ROLE_SLUGS = new Set(
70
+ profilesOf(foundation ?? ({} as WorkspaceYaml))
71
+ .filter((p) => isSharedScope(p.scope) && normalizeKind(p.profileKind) === "role")
72
+ .map((p) => p.slug)
73
+ );
74
+ // Shared KINDS: not centralized in foundation (e.g. `platform` lives in
75
+ // content-os), so derive from wherever the corpus declares scope:shared + kind.
76
+ const SHARED_KIND_SLUGS = new Set<string>();
77
+ for (const tpl of templates) {
78
+ for (const p of profilesOf(tpl)) {
79
+ if (isSharedScope(p.scope) && normalizeKind(p.profileKind) === "kind") {
80
+ SHARED_KIND_SLUGS.add(p.slug);
81
+ }
82
+ }
83
+ }
84
+
85
+ /** Stable fingerprint of a SINGLE property's DEFINITION (presentation excluded). */
86
+ function propertyDefFingerprint(p: TemplateProperty): string {
87
+ const constraints = (p.constraints ?? {}) as { defaultValue?: unknown };
88
+ return JSON.stringify({
89
+ valueType: p.valueType ?? null,
90
+ inputType: p.inputType ?? null,
91
+ enumValues: Array.isArray(p.enumValues) ? [...p.enumValues].sort() : null,
92
+ defaultValue: constraints.defaultValue ?? null,
93
+ });
94
+ }
95
+
96
+ /** ROW-LEVEL fields — the columns of the ONE pod-wide profile row. */
97
+ const ROW_LEVEL_FIELDS = [
98
+ "profileKind",
99
+ "applicableKinds",
100
+ "displayName",
101
+ "icon",
102
+ "color",
103
+ "description",
104
+ "semanticSlug",
105
+ ] as const;
106
+
107
+ /** Canonical, comparable form of ONE row-level field, or null for "not declared". */
108
+ function rowFieldValue(p: TemplateProfile, field: string): string | null {
109
+ const raw = (p as unknown as Record<string, unknown>)[field];
110
+ if (raw === undefined || raw === null) return null;
111
+ if (field === "applicableKinds") {
112
+ if (!Array.isArray(raw)) return null;
113
+ // Order-INSENSITIVE: applicableKinds is a SET (foundation vs grants declare
114
+ // client/partner/sponsor in opposite orders but mean the same thing).
115
+ return JSON.stringify([...(raw as string[])].sort());
116
+ }
117
+ return JSON.stringify(raw);
118
+ }
119
+
120
+ describe("workspace-templates — shared ecosystem role SSOT (corpus)", () => {
121
+ it("derives a non-empty shared-role vocabulary from foundation.yaml", () => {
122
+ // Guards the derivation itself: if foundation ever stops declaring its
123
+ // shared roles, rule (a) would silently guard nothing.
124
+ expect(foundation, "foundation template must exist in the corpus").toBeDefined();
125
+ expect([...SHARED_ROLE_SLUGS].sort()).toEqual(
126
+ ["churned", "client", "lead", "partner", "sponsor"]
127
+ );
128
+ });
129
+
130
+ it("(a) never declares a shared-role slug as a kind or workspace-scoped profile", () => {
131
+ const violations: string[] = [];
132
+ for (const tpl of templates) {
133
+ const tplSlug = templateSlug(tpl);
134
+ for (const p of profilesOf(tpl)) {
135
+ if (!SHARED_ROLE_SLUGS.has(p.slug)) continue;
136
+ if (normalizeKind(p.profileKind) !== "role") {
137
+ violations.push(
138
+ `${tplSlug}: role slug '${p.slug}' declared as profileKind='${normalizeKind(
139
+ p.profileKind
140
+ )}' (must be 'role')`
141
+ );
142
+ }
143
+ if (isWorkspaceScope(p.scope)) {
144
+ violations.push(
145
+ `${tplSlug}: role slug '${p.slug}' is workspace-scoped (scope='${
146
+ p.scope ?? "workspace"
147
+ }') — must be scope: shared`
148
+ );
149
+ }
150
+ }
151
+ }
152
+ expect(violations, violations.join("\n")).toEqual([]);
153
+ });
154
+
155
+ it("(a2) never declares a shared-KIND slug as a role or workspace-scoped profile", () => {
156
+ const violations: string[] = [];
157
+ for (const tpl of templates) {
158
+ const tplSlug = templateSlug(tpl);
159
+ for (const p of profilesOf(tpl)) {
160
+ if (!SHARED_KIND_SLUGS.has(p.slug)) continue;
161
+ if (normalizeKind(p.profileKind) !== "kind") {
162
+ violations.push(
163
+ `${tplSlug}: shared-kind slug '${p.slug}' declared as profileKind='role' (must be a kind)`
164
+ );
165
+ }
166
+ if (isWorkspaceScope(p.scope)) {
167
+ violations.push(
168
+ `${tplSlug}: shared-kind slug '${p.slug}' is workspace-scoped (scope='${
169
+ p.scope ?? "workspace"
170
+ }') — must be scope: shared so require-consumers reuse the pod-wide row`
171
+ );
172
+ }
173
+ }
174
+ }
175
+ expect(violations, violations.join("\n")).toEqual([]);
176
+ });
177
+
178
+ it("(b) never declares the same shared-profile property with CONFLICTING definitions (extras are legal overlays)", () => {
179
+ const defs = new Map<string, Map<string, Map<string, string[]>>>();
180
+ for (const tpl of templates) {
181
+ const tplSlug = templateSlug(tpl);
182
+ for (const p of profilesOf(tpl)) {
183
+ if (!isSharedScope(p.scope)) continue;
184
+ const perProfile = defs.get(p.slug) ?? new Map<string, Map<string, string[]>>();
185
+ for (const prop of p.properties ?? []) {
186
+ const propSlug = String(prop.slug);
187
+ const perProp = perProfile.get(propSlug) ?? new Map<string, string[]>();
188
+ const fp = propertyDefFingerprint(prop);
189
+ perProp.set(fp, [...(perProp.get(fp) ?? []), tplSlug]);
190
+ perProfile.set(propSlug, perProp);
191
+ }
192
+ defs.set(p.slug, perProfile);
193
+ }
194
+ }
195
+
196
+ const violations: string[] = [];
197
+ for (const [slug, perProfile] of defs) {
198
+ for (const [propSlug, perProp] of perProfile) {
199
+ if (perProp.size <= 1) continue;
200
+ const detail = [...perProp.entries()]
201
+ .map(([fp, carriers]) => `[${carriers.join(", ")}] => ${fp}`)
202
+ .join(" VS ");
203
+ violations.push(
204
+ `shared '${slug}.${propSlug}' is declared with ${perProp.size} DIFFERENT definitions — ` +
205
+ `they target the SAME pod-wide column, so the survivor depends on apply order (base drift): ${detail}`
206
+ );
207
+ }
208
+ }
209
+ expect(violations, violations.join("\n")).toEqual([]);
210
+ });
211
+
212
+ it("(c) never declares the same shared profile with CONFLICTING ROW-LEVEL fields (absence = no claim)", () => {
213
+ const decls = new Map<string, Map<string, Map<string, string[]>>>();
214
+ for (const tpl of templates) {
215
+ const tplSlug = templateSlug(tpl);
216
+ for (const p of profilesOf(tpl)) {
217
+ if (!isSharedScope(p.scope)) continue;
218
+ const perProfile = decls.get(p.slug) ?? new Map<string, Map<string, string[]>>();
219
+ for (const field of ROW_LEVEL_FIELDS) {
220
+ const value = rowFieldValue(p, field);
221
+ if (value === null) continue; // omitted = makes no claim
222
+ const perField = perProfile.get(field) ?? new Map<string, string[]>();
223
+ perField.set(value, [...(perField.get(value) ?? []), tplSlug]);
224
+ perProfile.set(field, perField);
225
+ }
226
+ decls.set(p.slug, perProfile);
227
+ }
228
+ }
229
+
230
+ const violations: string[] = [];
231
+ for (const [slug, perProfile] of decls) {
232
+ for (const [field, perField] of perProfile) {
233
+ if (perField.size <= 1) continue;
234
+ const detail = [...perField.entries()]
235
+ .map(([value, carriers]) => `[${carriers.join(", ")}] => ${value}`)
236
+ .join(" VS ");
237
+ violations.push(
238
+ `shared '${slug}.${field}' is declared with ${perField.size} DIFFERENT values — ` +
239
+ `there is ONE pod-wide row and no overlay mechanism for row-level fields, so the ` +
240
+ `losing template's value is simply false on every pod and which one loses depends ` +
241
+ `on apply order. The fix is for the template that does NOT own this slug to stop ` +
242
+ `redeclaring it (omitting = no claim): ${detail}`
243
+ );
244
+ }
245
+ }
246
+ expect(violations, violations.join("\n")).toEqual([]);
247
+ });
248
+
249
+ it("(d) every shared ROLE slug has applicableKinds declared by at least one template", () => {
250
+ const declared = new Map<string, boolean>();
251
+ for (const tpl of templates) {
252
+ for (const p of profilesOf(tpl)) {
253
+ if (!isSharedScope(p.scope)) continue;
254
+ if (normalizeKind(p.profileKind) !== "role") continue;
255
+ const has = Array.isArray(p.applicableKinds) && p.applicableKinds.length > 0;
256
+ declared.set(p.slug, (declared.get(p.slug) ?? false) || has);
257
+ }
258
+ }
259
+ const violations = [...declared.entries()]
260
+ .filter(([, has]) => !has)
261
+ .map(
262
+ ([slug]) =>
263
+ `shared role '${slug}' has NO template declaring a non-empty applicableKinds — ` +
264
+ `the pod-wide row would accept ANY entity kind as this role.`
265
+ );
266
+ expect(violations, violations.join("\n")).toEqual([]);
267
+ });
268
+ });
package/src/social.yaml CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  meta:
8
8
  slug: social
9
+ domain: content
9
10
  name: Social
10
11
  description: "Plan, schedule, and publish across your channels — then track how each post performs. Channels, posts, schedules, and engagement signals, all connected."
11
12
  icon: share-2
@@ -13,6 +14,17 @@ meta:
13
14
  tags: [social, distribution, publishing, channels]
14
15
  isPublic: true
15
16
 
17
+ # Social REUSES Content OS's pod-wide `platform` (publishing-account) vocabulary
18
+ # instead of forking its own `channel` kind. `platform` is `scope: shared` in
19
+ # content-os, so a `require`-consumer gains access by RE-DECLARING the slug (the
20
+ # resolver writes the grant on declare). This is a real standalone workspace —
21
+ # hence `require` (deps-first install), not `compose`.
22
+ dependencies:
23
+ - slug: content-os
24
+ kind: workspace
25
+ relation: require
26
+ reason: "Reuses Content OS's shared platform (publishing-account) vocabulary."
27
+
16
28
  workspace:
17
29
  name: Social
18
30
  description: "The distribution layer — the channels you publish on, the posts and schedules that pace them, and the engagement signals that tell you what's working."
@@ -28,35 +40,91 @@ workspace:
28
40
  social: provider
29
41
 
30
42
  profiles:
31
- - slug: channel
32
- displayName: Channel
33
- icon: radio
34
- color: "#1D9BF0"
35
- description: "A social account you publish on — a platform, a handle, and its audience."
43
+ # ── Platform (shared publishing-account vocabulary — SSOT is content-os.yaml) ─
44
+ # RE-DECLARED from content-os's `platform` (post-shared). This declaration is
45
+ # what grants THIS workspace the shared vocabulary — a `require`-consumer must
46
+ # DECLARE the shared slug, not merely reference it across the boundary. (It
47
+ # shares the profile DEFINITION, not the platform entities — no `entityScope:
48
+ # pod`.) The body must not conflict with content-os's — guarded locally by the
49
+ # shared-role SSOT check, not a CI gate.
50
+ - slug: platform
51
+ displayName: Platform
52
+ icon: share-2
53
+ color: "#06B6D4"
54
+ description: "A social account, newsletter publication, or podcast channel. Each platform has its own audience metrics and publishing rhythm."
55
+ scope: shared
36
56
  properties:
37
- - slug: platform
38
- label: Platform
57
+ - slug: platform-type
58
+ label: Type
39
59
  valueType: string
40
60
  inputType: select
41
- enumValues: [x, linkedin, instagram, tiktok, youtube, facebook, threads, other]
61
+ enumValues:
62
+ [
63
+ "LinkedIn",
64
+ "Instagram",
65
+ "TikTok",
66
+ "YouTube",
67
+ "Twitter / X",
68
+ "Threads",
69
+ "Bluesky",
70
+ "Newsletter",
71
+ "Podcast",
72
+ "Blog / Website",
73
+ "Other",
74
+ ]
42
75
  - slug: handle
43
76
  label: Handle
44
77
  valueType: string
45
78
  inputType: text
46
- - slug: channel-url
47
- label: URL
79
+ placeholder: "@username"
80
+ - slug: platform-status
81
+ label: Status
48
82
  valueType: string
49
- inputType: url
83
+ inputType: select
84
+ enumValues: ["Active", "Building", "Maintenance Mode", "Inactive"]
85
+ - slug: platform-url
86
+ label: Profile URL
87
+ valueType: string
88
+ inputType: text
89
+ placeholder: "https://..."
90
+ constraints: { format: url }
50
91
  - slug: follower-count
51
- label: Followers
92
+ label: "Follower / Subscriber Count"
52
93
  valueType: number
53
94
  inputType: number
54
- - slug: channel-status
55
- label: Status
95
+ - slug: follower-target
96
+ label: Target Count
97
+ valueType: number
98
+ inputType: number
99
+ - slug: avg-engagement-rate
100
+ label: "Avg Engagement Rate (%)"
101
+ valueType: number
102
+ inputType: number
103
+ - slug: platform-posting-freq
104
+ label: Posting Frequency
56
105
  valueType: string
57
106
  inputType: select
58
- enumValues: [active, paused, archived]
59
- constraints: { defaultValue: active }
107
+ enumValues:
108
+ [
109
+ "Daily",
110
+ "3x / week",
111
+ "2x / week",
112
+ "Weekly",
113
+ "Bi-weekly",
114
+ "Monthly",
115
+ "As Needed",
116
+ ]
117
+ - slug: last-post-date
118
+ label: Last Post Date
119
+ valueType: date
120
+ - slug: platform-notes
121
+ label: Notes
122
+ valueType: string
123
+ inputType: text
124
+ placeholder: "What works here? Tone, format, best times..."
125
+ - slug: platform-tags
126
+ label: Tags
127
+ valueType: array
60
128
 
61
129
  - slug: social-post
62
130
  displayName: Post
@@ -70,10 +138,10 @@ profiles:
70
138
  inputType: select
71
139
  enumValues: [draft, scheduled, published, failed]
72
140
  constraints: { defaultValue: draft }
73
- - slug: post-channel
74
- label: Channel
141
+ - slug: post-platform
142
+ label: Platform
75
143
  valueType: entity_id
76
- targetProfileSlug: channel
144
+ targetProfileSlug: platform
77
145
  inputType: entity_select
78
146
  - slug: post-content
79
147
  label: Content
@@ -102,10 +170,10 @@ profiles:
102
170
  color: "#8B5CF6"
103
171
  description: "A recurring cadence for a channel — the slot pattern that keeps posting consistent."
104
172
  properties:
105
- - slug: schedule-channel
106
- label: Channel
173
+ - slug: schedule-platform
174
+ label: Platform
107
175
  valueType: entity_id
108
- targetProfileSlug: channel
176
+ targetProfileSlug: platform
109
177
  inputType: entity_select
110
178
  - slug: cadence
111
179
  label: Cadence
@@ -127,10 +195,10 @@ profiles:
127
195
  color: "#10B981"
128
196
  description: "A performance reading for a channel over a period — reach, impressions, and the reactions that came back."
129
197
  properties:
130
- - slug: signal-channel
131
- label: Channel
198
+ - slug: signal-platform
199
+ label: Platform
132
200
  valueType: entity_id
133
- targetProfileSlug: channel
201
+ targetProfileSlug: platform
134
202
  inputType: entity_select
135
203
  - slug: reach
136
204
  label: Reach
@@ -173,11 +241,11 @@ views:
173
241
  scopeProfileSlug: social-post
174
242
  groupBy: post-status
175
243
  description: "Posts on a board by status — draft to published."
176
- - name: Channels
177
- slug: channels
244
+ - name: Platforms
245
+ slug: platforms
178
246
  type: table
179
- scopeProfileSlug: channel
180
- description: "The channels you publish on, with platform, handle, and audience."
247
+ scopeProfileSlug: platform
248
+ description: "The platforms you publish on, with type, handle, and audience."
181
249
  - name: Signals
182
250
  slug: signals
183
251
  type: table
@@ -196,14 +264,14 @@ onboarding:
196
264
  spraying thin across five. Refuse "we post everywhere" — ask which channel
197
265
  actually earns attention, and what the real cadence is.
198
266
  collect:
199
- - profileSlug: channel
200
- what: The channels you actually publish on — one per real account, with its platform and handle.
267
+ - profileSlug: platform
268
+ what: The platforms you actually publish on — one per real account, with its type and handle.
201
269
  cardinality: few
202
- keyFields: [platform, handle]
270
+ keyFields: [platform-type, handle]
203
271
  - profileSlug: social-post
204
- what: The posts in motion — drafts, scheduled, and recently published — each tied to its channel.
272
+ what: The posts in motion — drafts, scheduled, and recently published — each tied to its platform.
205
273
  cardinality: several
206
- keyFields: [post-status, post-channel]
274
+ keyFields: [post-status, post-platform]
207
275
  openingQuestions:
208
276
  - "Which channels are you actually active on right now, and which one earns the most attention?"
209
277
  - "What's your posting cadence per channel — daily, weekly, ad-hoc?"
@@ -226,8 +294,8 @@ layout:
226
294
  label: Content Calendar
227
295
  icon: calendar-clock
228
296
  - kind: view
229
- viewName: Channels
230
- label: Channels
297
+ viewName: Platforms
298
+ label: Platforms
231
299
  icon: radio
232
300
  - kind: view
233
301
  viewName: Signals
@@ -239,7 +307,7 @@ bento:
239
307
  viewName: Content Calendar
240
308
  blocks:
241
309
  - kind: view
242
- viewName: Channels
310
+ viewName: Platforms
243
311
  pos: { x: 0, y: 0, w: 6, h: 5 }
244
312
  - kind: view
245
313
  viewName: Signals