@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.
- package/dist/index.d.ts +207 -8
- package/dist/index.js +2148 -1480
- 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 +1 -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 +1 -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 +347 -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 +4548 -4304
- package/src/the-arch.yaml +1 -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
|
@@ -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
|
-
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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:
|
|
57
|
+
- slug: platform-type
|
|
58
|
+
label: Type
|
|
39
59
|
valueType: string
|
|
40
60
|
inputType: select
|
|
41
|
-
enumValues:
|
|
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
|
-
|
|
47
|
-
|
|
79
|
+
placeholder: "@username"
|
|
80
|
+
- slug: platform-status
|
|
81
|
+
label: Status
|
|
48
82
|
valueType: string
|
|
49
|
-
inputType:
|
|
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:
|
|
92
|
+
label: "Follower / Subscriber Count"
|
|
52
93
|
valueType: number
|
|
53
94
|
inputType: number
|
|
54
|
-
- slug:
|
|
55
|
-
label:
|
|
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:
|
|
59
|
-
|
|
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-
|
|
74
|
-
label:
|
|
141
|
+
- slug: post-platform
|
|
142
|
+
label: Platform
|
|
75
143
|
valueType: entity_id
|
|
76
|
-
targetProfileSlug:
|
|
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-
|
|
106
|
-
label:
|
|
173
|
+
- slug: schedule-platform
|
|
174
|
+
label: Platform
|
|
107
175
|
valueType: entity_id
|
|
108
|
-
targetProfileSlug:
|
|
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-
|
|
131
|
-
label:
|
|
198
|
+
- slug: signal-platform
|
|
199
|
+
label: Platform
|
|
132
200
|
valueType: entity_id
|
|
133
|
-
targetProfileSlug:
|
|
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:
|
|
177
|
-
slug:
|
|
244
|
+
- name: Platforms
|
|
245
|
+
slug: platforms
|
|
178
246
|
type: table
|
|
179
|
-
scopeProfileSlug:
|
|
180
|
-
description: "The
|
|
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:
|
|
200
|
-
what: The
|
|
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
|
|
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-
|
|
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:
|
|
230
|
-
label:
|
|
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:
|
|
310
|
+
viewName: Platforms
|
|
243
311
|
pos: { x: 0, y: 0, w: 6, h: 5 }
|
|
244
312
|
- kind: view
|
|
245
313
|
viewName: Signals
|