@synap-core/workspace-templates 0.11.0 → 0.23.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 (249) hide show
  1. package/README.md +483 -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 +420 -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 +471 -0
  54. package/capabilities/vercel.capability.json +424 -0
  55. package/dist/define-CPS0n8BV.d.ts +1331 -0
  56. package/dist/index.d.ts +189 -900
  57. package/dist/index.js +24654 -21639
  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 +41 -27
  63. package/packs/bookmark-views-pack.json +33 -0
  64. package/packs/crm-pack.json +76 -0
  65. package/packs/customer-profile.json +28 -0
  66. package/packs/deep-research.playbook.json +30 -0
  67. package/packs/event-views-pack.json +33 -0
  68. package/packs/habit-profile.json +27 -0
  69. package/packs/hr-pack.json +54 -0
  70. package/packs/invoice-profile.json +28 -0
  71. package/packs/knowledge-base-pack.json +56 -0
  72. package/packs/meeting-profile.json +29 -0
  73. package/packs/product-profile.json +30 -0
  74. package/packs/project-management-pack.json +49 -0
  75. package/packs/recipe-profile.json +29 -0
  76. package/packs/task-views-pack.json +39 -0
  77. package/retired.yaml +136 -0
  78. package/skills/app-factory-build-slice/SKILL.md +70 -0
  79. package/skills/app-factory-build-slice.catalog.json +17 -0
  80. package/skills/app-factory-design-canvas/SKILL.md +96 -0
  81. package/skills/app-factory-design-canvas.catalog.json +17 -0
  82. package/skills/app-factory-design-review/SKILL.md +99 -0
  83. package/skills/app-factory-design-review.catalog.json +18 -0
  84. package/skills/app-factory-research/SKILL.md +123 -0
  85. package/skills/app-factory-research.catalog.json +17 -0
  86. package/skills/bookmark-enrichment/SKILL.md +40 -0
  87. package/skills/carousel-format/carousel-format.skill.json +18 -0
  88. package/skills/collage-broll/collage-broll.skill.json +20 -0
  89. package/skills/content-taste/content-taste.skill.json +18 -0
  90. package/skills/creative-director/creative-director.skill.json +18 -0
  91. package/skills/design-critique/SKILL.md +135 -0
  92. package/skills/design-critique.catalog.json +12 -0
  93. package/skills/designing-with-real-components/SKILL.md +140 -0
  94. package/skills/designing-with-real-components.catalog.json +12 -0
  95. package/skills/edit-loop/edit-loop.skill.json +25 -0
  96. package/skills/facecam-edit/facecam-edit.skill.json +24 -0
  97. package/skills/mapping-the-server/SKILL.md +54 -0
  98. package/skills/math-explainer-manimgx/math-explainer-manimgx.skill.json +20 -0
  99. package/skills/motion-quality-gates/motion-quality-gates.skill.json +20 -0
  100. package/skills/open-design-edit/open-design-edit.skill.json +24 -0
  101. package/skills/reading-client-progress/SKILL.md +65 -0
  102. package/skills/routing-to-channels/SKILL.md +54 -0
  103. package/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
  104. package/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
  105. package/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
  106. package/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
  107. package/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
  108. package/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
  109. package/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
  110. package/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
  111. package/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
  112. package/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
  113. package/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
  114. package/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
  115. package/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
  116. package/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
  117. package/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
  118. package/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
  119. package/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
  120. package/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
  121. package/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
  122. package/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
  123. package/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
  124. package/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
  125. package/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
  126. package/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
  127. package/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
  128. package/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
  129. package/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
  130. package/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
  131. package/skills/video-direction/video-direction.skill.json +20 -0
  132. package/skills/video-style-vocabulary/video-style-vocabulary.skill.json +20 -0
  133. package/skills/video-toolbox/video-toolbox.skill.json +20 -0
  134. package/src/__fixtures__/lifecycle-presets.flat-before.json +205 -0
  135. package/src/agency-os.yaml +122 -0
  136. package/src/agent-os-skill-slugs.test.ts +65 -0
  137. package/src/apify-social-reader.test.ts +317 -0
  138. package/src/base.template.test.ts +25 -19
  139. package/src/base.yaml +95 -39
  140. package/src/blockchain-ecosystem.yaml +1 -0
  141. package/src/brand-library.yaml +525 -14
  142. package/src/builder-dev-fold.template.test.ts +113 -0
  143. package/src/builder.yaml +798 -92
  144. package/src/business-model-catalog-description.test.ts +47 -0
  145. package/src/business-model-grp.template.test.ts +214 -0
  146. package/src/business-model.yaml +598 -0
  147. package/src/catalog-safety.test.ts +198 -0
  148. package/src/catalog.template-items.test.ts +75 -0
  149. package/src/catalog.ts +85 -2
  150. package/src/civic-france.yaml +102 -0
  151. package/src/concept-guard.test.ts +28 -8
  152. package/src/concept-guard.ts +2 -2
  153. package/src/content-creation.template.test.ts +52 -0
  154. package/src/content-creation.yaml +192 -0
  155. package/src/content-os.edit-loop.template.test.ts +210 -0
  156. package/src/content-os.publish-due-posts.template.test.ts +261 -0
  157. package/src/content-os.template.test.ts +322 -0
  158. package/src/content-os.video-takes.template.test.ts +338 -0
  159. package/src/content-os.work-a-take.template.test.ts +125 -0
  160. package/src/content-os.yaml +1830 -39
  161. package/src/content-studio-retired.template.test.ts +82 -0
  162. package/src/corpus-flow-validity.test.ts +4 -5
  163. package/src/crm-absorbs-outreach.template.test.ts +117 -0
  164. package/src/crm-build-plan.template.test.ts +128 -0
  165. package/src/crm-lists.template.test.ts +96 -0
  166. package/src/crm-messages-cadence.template.test.ts +164 -0
  167. package/src/crm-one-send-door.template.test.ts +65 -0
  168. package/src/crm.yaml +1456 -181
  169. package/src/decisions-wave.template.test.ts +186 -0
  170. package/src/define.ts +130 -17
  171. package/src/design-to-implementation.template.test.ts +94 -0
  172. package/src/dist-freshness.test.ts +97 -0
  173. package/src/domain-template-type.test.ts +75 -0
  174. package/src/ecosystem.yaml +27 -66
  175. package/src/enterprise-os.yaml +10 -134
  176. package/src/facecam-edit.audio-studio.test.ts +188 -0
  177. package/src/fal-generate-image.test.ts +200 -0
  178. package/src/fal-media-lab-pod-files.test.ts +180 -0
  179. package/src/finance.yaml +254 -1
  180. package/src/foundation.yaml +130 -28
  181. package/src/frame-host-modules.d.mts +6 -0
  182. package/src/frame-host-modules.mjs +58 -0
  183. package/src/generate.mjs +9 -3
  184. package/src/github-merge.test.ts +210 -0
  185. package/src/grants.yaml +57 -17
  186. package/src/hr.yaml +39 -6
  187. package/src/index.ts +36 -1
  188. package/src/intent.test.ts +161 -0
  189. package/src/internal-runbook.yaml +19 -1
  190. package/src/jev.test.ts +356 -0
  191. package/src/legal.yaml +33 -1
  192. package/src/life-os.yaml +65 -240
  193. package/src/lifecycle-coverage.test.ts +124 -0
  194. package/src/lifecycle-declarations.test.ts +252 -0
  195. package/src/lifecycle-presets.test.ts +468 -0
  196. package/src/lifecycle-presets.ts +494 -0
  197. package/src/lifecycle-stuck.test.ts +46 -0
  198. package/src/lifecycle-waits.parity.test.ts +319 -0
  199. package/src/marketing.yaml +108 -4
  200. package/src/meetings-knowledge.yaml +18 -0
  201. package/src/networking.yaml +23 -2
  202. package/src/operations.yaml +304 -4
  203. package/src/pipeline-views.template.test.ts +88 -0
  204. package/src/playbook-goal-placeholders.test.ts +57 -0
  205. package/src/postiz-contract.test.ts +134 -0
  206. package/src/presentation-declarations.test.ts +174 -0
  207. package/src/process-contract.validate.test.ts +352 -0
  208. package/src/project-management.yaml +39 -740
  209. package/src/publish-body.d.mts +29 -0
  210. package/src/publish-body.mjs +74 -0
  211. package/src/publish-body.test.ts +74 -0
  212. package/src/publish-official-lib.test.ts +176 -0
  213. package/src/replicate-rvm.test.ts +504 -0
  214. package/src/research-base.yaml +119 -20
  215. package/src/research-decision.yaml +60 -3
  216. package/src/reserved-profile-slugs.test.ts +83 -0
  217. package/src/retired-goal-kinds.test.ts +58 -0
  218. package/src/role-reference-and-step-domain.test.ts +192 -0
  219. package/src/seed-obligation.test.ts +176 -0
  220. package/src/send-message-maps.test.ts +227 -0
  221. package/src/shared-role-ssot.test.ts +42 -1
  222. package/src/ship-a-change.template.test.ts +177 -0
  223. package/src/skills.test.ts +170 -0
  224. package/src/social.yaml +85 -27
  225. package/src/space-packages.freshness.test.ts +97 -0
  226. package/src/space-packages.generated.ts +40 -0
  227. package/src/space-packages.test.ts +125 -0
  228. package/src/space-packages.ts +73 -0
  229. package/src/start.test.ts +188 -0
  230. package/src/task-intents.test.ts +293 -0
  231. package/src/templates.ts +21639 -21522
  232. package/src/types.ts +443 -60
  233. package/src/unipile-publish-post.test.ts +162 -0
  234. package/src/validate-flow.drift.test.ts +2 -3
  235. package/src/validate-flow.ts +32 -2
  236. package/src/validate-onboarding.test.ts +59 -0
  237. package/src/validate.test.ts +156 -3
  238. package/src/validate.ts +1384 -7
  239. package/src/wire-core.ts +231 -0
  240. package/src/wire.test.ts +159 -0
  241. package/src/wire.ts +107 -0
  242. package/src/__fixtures__/report-automation-flow.json +0 -393
  243. package/src/agent-fleet.yaml +0 -1645
  244. package/src/business-developer.template.test.ts +0 -351
  245. package/src/business-developer.yaml +0 -860
  246. package/src/content-studio.yaml +0 -361
  247. package/src/dev-dashboard.yaml +0 -990
  248. package/src/outreach-comms.yaml +0 -486
  249. package/src/the-arch.yaml +0 -201
@@ -0,0 +1,188 @@
1
+ /**
2
+ * `meta.start` — a template declares itself an onboarding start (A1).
3
+ *
4
+ * Pinned:
5
+ * - a well-formed start validates with zero `start-*` errors;
6
+ * - each `start-*` rule fires on a minimal broken fixture (and only it);
7
+ * - the package door forwards a TOP-LEVEL `start`, never inside `_meta`;
8
+ * - the catalog header accepts the shape and refuses it on a non-space kind.
9
+ */
10
+ import { describe, expect, it } from "vitest";
11
+ import { validateTemplate } from "./validate.js";
12
+ import { toPackageDefinition } from "./define.js";
13
+ import { INTENT_LABEL_MAX } from "./types.js";
14
+ import type { TemplateProfile, TemplateStart, WorkspaceYaml } from "./types.js";
15
+ import { manifestSchemaFor } from "../schema/index.ts";
16
+ import { listWorkspaceTemplates } from "./templates.js";
17
+
18
+ const deal: TemplateProfile = {
19
+ slug: "deal",
20
+ displayName: "Deal",
21
+ properties: [
22
+ { slug: "commercialStage", label: "Stage", valueType: "string", inputType: "select", enumValues: ["lead", "negotiating", "won"] },
23
+ { slug: "value", label: "Value", valueType: "number" },
24
+ ],
25
+ } as TemplateProfile;
26
+
27
+ const goodStart = (over: Partial<TemplateStart> = {}): TemplateStart => ({
28
+ order: 1,
29
+ label: "Sales",
30
+ promise: "Leads and deals, each with a dated next step.",
31
+ suggestFor: ["founder"],
32
+ profile: "deal",
33
+ views: ["deal-pipeline", "deals-table"],
34
+ example: [1, 2, 3, 4].map((n) => ({ title: `Deal ${n}`, values: { commercialStage: "negotiating", value: n * 1000 } })),
35
+ firstSession: { title: "Your live deals", goal: "Add the deals you are moving now." },
36
+ starters: ["a", "b", "c"],
37
+ ...over,
38
+ });
39
+
40
+ const make = (slug: string, start?: Partial<TemplateStart> | null, extra: Partial<WorkspaceYaml> = {}): WorkspaceYaml =>
41
+ ({
42
+ meta: { slug, name: slug, description: "d", icon: "box", color: "blue", ...(start === null ? {} : { start: goodStart(start) }) },
43
+ workspace: { name: slug, description: "d" },
44
+ profiles: [deal],
45
+ views: [
46
+ { name: "Pipeline", slug: "deal-pipeline", type: "kanban", scopeProfileSlug: "deal" },
47
+ { name: "Deals", slug: "deals-table", type: "table", scopeProfileSlugs: ["deal"] },
48
+ { name: "Other", slug: "other-table", type: "table", scopeProfileSlug: "task" },
49
+ ],
50
+ ...extra,
51
+ }) as unknown as WorkspaceYaml;
52
+
53
+ const startErrors = (t: WorkspaceYaml, corpus?: WorkspaceYaml[]) =>
54
+ validateTemplate(t, { corpus }).errors.filter((e) => e.rule.startsWith("start-")).map((e) => e.rule);
55
+
56
+ describe("meta.start — validator rules", () => {
57
+ it("a well-formed start passes", () => {
58
+ expect(startErrors(make("t"))).toEqual([]);
59
+ });
60
+
61
+ it("no start → no start-* error", () => {
62
+ expect(startErrors(make("t", null))).toEqual([]);
63
+ });
64
+
65
+ it("start-shape: a missing field", () => {
66
+ const t = make("t");
67
+ delete (t.meta.start as Partial<TemplateStart>).promise;
68
+ expect(startErrors(t)).toEqual(["start-shape"]);
69
+ });
70
+
71
+ it("start-no-workspace: an overlay owns no space", () => {
72
+ const t = make("t", {}, { dependencies: [{ slug: "base", relation: "compose" }] } as Partial<WorkspaceYaml>);
73
+ expect(startErrors(t)).toEqual(["start-no-workspace"]);
74
+ });
75
+
76
+ it("start-label: longer than INTENT_LABEL_MAX", () => {
77
+ expect(startErrors(make("t", { label: "x".repeat(INTENT_LABEL_MAX + 1) }))).toEqual(["start-label"]);
78
+ });
79
+
80
+ it("start-example-count and start-starters-count", () => {
81
+ expect(startErrors(make("t", { example: goodStart().example.slice(0, 3) }))).toEqual(["start-example-count"]);
82
+ expect(startErrors(make("t", { starters: ["a"] }))).toEqual(["start-starters-count"]);
83
+ });
84
+
85
+ it("start-views-count: none, or more than three", () => {
86
+ expect(startErrors(make("t", { views: [] }))).toEqual(["start-views-count"]);
87
+ expect(startErrors(make("t", { views: ["deal-pipeline", "deals-table", "deal-pipeline", "deals-table"] }))).toEqual(["start-views-count"]);
88
+ });
89
+
90
+ it("start-views-exist: a slug this template does not declare", () => {
91
+ expect(startErrors(make("t", { views: ["deal-pipeline", "nope"] }))).toEqual(["start-views-exist"]);
92
+ });
93
+
94
+ it("start-views-scope: a view scoped to another kind", () => {
95
+ expect(startErrors(make("t", { views: ["other-table"] }))).toEqual(["start-views-scope"]);
96
+ });
97
+
98
+ it("start-profile-unknown: a kind nobody declares", () => {
99
+ expect(startErrors(make("t", { profile: "ghost", views: [], example: goodStart().example.map((e) => ({ title: e.title })) }))).toContain("start-profile-unknown");
100
+ });
101
+
102
+ it("start-profile-unknown: the kind may come from the dependency closure", () => {
103
+ const dep = { meta: { slug: "dep", name: "dep", description: "d", icon: "b", color: "c" }, workspace: { name: "d", description: "d" }, profiles: [deal] } as unknown as WorkspaceYaml;
104
+ const t = make("t", { views: [] }, { profiles: [], views: [], dependencies: [{ slug: "dep", relation: "require" }] } as Partial<WorkspaceYaml>);
105
+ expect(startErrors(t, [t, dep])).toEqual(["start-views-count"]); // profile resolves; only the empty views fire
106
+ });
107
+
108
+ it("start-example-property: a key the kind does not have", () => {
109
+ const example = goodStart().example.map((e) => ({ ...e, values: { ...e.values, mood: "x" } }));
110
+ expect(startErrors(make("t", { example }))).toEqual(Array(4).fill("start-example-property"));
111
+ });
112
+
113
+ it("start-example-enum: a select value outside enumValues", () => {
114
+ const example = goodStart().example.map((e) => ({ ...e, values: { commercialStage: "dancing" } }));
115
+ expect(startErrors(make("t", { example }))).toEqual(Array(4).fill("start-example-enum"));
116
+ });
117
+
118
+ it("start-suggest-duplicate: an id twice in one start, and across starts", () => {
119
+ expect(startErrors(make("t", { suggestFor: ["founder", "founder"] }))).toEqual(["start-suggest-duplicate"]);
120
+ const a = make("a", { order: 1, suggestFor: ["founder"] });
121
+ const b = make("b", { order: 2, suggestFor: ["founder"] });
122
+ expect(startErrors(b, [a, b])).toEqual(["start-suggest-duplicate"]);
123
+ });
124
+
125
+ it("start-order-duplicate: two starts on one card slot", () => {
126
+ const a = make("a", { order: 1, suggestFor: ["x"] });
127
+ const b = make("b", { order: 1, suggestFor: ["y"] });
128
+ expect(startErrors(b, [a, b])).toEqual(["start-order-duplicate"]);
129
+ expect(startErrors(make("b", { order: 2, suggestFor: ["y"] }), [a, make("b", { order: 2, suggestFor: ["y"] })])).toEqual([]);
130
+ });
131
+ });
132
+
133
+ describe("meta.start — package door", () => {
134
+ it("carries a top-level start, never in _meta", () => {
135
+ const def = toPackageDefinition(make("t"));
136
+ expect(def.start).toEqual(goodStart());
137
+ expect("start" in def._meta).toBe(false);
138
+ });
139
+
140
+ it("no start → no field", () => {
141
+ expect("start" in toPackageDefinition(make("t", null))).toBe(false);
142
+ });
143
+ });
144
+
145
+ describe("meta.start — catalog header", () => {
146
+ const manifest = (kind: "workspace" | "skill", meta: Record<string, unknown>) => ({
147
+ apiVersion: "synap/v1",
148
+ kind,
149
+ meta: { slug: "t", name: "T", ...meta },
150
+ spec: kind === "workspace" ? { workspace: { name: "T", description: "d" }, profiles: [] } : { name: "t", content: "x" },
151
+ });
152
+
153
+ it("a workspace header accepts the shape and refuses an unknown key", () => {
154
+ expect(manifestSchemaFor("workspace").safeParse(manifest("workspace", { start: goodStart() })).success).toBe(true);
155
+ expect(manifestSchemaFor("workspace").safeParse(manifest("workspace", { start: { ...goodStart(), why: 1 } })).success).toBe(false);
156
+ });
157
+
158
+ it("is refused on a kind that is not a workspace", () => {
159
+ const r = manifestSchemaFor("skill").safeParse(manifest("skill", { start: goodStart() }));
160
+ expect(r.success).toBe(false);
161
+ expect(r.error?.issues.some((i) => i.path.join(".") === "meta.start")).toBe(true);
162
+ });
163
+ });
164
+
165
+ describe("meta.start — the shipped catalog (A2)", () => {
166
+ const all = listWorkspaceTemplates();
167
+ const starts = all.filter((t) => t.meta.start);
168
+
169
+ it("declares exactly the four starts, card orders 1-4", () => {
170
+ expect(starts.map((t) => [t.meta.start!.order, t.meta.slug]).sort()).toEqual([
171
+ [1, "builder-workspace"],
172
+ [2, "content-os"],
173
+ [3, "crm"],
174
+ [4, "operations"],
175
+ ]);
176
+ });
177
+
178
+ it("every start passes the start-* rules against the whole corpus", () => {
179
+ for (const t of starts) expect(startErrors(t, all), t.meta.slug).toEqual([]);
180
+ });
181
+
182
+ it("CRM has three deal views scoped to deal", () => {
183
+ const crm = all.find((t) => t.meta.slug === "crm")!;
184
+ const dealViews = (crm.views ?? []).filter((v) => v.scopeProfileSlug === "deal").map((v) => v.slug);
185
+ expect(dealViews).toEqual(["deal-pipeline", "deals-table", "deals-list"]);
186
+ expect(crm.meta.start!.views).toEqual(dealViews);
187
+ });
188
+ });
@@ -0,0 +1,293 @@
1
+ /**
2
+ * TRIPWIRE: `taskIntents` — the workspace's DECLARED task requirements.
3
+ *
4
+ * A template declares the intents (closed vocabulary) it needs to function, so
5
+ * an agent entering that space can be told what it is FOR without discovering
6
+ * it by failing. Three failure classes are guarded here, in increasing order of
7
+ * how quietly they would ship:
8
+ *
9
+ * 1. UNKNOWN SLUG — a `taskIntents` value outside the closed vocabulary. Caught
10
+ * at authoring time by `validateTemplate`; asserted here over the whole
11
+ * corpus so a seed added without ever running the validator still fails.
12
+ * 2. A TEMPLATE WITH NO DECLARATION — not an error by itself (a workspace
13
+ * that needs no external capability declares nothing), but asserted
14
+ * against a DERIVED set so a new template cannot silently ship undeclared.
15
+ * 3. CONTENT STUDIO SPECIFICALLY — the founder-specified reference list, and
16
+ * the one template whose exact declaration is pinned as data in this file.
17
+ *
18
+ * WHAT IT PROVES: that the declared set is (a) inside the closed vocabulary,
19
+ * (b) free of duplicates, (c) agreed on by `validateTemplate` — the gate
20
+ * `build.mjs`, the CP publish path and the CLI actually call — and (d) exactly
21
+ * the founder-specified list for Content Studio. It REPORTS (never gates) how
22
+ * much of each declaration is currently backed by an installed pack; see the
23
+ * long note on that describe block for why a gap is a fact, not an error.
24
+ *
25
+ * WHAT IT DOES NOT PROVE: that any declared intent is the RIGHT requirement for
26
+ * a workspace — that is a judgement, not a derivable fact.
27
+ *
28
+ * ⚠️ THE SCAN IS DERIVED, NOT HAND-LISTED. The template set comes from
29
+ * `readYamlTemplates()` (the same reader `build.mjs` uses), so a template added
30
+ * tomorrow joins this scan BY EXISTING. A hand-written array of slugs is how a
31
+ * guard ends up holding only the entries that were already correct.
32
+ *
33
+ * ⚠️ NON-VACUITY IS ASSERTED EXPLICITLY, twice: the corpus must be non-empty,
34
+ * and the vocabulary must be a real closed set. A glob that matches zero files
35
+ * passes every "no unknown intent" assertion there is.
36
+ */
37
+
38
+ import { describe, expect, it } from "vitest";
39
+ import { existsSync, readFileSync } from "node:fs";
40
+ import { dirname, join } from "node:path";
41
+ import { fileURLToPath } from "node:url";
42
+
43
+ import { CAPABILITY_INTENTS } from "@synap-core/types/capability-intents";
44
+ import { validateTemplate } from "./validate.js";
45
+ import type { WorkspaceYaml } from "./types.js";
46
+
47
+ const HERE = dirname(fileURLToPath(import.meta.url));
48
+ // The reference list the founder specified for Content Studio, now carried by
49
+ // Content OS (content-studio retired into it, 2026-11-03). Pinned HERE as
50
+ // data so the test states what "correct" is rather than reading it from the
51
+ // file it is meant to police.
52
+ const CONTENT_OS_INTENTS = [
53
+ "generate_media",
54
+ "manage_file",
55
+ "capture_into_pod",
56
+ "search_external",
57
+ ];
58
+
59
+ // The catalog's own capability manifests (the CP keeps a byte-checked mirror
60
+ // until its pin bump; see the CP's catalog-mirror.tripwire.test.ts).
61
+ const CAPABILITY_TEMPLATES_DIR = join(HERE, "../capabilities");
62
+
63
+ /** The closed vocabulary as a set, plus a readable unknown-value report. */
64
+ const VOCABULARY = new Set<string>(CAPABILITY_INTENTS);
65
+
66
+ /**
67
+ * The intents a capability pack actually provides, from its declared verb
68
+ * intents. Reads the catalog manifest (its `spec` is the CP seed JSON, kept
69
+ * identical by the mirror tripwire) — so "declared" here means what the seeder
70
+ * will really install.
71
+ *
72
+ * Returns null for a pack this checkout cannot read, which callers treat as
73
+ * "cannot check" rather than "provides nothing" — an unresolvable pack must not
74
+ * silently pass a subset assertion.
75
+ */
76
+ function providedIntents(templateKey: string): Set<string> | null {
77
+ const file = join(CAPABILITY_TEMPLATES_DIR, `${templateKey}.capability.json`);
78
+ if (!existsSync(file)) return null;
79
+ const json = JSON.parse(readFileSync(file, "utf-8")) as unknown;
80
+ const intents = new Set<string>();
81
+ const walk = (node: unknown): void => {
82
+ if (Array.isArray(node)) {
83
+ node.forEach(walk);
84
+ return;
85
+ }
86
+ if (node && typeof node === "object") {
87
+ for (const [key, value] of Object.entries(node)) {
88
+ if (key === "intent" && typeof value === "string") intents.add(value);
89
+ walk(value);
90
+ }
91
+ }
92
+ };
93
+ walk(json);
94
+ return intents;
95
+ }
96
+
97
+ /** Load the corpus through the ONE reader `build.mjs` uses. */
98
+ function loadTemplates(): Array<{ file: string; slug: string; data: WorkspaceYaml }> {
99
+ // Imported lazily so this module's import cost is paid inside the tests, and
100
+ // so a YAML parse error surfaces as a test failure with context.
101
+ const { readYamlTemplates } = require("./generate.mjs") as {
102
+ readYamlTemplates: () => Array<{ file: string; slug: string; data: unknown }>;
103
+ };
104
+ return readYamlTemplates() as Array<{
105
+ file: string;
106
+ slug: string;
107
+ data: WorkspaceYaml;
108
+ }>;
109
+ }
110
+
111
+ const TEMPLATES = loadTemplates();
112
+ const DECLARED = TEMPLATES.filter((t) => Array.isArray(t.data.taskIntents));
113
+
114
+ describe("taskIntents — every declared slug is a known intent", () => {
115
+ it("non-vacuity: the scan actually found templates and a real vocabulary", () => {
116
+ // Without these, "no unknown intent" passes on an empty set.
117
+ expect(TEMPLATES.length).toBeGreaterThanOrEqual(10);
118
+ expect(CAPABILITY_INTENTS.length).toBeGreaterThanOrEqual(13);
119
+ expect(CAPABILITY_INTENTS.length).toBe(new Set(CAPABILITY_INTENTS).size);
120
+ expect(DECLARED.length).toBeGreaterThanOrEqual(5);
121
+ // The scan can still see a literal it hunts: prove the reader picks up a
122
+ // value rather than normalising it away.
123
+ expect(CONTENT_OS_INTENTS.filter((i) => VOCABULARY.has(i))).toHaveLength(
124
+ CONTENT_OS_INTENTS.length
125
+ );
126
+ });
127
+
128
+ it("no template declares an intent outside the closed vocabulary", () => {
129
+ const offenders: string[] = [];
130
+ for (const t of DECLARED) {
131
+ for (const intent of t.data.taskIntents as string[]) {
132
+ if (!VOCABULARY.has(intent)) {
133
+ offenders.push(`${t.file}: "${intent}"`);
134
+ }
135
+ }
136
+ }
137
+ expect(
138
+ offenders,
139
+ `Unknown taskIntents (closed vocabulary: ${CAPABILITY_INTENTS.join(", ")}):`
140
+ ).toEqual([]);
141
+ });
142
+
143
+ it("no template declares a duplicate intent", () => {
144
+ const offenders: string[] = [];
145
+ for (const t of DECLARED) {
146
+ const list = t.data.taskIntents as string[];
147
+ const dupes = list.filter((v, i) => list.indexOf(v) !== i);
148
+ if (dupes.length) offenders.push(`${t.file}: ${[...new Set(dupes)].join(", ")}`);
149
+ }
150
+ expect(offenders, "taskIntents must not repeat an intent").toEqual([]);
151
+ });
152
+
153
+ it("validateTemplate agrees with this scan (the authoring-time gate fires)", () => {
154
+ // The scanner above reads raw YAML; the validator is what build.mjs, the
155
+ // CP publish path and the CLI actually call. Assert the two cannot diverge
156
+ // on the case that matters — an unknown slug must be an ERROR there, not
157
+ // merely a warning this file swallowed.
158
+ const withUnknown: WorkspaceYaml = {
159
+ ...TEMPLATES[0].data,
160
+ taskIntents: ["generate_media", "definitely_not_an_intent"],
161
+ };
162
+ const result = validateTemplate(withUnknown);
163
+ const rule = result.errors.filter((e) => e.rule === "task-intent-unknown");
164
+ expect(rule).toHaveLength(1);
165
+ expect(rule[0].path).toContain("taskIntents");
166
+ // And a fully-valid list produces no taskIntents error at all.
167
+ const ok = validateTemplate({
168
+ ...TEMPLATES[0].data,
169
+ taskIntents: [...CONTENT_OS_INTENTS],
170
+ });
171
+ expect(ok.errors.filter((e) => e.rule.startsWith("task-intent"))).toEqual([]);
172
+ });
173
+ });
174
+
175
+ describe("taskIntents — declared requirements vs. what is installed today", () => {
176
+ /**
177
+ * ⚠️ THIS IS A REPORT, NOT A GATE — deliberately, and the reason is the point
178
+ * of the field.
179
+ *
180
+ * An earlier draft asserted `taskIntents ⊆ intents(provided by integrations)`
181
+ * as a hard failure. It went red immediately and was WRONG: the founder's
182
+ * reference declaration for Content Studio is
183
+ * `[generate_media, manage_file, capture_into_pod, search_external]`, while
184
+ * at the time its only installed pack (`remotion`, since retired) provided
185
+ * just `generate_media` — and today it ships NO pack at all (HyperFrames
186
+ * runs on the user's own agent, plan D9). A subset rule would have forbidden
187
+ * the specification itself.
188
+ *
189
+ * `taskIntents` is what a workspace NEEDS TO FUNCTION, not a snapshot of what
190
+ * it currently has. Declaring an intent ahead of the pack that serves it is
191
+ * the requirement doing its job — it is what a later install, or a Phase-3
192
+ * match, is checked against. So a gap is a FACT to surface, not an error to
193
+ * reject.
194
+ *
195
+ * What this therefore does NOT prove: that every requirement is satisfiable.
196
+ * A template could declare an intent nothing can ever provide, and this stays
197
+ * green. Only a human (or the Phase-3 resolver) can say a requirement is
198
+ * met; a structural check can only say whether it is currently backed.
199
+ *
200
+ * It DOES prove the reporting machinery works: the corpus is walked, packs are
201
+ * resolved for real, and an unresolvable pack is reported rather than counted
202
+ * as providing nothing — the "silently passing" failure this file exists to
203
+ * prevent.
204
+ */
205
+ it("reports each declaration's backing and its gaps", () => {
206
+ interface Coverage {
207
+ file: string;
208
+ declared: number;
209
+ backed: number;
210
+ gaps: string[];
211
+ unreadable: string[];
212
+ }
213
+ const coverage: Coverage[] = [];
214
+ for (const t of DECLARED) {
215
+ const installed = t.data.integrations ?? [];
216
+ const provided = new Set<string>();
217
+ const unreadable: string[] = [];
218
+ for (const integration of installed) {
219
+ const intents = providedIntents(integration.templateKey);
220
+ if (intents === null) {
221
+ unreadable.push(integration.templateKey);
222
+ continue;
223
+ }
224
+ intents.forEach((i) => provided.add(i));
225
+ }
226
+ const declared = t.data.taskIntents as string[];
227
+ coverage.push({
228
+ file: t.file,
229
+ declared: declared.length,
230
+ backed: declared.filter((i) => provided.has(i)).length,
231
+ gaps: declared.filter((i) => !provided.has(i)),
232
+ unreadable,
233
+ });
234
+ }
235
+
236
+ // Non-vacuity: the report is real — every declared template was walked and
237
+ // at least one pack resolved, so the gap list is measured, not empty-by-
238
+ // construction.
239
+ expect(coverage.length).toBeGreaterThanOrEqual(5);
240
+ const resolvable = coverage.filter((c) => c.unreadable.length < (c.declared ? 1 : 0));
241
+ expect(
242
+ coverage.some((c) => c.backed > 0),
243
+ "no declared intent matched any installed pack — the derivation is broken"
244
+ ).toBe(true);
245
+
246
+ // The only hard assertion: a template cannot claim MORE intents than it
247
+ // declared. (Trivially true by construction — kept as the shape check that
248
+ // fails if the coverage walk ever stops being total.)
249
+ for (const c of coverage) {
250
+ expect(c.backed).toBeLessThanOrEqual(c.declared);
251
+ }
252
+
253
+ if (process.env.SYNAP_REPORT_TASK_INTENTS === "1") {
254
+ for (const c of coverage) {
255
+ console.warn(
256
+ `[taskIntents] ${c.file}: ${c.backed}/${c.declared} backed` +
257
+ (c.gaps.length ? ` · gaps: ${c.gaps.join(", ")}` : "") +
258
+ (c.unreadable.length
259
+ ? ` · unreadable packs: ${c.unreadable.join(", ")}`
260
+ : "")
261
+ );
262
+ }
263
+ }
264
+ });
265
+ });
266
+
267
+ // Content OS carries the founder-specified intents over from the retired
268
+ // content-studio (retired.yaml, 2026-11-03), with the video pipeline they describe.
269
+ describe("taskIntents — Content OS is the reference declaration", () => {
270
+ it("declares exactly the founder-specified intents", () => {
271
+ const contentOs = TEMPLATES.find((t) => t.slug === "content-os");
272
+ expect(contentOs, "content-os template not found in src/*.yaml").toBeTruthy();
273
+ expect(contentOs?.data.taskIntents, "content-os must declare taskIntents")
274
+ .toEqual(CONTENT_OS_INTENTS);
275
+ });
276
+
277
+ it("every one of them is a real intent", () => {
278
+ for (const intent of CONTENT_OS_INTENTS) {
279
+ expect(VOCABULARY.has(intent), `"${intent}" is not a known intent`).toBe(
280
+ true
281
+ );
282
+ }
283
+ });
284
+
285
+ it("and the template itself validates", () => {
286
+ const contentOs = TEMPLATES.find((t) => t.slug === "content-os");
287
+ const result = validateTemplate(contentOs!.data);
288
+ expect(
289
+ result.errors.filter((e) => e.rule.startsWith("task-intent")),
290
+ "content-os failed taskIntents validation"
291
+ ).toEqual([]);
292
+ });
293
+ });