@synap-core/workspace-templates 0.12.0 → 0.24.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.
Files changed (237) hide show
  1. package/README.md +509 -0
  2. package/automations/bookmark-enrichment.automation.json +321 -0
  3. package/automations/github-pr-events.automation.json +505 -0
  4. package/automations/mail-feed.automation.json +86 -0
  5. package/automations/ship-a-change-checks-green.automation.json +106 -0
  6. package/automations/ship-a-change-checks-red.automation.json +106 -0
  7. package/automations/ship-a-change-deployed.automation.json +56 -0
  8. package/automations/ship-a-change-pr-merged.automation.json +110 -0
  9. package/automations/ship-a-change-pr-opened.automation.json +104 -0
  10. package/automations/url-bookmark-capture.automation.json +132 -0
  11. package/capabilities/agency-skills.capability.json +157 -0
  12. package/capabilities/apify.capability.json +265 -0
  13. package/capabilities/apollo.capability.json +120 -0
  14. package/capabilities/arch-backend.capability.json +97 -0
  15. package/capabilities/arch-client-intelligence.capability.json +401 -0
  16. package/capabilities/backblaze-b2.capability.json +205 -0
  17. package/capabilities/cal-com.capability.json +266 -0
  18. package/capabilities/canva.capability.json +274 -0
  19. package/capabilities/claude-code-routines.capability.json +172 -0
  20. package/capabilities/claude-managed-agents.capability.json +215 -0
  21. package/capabilities/client-comms-report.capability.json +246 -0
  22. package/capabilities/content-idea-to-production.capability.json +29 -0
  23. package/capabilities/datagouv.capability.json +315 -0
  24. package/capabilities/discord-bot.capability.json +157 -0
  25. package/capabilities/dropbox.capability.json +281 -0
  26. package/capabilities/exa.capability.json +109 -0
  27. package/capabilities/fal-ai.capability.json +83 -0
  28. package/capabilities/fal-media-lab.capability.json +158 -0
  29. package/capabilities/fireflies.capability.json +191 -0
  30. package/capabilities/freellmapi.capability.json +114 -0
  31. package/capabilities/generic-apikey.capability.json +92 -0
  32. package/capabilities/github-merge.capability.json +99 -0
  33. package/capabilities/github-read.capability.json +325 -0
  34. package/capabilities/governance-tighten-recommender.capability.json +81 -0
  35. package/capabilities/jev.capability.json +226 -0
  36. package/capabilities/lead-conversion-enrollment.capability.json +246 -0
  37. package/capabilities/mail-feed.capability.json +277 -0
  38. package/capabilities/mailgun.capability.json +52 -0
  39. package/capabilities/nango-google.capability.json +639 -0
  40. package/capabilities/notion.capability.json +417 -0
  41. package/capabilities/omniroute.capability.json +137 -0
  42. package/capabilities/posthog-analytics.capability.json +258 -0
  43. package/capabilities/postiz.capability.json +467 -0
  44. package/capabilities/proactive-digest.capability.json +190 -0
  45. package/capabilities/proton.capability.json +288 -0
  46. package/capabilities/replicate-rvm.capability.json +97 -0
  47. package/capabilities/research-methods.capability.json +57 -0
  48. package/capabilities/shopify.capability.json +395 -0
  49. package/capabilities/slack.capability.json +369 -0
  50. package/capabilities/stripe.capability.json +1496 -0
  51. package/capabilities/telegram-bridge.capability.json +56 -0
  52. package/capabilities/treg.capability.json +102 -0
  53. package/capabilities/unipile-linkedin.capability.json +494 -0
  54. package/capabilities/vercel.capability.json +424 -0
  55. package/dist/define-DqoItRiR.d.ts +1370 -0
  56. package/dist/index.d.ts +174 -900
  57. package/dist/index.js +26447 -24075
  58. package/dist/index.js.map +1 -1
  59. package/dist/wire.d.ts +222 -0
  60. package/dist/wire.js +77 -0
  61. package/dist/wire.js.map +1 -0
  62. package/package.json +27 -4
  63. package/retired.yaml +136 -0
  64. package/skills/app-factory-build-slice/SKILL.md +70 -0
  65. package/skills/app-factory-build-slice.catalog.json +17 -0
  66. package/skills/app-factory-design-canvas/SKILL.md +96 -0
  67. package/skills/app-factory-design-canvas.catalog.json +17 -0
  68. package/skills/app-factory-design-review/SKILL.md +105 -0
  69. package/skills/app-factory-design-review.catalog.json +18 -0
  70. package/skills/app-factory-research/SKILL.md +123 -0
  71. package/skills/app-factory-research.catalog.json +17 -0
  72. package/skills/bookmark-enrichment/SKILL.md +40 -0
  73. package/skills/carousel-format/carousel-format.skill.json +18 -0
  74. package/skills/collage-broll/collage-broll.skill.json +20 -0
  75. package/skills/content-taste/content-taste.skill.json +18 -0
  76. package/skills/creative-director/creative-director.skill.json +18 -0
  77. package/skills/design-critique/SKILL.md +138 -0
  78. package/skills/design-critique.catalog.json +12 -0
  79. package/skills/designing-with-real-components/SKILL.md +140 -0
  80. package/skills/designing-with-real-components.catalog.json +12 -0
  81. package/skills/edit-loop/edit-loop.skill.json +25 -0
  82. package/skills/facecam-edit/facecam-edit.skill.json +24 -0
  83. package/skills/mapping-the-server/SKILL.md +54 -0
  84. package/skills/math-explainer-manimgx/math-explainer-manimgx.skill.json +20 -0
  85. package/skills/motion-quality-gates/motion-quality-gates.skill.json +20 -0
  86. package/skills/open-design-edit/open-design-edit.skill.json +24 -0
  87. package/skills/reading-client-progress/SKILL.md +65 -0
  88. package/skills/routing-to-channels/SKILL.md +54 -0
  89. package/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
  90. package/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
  91. package/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
  92. package/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
  93. package/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
  94. package/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
  95. package/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
  96. package/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
  97. package/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
  98. package/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
  99. package/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
  100. package/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
  101. package/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
  102. package/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
  103. package/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
  104. package/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
  105. package/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
  106. package/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
  107. package/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
  108. package/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
  109. package/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
  110. package/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
  111. package/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
  112. package/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
  113. package/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
  114. package/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
  115. package/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
  116. package/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
  117. package/skills/video-direction/video-direction.skill.json +20 -0
  118. package/skills/video-style-vocabulary/video-style-vocabulary.skill.json +20 -0
  119. package/skills/video-toolbox/video-toolbox.skill.json +20 -0
  120. package/src/__fixtures__/lifecycle-presets.flat-before.json +205 -0
  121. package/src/agency-os.yaml +122 -0
  122. package/src/agent-os-skill-slugs.test.ts +65 -0
  123. package/src/apify-social-reader.test.ts +317 -0
  124. package/src/base.yaml +85 -39
  125. package/src/blockchain-ecosystem.yaml +1 -0
  126. package/src/brand-library.yaml +525 -14
  127. package/src/builder-dev-fold.template.test.ts +113 -0
  128. package/src/builder.yaml +779 -91
  129. package/src/business-model-catalog-description.test.ts +47 -0
  130. package/src/business-model-grp.template.test.ts +214 -0
  131. package/src/business-model.yaml +296 -49
  132. package/src/catalog-safety.test.ts +198 -0
  133. package/src/catalog.template-items.test.ts +75 -0
  134. package/src/catalog.ts +66 -2
  135. package/src/concept-guard.test.ts +28 -8
  136. package/src/concept-guard.ts +2 -2
  137. package/src/content-creation.template.test.ts +52 -0
  138. package/src/content-creation.yaml +192 -0
  139. package/src/content-os.edit-loop.template.test.ts +210 -0
  140. package/src/content-os.publish-due-posts.template.test.ts +261 -0
  141. package/src/content-os.template.test.ts +349 -0
  142. package/src/content-os.video-takes.template.test.ts +338 -0
  143. package/src/content-os.work-a-take.template.test.ts +125 -0
  144. package/src/content-os.yaml +1873 -39
  145. package/src/content-studio-retired.template.test.ts +82 -0
  146. package/src/corpus-flow-validity.test.ts +4 -5
  147. package/src/crm-absorbs-outreach.template.test.ts +117 -0
  148. package/src/crm-build-plan.template.test.ts +128 -0
  149. package/src/crm-lists.template.test.ts +96 -0
  150. package/src/crm-messages-cadence.template.test.ts +246 -0
  151. package/src/crm-one-send-door.template.test.ts +65 -0
  152. package/src/crm.yaml +1563 -181
  153. package/src/decisions-wave.template.test.ts +186 -0
  154. package/src/define.ts +146 -17
  155. package/src/design-to-implementation.template.test.ts +94 -0
  156. package/src/domain-template-type.test.ts +75 -0
  157. package/src/ecosystem.yaml +27 -66
  158. package/src/enterprise-os.yaml +10 -134
  159. package/src/facecam-edit.audio-studio.test.ts +188 -0
  160. package/src/fal-generate-image.test.ts +200 -0
  161. package/src/fal-media-lab-pod-files.test.ts +180 -0
  162. package/src/finance.yaml +254 -1
  163. package/src/foundation.yaml +78 -38
  164. package/src/frame-host-modules.d.mts +6 -0
  165. package/src/frame-host-modules.mjs +58 -0
  166. package/src/generate.mjs +9 -3
  167. package/src/github-merge.test.ts +210 -0
  168. package/src/grants.yaml +46 -17
  169. package/src/hr.yaml +39 -6
  170. package/src/index.ts +36 -1
  171. package/src/intent.test.ts +161 -0
  172. package/src/internal-runbook.yaml +19 -1
  173. package/src/jev.test.ts +356 -0
  174. package/src/legal.yaml +33 -1
  175. package/src/life-os.yaml +51 -240
  176. package/src/lifecycle-coverage.test.ts +124 -0
  177. package/src/lifecycle-declarations.test.ts +252 -0
  178. package/src/lifecycle-presets.test.ts +468 -0
  179. package/src/lifecycle-presets.ts +494 -0
  180. package/src/lifecycle-stuck.test.ts +46 -0
  181. package/src/lifecycle-waits.parity.test.ts +319 -0
  182. package/src/marketing.yaml +108 -4
  183. package/src/meetings-knowledge.yaml +18 -0
  184. package/src/networking.yaml +23 -2
  185. package/src/operations.yaml +304 -4
  186. package/src/pipeline-views.template.test.ts +88 -0
  187. package/src/playbook-goal-placeholders.test.ts +57 -0
  188. package/src/postiz-contract.test.ts +134 -0
  189. package/src/presentation-declarations.test.ts +174 -0
  190. package/src/process-contract.validate.test.ts +352 -0
  191. package/src/project-management.yaml +39 -740
  192. package/src/publish-body.d.mts +29 -0
  193. package/src/publish-body.mjs +74 -0
  194. package/src/publish-body.test.ts +74 -0
  195. package/src/publish-official-lib.test.ts +176 -0
  196. package/src/replicate-rvm.test.ts +504 -0
  197. package/src/research-base.yaml +119 -20
  198. package/src/research-decision.yaml +60 -3
  199. package/src/reserved-profile-slugs.test.ts +81 -0
  200. package/src/retired-goal-kinds.test.ts +52 -0
  201. package/src/role-reference-and-step-domain.test.ts +192 -0
  202. package/src/seed-obligation.test.ts +1 -6
  203. package/src/send-message-maps.test.ts +227 -0
  204. package/src/shared-role-ssot.test.ts +40 -2
  205. package/src/ship-a-change.template.test.ts +177 -0
  206. package/src/skills.test.ts +170 -0
  207. package/src/social.yaml +85 -27
  208. package/src/space-packages.freshness.test.ts +97 -0
  209. package/src/space-packages.generated.ts +40 -0
  210. package/src/space-packages.test.ts +125 -0
  211. package/src/space-packages.ts +73 -0
  212. package/src/start.test.ts +188 -0
  213. package/src/stripe-capability.test.ts +192 -0
  214. package/src/task-intents.test.ts +293 -0
  215. package/src/template-documents.test.ts +158 -0
  216. package/src/templates.ts +20907 -21464
  217. package/src/types.ts +475 -61
  218. package/src/unipile-linkedin-find-profile.test.ts +183 -0
  219. package/src/unipile-publish-post.test.ts +162 -0
  220. package/src/validate-flow.drift.test.ts +2 -3
  221. package/src/validate-flow.ts +22 -2
  222. package/src/validate-onboarding.test.ts +59 -0
  223. package/src/validate.test.ts +71 -4
  224. package/src/validate.ts +1416 -7
  225. package/src/wire-core.ts +231 -0
  226. package/src/wire.test.ts +159 -0
  227. package/src/wire.ts +107 -0
  228. package/views/bookmark-views-pack.view.json +39 -0
  229. package/views/event-views-pack.view.json +45 -0
  230. package/views/task-views-pack.view.json +53 -0
  231. package/src/agent-fleet.yaml +0 -1645
  232. package/src/business-developer.template.test.ts +0 -351
  233. package/src/business-developer.yaml +0 -860
  234. package/src/content-studio.yaml +0 -361
  235. package/src/dev-dashboard.yaml +0 -990
  236. package/src/outreach-comms.yaml +0 -486
  237. package/src/the-arch.yaml +0 -201
@@ -0,0 +1,198 @@
1
+ /**
2
+ * W0 catalog safety — checks that SPAN SOURCES (before this file, no guard
3
+ * spanned sources). Read-only over sibling repos; a sibling that is absent (this
4
+ * package checked out alone, e.g. in synap-app CI) SKIPS its half honestly,
5
+ * with the reason logged, instead of passing vacuously.
6
+ *
7
+ * (a) ONE SOURCE PER PLAYBOOK NAME. The pod reuses playbooks by name (one live
8
+ * row per lower(name) per workspace, migration 0227), so the same name
9
+ * declared by two sources — a workspace YAML and a CP capability, or two
10
+ * capabilities — contend for ONE row, and whichever applies first silently
11
+ * wins. Every name may be declared by exactly one source file; the known
12
+ * exceptions are listed with their reason and must match EXACTLY, so a new
13
+ * duplicate AND a resolved one both turn this red.
14
+ *
15
+ * (b) EVERY SKILL REFERENCE RESOLVES. `dependencies[]` of kind `skill`, every
16
+ * `skills:` link, and every `system/<pkg>/<stem>` named in template or
17
+ * capability prose must name a skill package in this corpus
18
+ * (`skills/*.skill.json` or `skills/<slug>/*.skill.json`) or a system skill on disk under
19
+ * `synap-backend/skills/<pkg>/<stem>.md` (the pod's `ensureSystemSkills`
20
+ * derives `system/<pkg>/<file-stem>` from exactly that layout).
21
+ * Not covered: whether the corpus skill package is PUBLISHED.
22
+ *
23
+ * (c) The catalog's content directories ship in the npm bundle (`files` +
24
+ * the `exports` subpaths, in both `exports` and `publishConfig.exports`),
25
+ * and the build reads none of them (it compiles `src/*.yaml` only).
26
+ *
27
+ * The capability half reads the CATALOG's `capabilities/*.capability.json`
28
+ * (manifests; the CapabilityDefinition is `spec`) — the catalog is the source;
29
+ * the CP's `src/seeds/capability-templates/` is a transition mirror held
30
+ * identical by the CP's `catalog-mirror.tripwire.test.ts`.
31
+ */
32
+
33
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
34
+ import { dirname, join } from "node:path";
35
+ import { fileURLToPath } from "node:url";
36
+ import { describe, expect, it } from "vitest";
37
+ import { readYamlTemplates } from "./generate.mjs";
38
+ import type { WorkspaceYaml } from "./types.js";
39
+ // One duplicate detector, shared with validateCatalog (duplicate-playbook-name).
40
+ import { duplicateNames } from "../scripts/catalog-lib.mjs";
41
+
42
+ const HERE = dirname(fileURLToPath(import.meta.url));
43
+ const PKG = join(HERE, "..");
44
+ const MONO = join(HERE, "../../../..");
45
+ const CATALOG_CAPS = join(PKG, "capabilities");
46
+ const BACKEND_SKILLS = join(MONO, "synap-backend/skills");
47
+ const CORPUS_SKILLS = join(PKG, "skills");
48
+
49
+ const yamls = readYamlTemplates() as Array<{ file: string; slug: string; data: WorkspaceYaml }>;
50
+ const hasBackend = existsSync(BACKEND_SKILLS);
51
+ if (!hasBackend) console.warn(`[catalog-safety] SKIP the system-skill half: ${BACKEND_SKILLS} is absent`);
52
+
53
+ const capFiles = readdirSync(CATALOG_CAPS)
54
+ .filter((f) => f.endsWith(".capability.json"))
55
+ .map((f) => ({ file: f, text: readFileSync(join(CATALOG_CAPS, f), "utf8") }));
56
+
57
+ // ── (a) one source per playbook name ────────────────────────────────────────
58
+
59
+ type Declared = { name: string; source: string };
60
+
61
+ const declared: Declared[] = [
62
+ ...yamls.flatMap((t) =>
63
+ (t.data.playbooks ?? []).map((p) => ({ name: p.name, source: `yaml:${t.file}` }))
64
+ ),
65
+ ...capFiles.flatMap((c) =>
66
+ ((JSON.parse(c.text).spec?.playbooks ?? []) as Array<{ name: string }>).map((p) => ({
67
+ name: p.name,
68
+ source: `cap:${c.file}`,
69
+ }))
70
+ ),
71
+ ];
72
+
73
+ /**
74
+ * Known duplicates, each with its reason. Both are CP-internal (out of this
75
+ * package's lane); giving each one home is wave W4.
76
+ */
77
+ const KNOWN_DUPLICATES: Record<string, string[]> = {
78
+ "client onboarding": ["cap:agency-skills.capability.json", "cap:discord-bot.capability.json"],
79
+ "lead outreach": ["cap:agency-skills.capability.json", "cap:discord-bot.capability.json"],
80
+ };
81
+
82
+ describe("(a) one source per playbook name", () => {
83
+ it("non-vacuity: the scan sees the YAML playbooks (and the capability ones when present)", () => {
84
+ expect(declared.filter((d) => d.source.startsWith("yaml:")).length).toBeGreaterThanOrEqual(15);
85
+ expect(capFiles.length).toBeGreaterThanOrEqual(30);
86
+ expect(declared.filter((d) => d.source.startsWith("cap:")).length).toBeGreaterThanOrEqual(5);
87
+ });
88
+
89
+ it("self-check: a name declared by two sources, in any case, is reported", () => {
90
+ expect(
91
+ duplicateNames([
92
+ { name: "Produce Content", source: "yaml:a.yaml" },
93
+ { name: "produce content ", source: "cap:b.json" },
94
+ { name: "Solo", source: "yaml:a.yaml" },
95
+ ])
96
+ ).toEqual({ "produce content": ["cap:b.json", "yaml:a.yaml"] });
97
+ });
98
+
99
+ it("no playbook name has two sources beyond the listed exceptions (exact)", () => {
100
+ expect(duplicateNames(declared)).toEqual(KNOWN_DUPLICATES);
101
+ });
102
+ });
103
+
104
+ // ── (b) every skill reference resolves ──────────────────────────────────────
105
+
106
+ /** `skills/*.skill.json` and `skills/<slug>/*.skill.json` (one level, as the catalog lib reads them). */
107
+ const corpusSkillFiles = readdirSync(CORPUS_SKILLS, { withFileTypes: true }).flatMap((d) =>
108
+ d.isDirectory()
109
+ ? readdirSync(join(CORPUS_SKILLS, d.name))
110
+ .filter((f) => f.endsWith(".skill.json"))
111
+ .map((f) => join(CORPUS_SKILLS, d.name, f))
112
+ : d.name.endsWith(".skill.json")
113
+ ? [join(CORPUS_SKILLS, d.name)]
114
+ : []
115
+ );
116
+ const corpusSkillSlugs = new Set(corpusSkillFiles.map((f) => JSON.parse(readFileSync(f, "utf8")).meta.slug as string));
117
+
118
+ /** `system/<pkg>/<stem>` → is there a `synap-backend/skills/<pkg>/<stem>.md` (stem case-insensitive)? */
119
+ function systemSkillExists(ref: string): boolean {
120
+ const [, pkg, stem] = ref.split("/");
121
+ const dir = join(BACKEND_SKILLS, pkg ?? "");
122
+ if (!pkg || !stem || !existsSync(dir)) return false;
123
+ return readdirSync(dir).some((f) => f.toLowerCase() === `${stem.toLowerCase()}.md`);
124
+ }
125
+
126
+ const SYSTEM_REF = /(?<![A-Za-z0-9_/-])system\/[a-z0-9_-]+\/[a-z0-9_-]+/g;
127
+
128
+ const structuredRefs = yamls.flatMap((t) => [
129
+ ...(t.data.dependencies ?? [])
130
+ .filter((d) => d.kind === "skill")
131
+ .map((d) => ({ where: `${t.file} dependencies`, slug: d.slug })),
132
+ ...(t.data.skills ?? []).map((s) => ({ where: `${t.file} skills:`, slug: s.slug })),
133
+ ]);
134
+
135
+ const proseRefs = [
136
+ ...yamls.flatMap((t) =>
137
+ [...readFileSync(join(HERE, t.file), "utf8").matchAll(SYSTEM_REF)].map((m) => ({
138
+ where: t.file,
139
+ slug: m[0],
140
+ }))
141
+ ),
142
+ ...capFiles.flatMap((c) =>
143
+ [...c.text.matchAll(SYSTEM_REF)].map((m) => ({ where: c.file, slug: m[0] }))
144
+ ),
145
+ ];
146
+
147
+ describe("(b) every skill reference resolves", () => {
148
+ it("non-vacuity: structured refs exist, and the resolvers see a literal sample", () => {
149
+ expect(structuredRefs.length).toBeGreaterThanOrEqual(2);
150
+ expect(corpusSkillSlugs.has("creative-director")).toBe(true);
151
+ // The prose matcher catches a system ref and ignores a path that merely ends in one.
152
+ expect("load system/synap/concepts now".match(SYSTEM_REF)).toEqual(["system/synap/concepts"]);
153
+ expect("ecosystem/operations/crm".match(SYSTEM_REF)).toBeNull();
154
+ });
155
+
156
+ it("every skill dependency and skills: link names a corpus or system skill", () => {
157
+ const dangling = structuredRefs.filter((r) =>
158
+ r.slug.startsWith("system/")
159
+ ? hasBackend && !systemSkillExists(r.slug)
160
+ : !corpusSkillSlugs.has(r.slug)
161
+ );
162
+ expect(dangling.map((r) => `${r.where}: ${r.slug}`)).toEqual([]);
163
+ });
164
+
165
+ it.skipIf(!hasBackend)("every system/<pkg>/<stem> named in prose exists under synap-backend/skills", () => {
166
+ expect(systemSkillExists("system/synap/concepts")).toBe(true); // literal sample
167
+ expect(systemSkillExists("system/synap/creative-director")).toBe(false); // the stale ref
168
+ const dangling = proseRefs.filter((r) => !systemSkillExists(r.slug));
169
+ expect(dangling.map((r) => `${r.where}: ${r.slug}`)).toEqual([]);
170
+ });
171
+ });
172
+
173
+ // ── (c) the content directories ship, the build reads none of them ─────────
174
+
175
+ const CONTENT_DIRS = ["capabilities", "skills", "automations", "views"];
176
+
177
+ describe("(c) catalog content ships in the npm bundle; the build reads only src/*.yaml", () => {
178
+ const pkg = JSON.parse(readFileSync(join(PKG, "package.json"), "utf8"));
179
+
180
+ it("non-vacuity: every content directory exists and holds files", () => {
181
+ for (const d of CONTENT_DIRS) expect(readdirSync(join(PKG, d)).length, d).toBeGreaterThan(0);
182
+ });
183
+
184
+ it("`files` carries every content directory, and both export maps expose it", () => {
185
+ for (const d of CONTENT_DIRS) {
186
+ expect(pkg.files, d).toContain(d);
187
+ expect(pkg.exports[`./${d}/*`], d).toBe(`./${d}/*`);
188
+ expect(pkg.publishConfig.exports[`./${d}/*`], d).toBe(`./${d}/*`);
189
+ }
190
+ expect(pkg.files).toEqual(expect.arrayContaining(["dist", "src"]));
191
+ });
192
+
193
+ it("build.mjs compiles src/*.yaml only", () => {
194
+ expect(yamls.every((t) => t.file.endsWith(".yaml") && !t.file.includes("/"))).toBe(true);
195
+ const build = readFileSync(join(PKG, "build.mjs"), "utf8");
196
+ for (const d of CONTENT_DIRS) expect(build, d).not.toMatch(new RegExp(`["'\`/]${d}/`));
197
+ });
198
+ });
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Bundled catalog entries carry DERIVED playbook sub-items — `track-template`
3
+ * for `scope: project`, `work-template` otherwise — the same rule the control
4
+ * plane computes in SQL for remote rows (`packageItemsSql`). A bundle usually
5
+ * WINS the merge for an official template, so without this the marketplace's
6
+ * "Track templates" lens could never see the templates this repo ships.
7
+ *
8
+ * Driven from the REAL bundled corpus through the REAL `mergeCatalog` door, not
9
+ * a hand-built entry.
10
+ */
11
+ import { test } from "vitest";
12
+ import assert from "node:assert/strict";
13
+ import { mergeCatalog, TRACK_TEMPLATE_ITEM_KIND, WORK_TEMPLATE_ITEM_KIND } from "./catalog.js";
14
+ import { listWorkspaceTemplates } from "./index.js";
15
+
16
+ test("a bundled template with a project-scope playbook reads as shipping a track template", () => {
17
+ const bundled = listWorkspaceTemplates();
18
+ const withMethod = bundled.filter((t) =>
19
+ (t.playbooks ?? []).some((p) => p.scope === "project")
20
+ );
21
+ // Non-vacuity: the corpus really ships at least one track template.
22
+ assert.ok(withMethod.length >= 1, "no bundled template ships a scope:project playbook");
23
+
24
+ const { entries } = mergeCatalog({ bundled, remote: [], bundleVersion: "0.0.0", remoteStatus: "ok" });
25
+ for (const tpl of withMethod) {
26
+ const entry = entries.find((e) => e.slug === tpl.meta.slug);
27
+ assert.ok(entry, `bundled ${tpl.meta.slug} missing from the merge`);
28
+ const tracks = entry.items.filter((i) => i.kind === TRACK_TEMPLATE_ITEM_KIND);
29
+ const expected = (tpl.playbooks ?? []).filter((p) => p.scope === "project").map((p) => p.name);
30
+ assert.deepEqual(tracks.map((i) => i.name), expected);
31
+ }
32
+ });
33
+
34
+ test("a session / unscoped playbook is a work template, never a track template", () => {
35
+ const tpl = listWorkspaceTemplates()[0];
36
+ const synthetic = {
37
+ ...tpl,
38
+ meta: { ...tpl.meta, slug: "zz-synthetic" },
39
+ playbooks: [
40
+ { name: "Digest", goalTemplate: "g", scope: "session" as const },
41
+ { name: "Enrich", goalTemplate: "g" },
42
+ ],
43
+ };
44
+ const { entries } = mergeCatalog({ bundled: [synthetic], remote: [], bundleVersion: "0.0.0", remoteStatus: "ok" });
45
+ assert.deepEqual(
46
+ entries[0].items
47
+ .filter((i) => i.kind === WORK_TEMPLATE_ITEM_KIND || i.kind === TRACK_TEMPLATE_ITEM_KIND)
48
+ .map((i) => i.kind),
49
+ [WORK_TEMPLATE_ITEM_KIND, WORK_TEMPLATE_ITEM_KIND]
50
+ );
51
+ });
52
+
53
+ // G1 (2026-09-27): what a package brings BESIDE its templates — kinds, roles,
54
+ // views — rides the same derived items, so a card can say "+ 4 kinds, 4 views"
55
+ // without the definition. Same rule as the CP `packageItemsSql`.
56
+ test("a bundled template's profiles and views derive kind / role / view items", () => {
57
+ const bundled = listWorkspaceTemplates();
58
+ const { entries } = mergeCatalog({ bundled, remote: [], bundleVersion: "0.0.0", remoteStatus: "ok" });
59
+ const grp = bundled.find((t) => t.meta.slug === "business-model");
60
+ assert.ok(grp, "business-model is no longer bundled");
61
+ const entry = entries.find((e) => e.slug === "business-model")!;
62
+ const count = (k: string) => entry.items.filter((i) => i.kind === k).length;
63
+ const kinds = grp.profiles.filter((p) => p.profileKind !== "role").length;
64
+ const roles = grp.profiles.filter((p) => p.profileKind === "role").length;
65
+ assert.ok(kinds > 0 && (grp.views ?? []).length > 0, "non-vacuity: GRP ships kinds and views");
66
+ assert.equal(count("kind"), kinds);
67
+ assert.equal(count("role"), roles);
68
+ assert.equal(count("view"), (grp.views ?? []).length);
69
+ // A role-profile reads as a role, never a kind.
70
+ const withRole = bundled.find((t) => t.profiles.some((p) => p.profileKind === "role"));
71
+ assert.ok(withRole, "non-vacuity: some bundled template declares a role");
72
+ const roleEntry = entries.find((e) => e.slug === withRole.meta.slug)!;
73
+ const roleSlug = withRole.profiles.find((p) => p.profileKind === "role")!.slug;
74
+ assert.equal(roleEntry.items.find((i) => i.key === roleSlug)?.kind, "role");
75
+ });
package/src/catalog.ts CHANGED
@@ -47,6 +47,68 @@ export interface PackageItemSummary {
47
47
  name: string;
48
48
  }
49
49
 
50
+ /**
51
+ * The DERIVED template kinds a playbook sub-item carries. A playbook's own
52
+ * `scope` decides it — `"project"` is a method a project runs as a track, any
53
+ * other value (the schema default `session`) is one sitting of work. The author
54
+ * never declares it. The control plane derives the SAME rule in SQL for remote
55
+ * rows (`packageItemsSql`, synap-control-plane-api `routes/packages.ts`); the
56
+ * user-facing noun is `resolveTemplateNoun(scope)` (`@synap-core/types/vocabulary`).
57
+ */
58
+ export const TRACK_TEMPLATE_ITEM_KIND = "track-template";
59
+ export const WORK_TEMPLATE_ITEM_KIND = "work-template";
60
+
61
+ /** Playbooks → `{kind,key,name}` sub-items. `key` = name (playbooks carry no key). */
62
+ export function derivePlaybookItems(
63
+ playbooks:
64
+ | ReadonlyArray<{ name?: unknown; scope?: unknown } | null | undefined>
65
+ | null
66
+ | undefined
67
+ ): PackageItemSummary[] {
68
+ const out: PackageItemSummary[] = [];
69
+ for (const pb of playbooks ?? []) {
70
+ if (!pb || typeof pb.name !== "string" || !pb.name) continue;
71
+ out.push({
72
+ kind: pb.scope === "project" ? TRACK_TEMPLATE_ITEM_KIND : WORK_TEMPLATE_ITEM_KIND,
73
+ key: pb.name,
74
+ name: pb.name,
75
+ });
76
+ }
77
+ return out;
78
+ }
79
+
80
+ /**
81
+ * Schema sub-items: what a package brings BESIDE its templates — each profile as
82
+ * `kind` (or `role` when `profileKind: role`), each view as `view`. The CP derives
83
+ * the SAME rule in SQL (`packageItemsSql`). A card reads them only to say, in a
84
+ * secondary line, what else comes with the package ("+ 4 kinds, 4 views").
85
+ */
86
+ export function deriveSchemaItems(tpl: {
87
+ profiles?: ReadonlyArray<{ slug?: unknown; displayName?: unknown; profileKind?: unknown } | null | undefined> | null;
88
+ views?: ReadonlyArray<{ name?: unknown; displayName?: unknown; slug?: unknown } | null | undefined> | null;
89
+ }): PackageItemSummary[] {
90
+ const out: PackageItemSummary[] = [];
91
+ for (const p of tpl.profiles ?? []) {
92
+ if (!p || typeof p.slug !== "string" || !p.slug) continue;
93
+ out.push({
94
+ kind: p.profileKind === "role" ? "role" : "kind",
95
+ key: p.slug,
96
+ name: typeof p.displayName === "string" && p.displayName ? p.displayName : p.slug,
97
+ });
98
+ }
99
+ for (const v of tpl.views ?? []) {
100
+ const name =
101
+ v && typeof v.name === "string" && v.name
102
+ ? v.name
103
+ : v && typeof v.displayName === "string" && v.displayName
104
+ ? v.displayName
105
+ : null;
106
+ if (!v || !name) continue;
107
+ out.push({ kind: "view", key: typeof v.slug === "string" && v.slug ? v.slug : name, name });
108
+ }
109
+ return out;
110
+ }
111
+
50
112
  /**
51
113
  * A bundled template — the canonical `WorkspaceYaml` this package ships. Callers
52
114
  * pass `listPublicTemplates()` (18) or `listWorkspaceTemplates()` (21); the door
@@ -307,8 +369,10 @@ function bundledToEntry(tpl: WorkspaceYaml): BundledCatalogEntry {
307
369
  isPrivate: tpl.meta.isPublic === false,
308
370
  // Bundled templates ship in-client and are never tier-gated.
309
371
  requiredTier: null,
310
- // Bundled workspace templates carry no cells — be honest, not `undefined`.
311
- items: [],
372
+ // Bundled workspace templates carry no cells; their playbooks are summarized
373
+ // with the same derived kind the CP computes for a remote row, so a bundle
374
+ // that wins the merge still reads as shipping a track template.
375
+ items: [...derivePlaybookItems(tpl.playbooks), ...deriveSchemaItems(tpl)],
312
376
  template: tpl,
313
377
  };
314
378
  }
@@ -45,18 +45,19 @@ describe("concept guard — shipped corpus", () => {
45
45
 
46
46
  it("needs NO declared divergence — the CRM fork was resolved, not allowlisted", () => {
47
47
  // The allowlist was born carrying `crm-lead|lead` + `client|crm-client`.
48
- // business-developer.yaml now re-declares the SHARED `lead`/`client` roles
49
- // and carries its business-development fields as workspace-overlay
50
- // properties, so there is no fork left to declare. Rule 3 (stale-allowlist)
48
+ // crm.yaml re-declares the SHARED `lead`/`client` roles and carries the
49
+ // qualification/handoff fields (folded in from business-developer,
50
+ // 2026-10-12) as workspace-overlay properties, so there is no fork left to
51
+ // declare. Rule 3 (stale-allowlist)
51
52
  // is what forces this to stay true: a leftover entry fails CI.
52
53
  expect(Object.keys(DECLARED_DIVERGENCES)).toEqual([]);
53
54
 
54
- const bd = WORKSPACE_TEMPLATES["business-developer"]?.profiles ?? [];
55
- expect(bd.map((p) => p.slug)).not.toContain("crm-lead");
56
- expect(bd.map((p) => p.slug)).not.toContain("crm-client");
55
+ const crm = WORKSPACE_TEMPLATES["crm"]?.profiles ?? [];
56
+ expect(crm.map((p) => p.slug)).not.toContain("crm-lead");
57
+ expect(crm.map((p) => p.slug)).not.toContain("crm-client");
57
58
  for (const slug of ["lead", "client"]) {
58
- const reused = bd.find((p) => p.slug === slug);
59
- expect(reused, `business-developer must reuse shared '${slug}'`).toBeDefined();
59
+ const reused = crm.find((p) => p.slug === slug);
60
+ expect(reused, `crm must reuse shared '${slug}'`).toBeDefined();
60
61
  expect(reused?.scope).toBe("shared");
61
62
  expect(reused?.profileKind).toBe("role");
62
63
  // No alias: the shared slug IS the concept identity.
@@ -190,3 +191,22 @@ describe("concept guard — stays green on legitimate patterns", () => {
190
191
  expect(violations).toEqual([]);
191
192
  });
192
193
  });
194
+
195
+ // "Pipeline" is a VIEW, never a stored kind (work model L3, 2026-10-11): the
196
+ // pipeline is computed from a kind or role + its lifecycle. A stored kind named
197
+ // pipeline (content-studio's old `content-pipeline`) makes "open the pipeline"
198
+ // ambiguous. Scans every profile slug and semanticSlug in the shipped corpus.
199
+ // Does NOT scan property slugs (builder's agent-run `pipeline_phase` is a run
200
+ // phase, a different word) nor view names ("Lead Pipeline" IS the view).
201
+ describe("no stored kind is named pipeline", () => {
202
+ it("the corpus declares no profile whose slug or semanticSlug says pipeline", () => {
203
+ const all = Object.values(WORKSPACE_TEMPLATES).flatMap((t) =>
204
+ (t.profiles ?? []).map((p) => ({ tpl: t.meta?.slug, p }))
205
+ );
206
+ expect(all.length).toBeGreaterThan(100); // non-vacuity
207
+ const hits = all
208
+ .filter(({ p }) => /pipeline/i.test(p.slug) || /pipeline/i.test(p.semanticSlug ?? ""))
209
+ .map(({ tpl, p }) => `${tpl}:${p.slug}`);
210
+ expect(hits).toEqual([]);
211
+ });
212
+ });
@@ -51,8 +51,8 @@ export interface ConceptViolation {
51
51
  *
52
52
  * Currently EMPTY, and that is the healthy state. The two entries this list was
53
53
  * born for — `crm-lead|lead` and `client|crm-client` — were RESOLVED rather
54
- * than kept: business-developer.yaml now re-declares the shared `lead`/`client`
55
- * roles and carries its business-development fields as workspace-overlay
54
+ * than kept: crm.yaml (business-developer folded into it, 2026-10-12)
55
+ * re-declares the shared `lead`/`client` roles and carries its business-development fields as workspace-overlay
56
56
  * PROPERTIES. One concept, one slug, workspace-specific fields. Rule 3 below
57
57
  * would have failed CI had the entries been left behind.
58
58
  */
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Contract tests for the Content Creation suite.
3
+ *
4
+ * Reads the generated `templates.ts` corpus (same as every installer).
5
+ */
6
+ import { describe, expect, it } from "vitest";
7
+ import { getWorkspaceTemplate } from "./templates.js";
8
+
9
+ const template = (() => {
10
+ const resolved = getWorkspaceTemplate("content-creation");
11
+ if (!resolved) throw new Error("content-creation template is not registered");
12
+ return resolved;
13
+ })();
14
+
15
+ describe("Content Creation suite", () => {
16
+ it("is a public suite tagged for content creation", () => {
17
+ expect(template.meta.slug).toBe("content-creation");
18
+ expect(template.meta.isPublic).toBe(true);
19
+ expect(template.meta.domain).toBe("content");
20
+ expect(template.meta.tags).toEqual(
21
+ expect.arrayContaining(["suite", "content", "creation"])
22
+ );
23
+ });
24
+
25
+ it("requires content-os and brand-library because content-os consumes brand; never the retired content-studio", () => {
26
+ const required = (template.dependencies ?? []).map((d) => ({
27
+ slug: d.slug,
28
+ relation: d.relation ?? "require",
29
+ }));
30
+ expect(required).toEqual(
31
+ expect.arrayContaining([
32
+ { slug: "content-os", relation: "require" },
33
+ { slug: "brand-library", relation: "require" },
34
+ ])
35
+ );
36
+ // content-studio is retired into content-os (retired.yaml, 2026-11-03).
37
+ expect(required.map((d) => d.slug)).not.toContain("content-studio");
38
+ expect(required.every((d) => d.relation === "require")).toBe(true);
39
+ // Foundation is NOT a direct dep: content-os already requires it, and
40
+ // neither constituent's sourceRoles consume `strategy`.
41
+ expect(required.map((d) => d.slug)).not.toContain("foundation");
42
+ });
43
+
44
+ it("does not silently skip the brand spine its constituents consume", () => {
45
+ expect(template.workspace.sourceRoles?.brand).toBe("consumer");
46
+ const contentOs = getWorkspaceTemplate("content-os");
47
+ expect(contentOs?.workspace.sourceRoles?.brand).toBe("consumer");
48
+ expect((template.dependencies ?? []).some((d) => d.slug === "brand-library")).toBe(
49
+ true
50
+ );
51
+ });
52
+ });
@@ -0,0 +1,192 @@
1
+ # Content Creation — the public editorial + production suite
2
+ # ==========================================================
3
+ # A SUITE template: it stands up a content operation in one install by REQUIRING
4
+ # its constituent workspaces (deps-first), then adds a thin "program tower" home
5
+ # of its own — a place to track the editorial programs that cut across the
6
+ # lenses it just installed.
7
+ #
8
+ # What it composes (all public, all bundled):
9
+ # • brand-library — brand identity spine (both constituents consume `brand`)
10
+ # • content-os — the editorial engine (pillars, posts, platforms, newsletters)
11
+ # AND the video / visual pipeline (compositions, assets,
12
+ # renders); transitively requires foundation + marketing-campaign
13
+ #
14
+ # content-studio was retired into content-os (retired.yaml, 2026-11-03): one
15
+ # space per domain, and content-os.yaml said never to install it beside
16
+ # content-os. This suite no longer requires it.
17
+ #
18
+ # Brand is required because content-os declares `sourceRoles.brand: consumer`
19
+ # — skipping it would silently drop the spine it consumes. Foundation is NOT a direct dep: content-os already
20
+ # requires it (and marketing-campaign) via its own `dependencies[]`; sourceRoles
21
+ # do not consume `strategy`.
22
+ #
23
+ # It uses `require` (not `compose`): each constituent is a REAL workspace, not an
24
+ # overlay. Installing Content Creation a second time is idempotent — the resolver
25
+ # reuses existing constituent workspaces by slug.
26
+
27
+ meta:
28
+ slug: content-creation
29
+ domain: content
30
+ name: Content Creation
31
+ description: Stand up a content operation in one install — brand, editorial engine, and video pipeline — plus a program tower to track what you are making across them.
32
+ icon: pen-tool
33
+ color: "#6366F1"
34
+ tags: [suite, content, creation, editorial, video, studio]
35
+ isPublic: true
36
+
37
+ # Deps-first: brand-library is the brand-vocabulary provider content-os
38
+ # consumes, so it must resolve before content-os reuses it. `require` (not
39
+ # `compose`): each is a standalone workspace, not an additive overlay.
40
+ dependencies:
41
+ - slug: brand-library
42
+ kind: workspace
43
+ relation: require
44
+ reason: "Brand identity spine — Content OS consumes `brand`; skipping it would drop the provider that sourceRole requires."
45
+ - slug: content-os
46
+ kind: workspace
47
+ relation: require
48
+ reason: "The editorial engine and the video / visual pipeline — pillars, posts, platforms, newsletters, compositions and renders. Transitively brings foundation (strategy spine) and marketing-campaign."
49
+
50
+ workspace:
51
+ name: Content Creation
52
+ description: The program tower for the content operation — track the editorial programs that cut across the brand, editorial, and production lenses this suite installs.
53
+ subtype: content-creation
54
+ visibility: pod_visible
55
+ sourceRoles:
56
+ brand: consumer
57
+ content: provider
58
+
59
+ profiles:
60
+ # The one native profile the program tower owns: a content program the
61
+ # operation is driving across the lenses this suite installs. Deliberately
62
+ # light — the depth lives in the constituent workspaces; this is the
63
+ # cross-cutting scoreboard that ties them together.
64
+ - slug: content-program
65
+ displayName: Content Program
66
+ icon: target
67
+ color: "#6366F1"
68
+ description: A content program the operation is driving — the cross-cutting outcome the editorial and production lenses serve.
69
+ # The ONE lifecycle (work model L1): every option's category.
70
+ lifecycle:
71
+ property: status
72
+ options:
73
+ - { key: on-track, category: active }
74
+ - { key: at-risk, category: active }
75
+ - { key: off-track, category: active }
76
+ - { key: shipped, category: done }
77
+ properties:
78
+ - slug: outcome
79
+ label: Outcome
80
+ valueType: string
81
+ inputType: textarea
82
+ - slug: status
83
+ label: Status
84
+ valueType: string
85
+ inputType: select
86
+ enumValues: [on-track, at-risk, off-track, shipped]
87
+ constraints: { defaultValue: on-track }
88
+ - slug: horizon
89
+ label: Horizon
90
+ valueType: string
91
+ inputType: select
92
+ enumValues: [now, next, later]
93
+ constraints: { defaultValue: now }
94
+ - slug: owner
95
+ label: Owner
96
+ valueType: string
97
+ inputType: text
98
+ - slug: targetDate
99
+ label: Target Date
100
+ valueType: date
101
+ inputType: date
102
+
103
+ views:
104
+ - name: Programs
105
+ slug: programs
106
+ type: table
107
+ scopeProfileSlug: content-program
108
+ description: The content programs across editorial and production, by horizon and status.
109
+ defaultView: true
110
+ config:
111
+ columns: [title, status, horizon, owner, targetDate]
112
+ - name: Program Board
113
+ slug: program-board
114
+ type: kanban
115
+ scopeProfileSlug: content-program
116
+ description: Programs grouped by status — a running scoreboard for the content operation.
117
+ config:
118
+ groupBy: status
119
+
120
+ onboarding:
121
+ goal: >
122
+ Stand up the content operation and name the handful of programs it is
123
+ driving — so brand, editorial, and production are all in place and pointed
124
+ at the same outcomes from day one.
125
+ framing: |
126
+ THE EDITORIAL PRODUCER: Act as a sharp producer standing up the content OS.
127
+ Brand and Content OS are already installed — your job is
128
+ to name the 3-5 programs that cut across them, so the operation has a
129
+ scoreboard, not just structure.
130
+ expertise:
131
+ starters:
132
+ - >
133
+ Name few, real programs. A content operation with 20 programs has
134
+ none; force the 3-5 that actually decide this quarter, each with an
135
+ owner and a horizon (now / next / later).
136
+ - >
137
+ Tie each program to a lens. A good program names where it lives —
138
+ a series (editorial), a production pipeline (video and visual
139
+ posts), or a brand system
140
+ (library) — so the scoreboard maps to the structure underneath it.
141
+ blindSpots:
142
+ - >
143
+ Programs with no owner and no date are wishes. Every one needs a
144
+ single owner and a target date, or it will not move.
145
+ - >
146
+ "Make more content" is not a program — it is a category. Push for
147
+ the specific outcome that would make this quarter a win.
148
+ bar: >
149
+ A short, honest scoreboard: 3-5 programs, each owned, dated, placed on a
150
+ horizon, and tied to the lens that serves it — sitting on top of a fully
151
+ installed content operation.
152
+ collect:
153
+ - profileSlug: content-program
154
+ what: The 3-5 content programs the operation is driving this horizon.
155
+ cardinality: few
156
+ keyFields: [status, horizon, owner]
157
+ openingQuestions:
158
+ - "What are the 3-5 content programs that will decide this quarter?"
159
+ - "Who owns each one, and is it a now, next, or later horizon?"
160
+ doneWhen: A few programs captured, each with an owner and a horizon.
161
+
162
+ app:
163
+ kind: stored
164
+ cellKey: entity-detail
165
+
166
+ layout:
167
+ defaultView: Programs
168
+ defaultApp: null
169
+ pinnedApps: [dashboard, data]
170
+ sidebarItems:
171
+ - kind: view
172
+ viewName: Programs
173
+ label: Programs
174
+ icon: target
175
+ - kind: view
176
+ viewName: Program Board
177
+ label: Board
178
+ icon: kanban-square
179
+ - kind: app
180
+ appId: data
181
+ label: Data
182
+ icon: database
183
+
184
+ bento:
185
+ viewName: Programs
186
+ blocks:
187
+ - kind: view
188
+ viewName: Programs
189
+ pos: { x: 0, y: 0, w: 8, h: 4 }
190
+ - kind: view
191
+ viewName: Program Board
192
+ pos: { x: 8, y: 0, w: 4, h: 4 }