@synap-core/workspace-templates 0.12.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 (245) 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 +174 -900
  57. package/dist/index.js +26148 -23931
  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/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.yaml +85 -39
  139. package/src/blockchain-ecosystem.yaml +1 -0
  140. package/src/brand-library.yaml +525 -14
  141. package/src/builder-dev-fold.template.test.ts +113 -0
  142. package/src/builder.yaml +779 -91
  143. package/src/business-model-catalog-description.test.ts +47 -0
  144. package/src/business-model-grp.template.test.ts +214 -0
  145. package/src/business-model.yaml +296 -49
  146. package/src/catalog-safety.test.ts +198 -0
  147. package/src/catalog.template-items.test.ts +75 -0
  148. package/src/catalog.ts +66 -2
  149. package/src/concept-guard.test.ts +28 -8
  150. package/src/concept-guard.ts +2 -2
  151. package/src/content-creation.template.test.ts +52 -0
  152. package/src/content-creation.yaml +192 -0
  153. package/src/content-os.edit-loop.template.test.ts +210 -0
  154. package/src/content-os.publish-due-posts.template.test.ts +261 -0
  155. package/src/content-os.template.test.ts +322 -0
  156. package/src/content-os.video-takes.template.test.ts +338 -0
  157. package/src/content-os.work-a-take.template.test.ts +125 -0
  158. package/src/content-os.yaml +1830 -39
  159. package/src/content-studio-retired.template.test.ts +82 -0
  160. package/src/corpus-flow-validity.test.ts +4 -5
  161. package/src/crm-absorbs-outreach.template.test.ts +117 -0
  162. package/src/crm-build-plan.template.test.ts +128 -0
  163. package/src/crm-lists.template.test.ts +96 -0
  164. package/src/crm-messages-cadence.template.test.ts +164 -0
  165. package/src/crm-one-send-door.template.test.ts +65 -0
  166. package/src/crm.yaml +1456 -181
  167. package/src/decisions-wave.template.test.ts +186 -0
  168. package/src/define.ts +130 -17
  169. package/src/design-to-implementation.template.test.ts +94 -0
  170. package/src/domain-template-type.test.ts +75 -0
  171. package/src/ecosystem.yaml +27 -66
  172. package/src/enterprise-os.yaml +10 -134
  173. package/src/facecam-edit.audio-studio.test.ts +188 -0
  174. package/src/fal-generate-image.test.ts +200 -0
  175. package/src/fal-media-lab-pod-files.test.ts +180 -0
  176. package/src/finance.yaml +254 -1
  177. package/src/foundation.yaml +78 -38
  178. package/src/frame-host-modules.d.mts +6 -0
  179. package/src/frame-host-modules.mjs +58 -0
  180. package/src/generate.mjs +9 -3
  181. package/src/github-merge.test.ts +210 -0
  182. package/src/grants.yaml +46 -17
  183. package/src/hr.yaml +39 -6
  184. package/src/index.ts +36 -1
  185. package/src/intent.test.ts +161 -0
  186. package/src/internal-runbook.yaml +19 -1
  187. package/src/jev.test.ts +356 -0
  188. package/src/legal.yaml +33 -1
  189. package/src/life-os.yaml +51 -240
  190. package/src/lifecycle-coverage.test.ts +124 -0
  191. package/src/lifecycle-declarations.test.ts +252 -0
  192. package/src/lifecycle-presets.test.ts +468 -0
  193. package/src/lifecycle-presets.ts +494 -0
  194. package/src/lifecycle-stuck.test.ts +46 -0
  195. package/src/lifecycle-waits.parity.test.ts +319 -0
  196. package/src/marketing.yaml +108 -4
  197. package/src/meetings-knowledge.yaml +18 -0
  198. package/src/networking.yaml +23 -2
  199. package/src/operations.yaml +304 -4
  200. package/src/pipeline-views.template.test.ts +88 -0
  201. package/src/playbook-goal-placeholders.test.ts +57 -0
  202. package/src/postiz-contract.test.ts +134 -0
  203. package/src/presentation-declarations.test.ts +174 -0
  204. package/src/process-contract.validate.test.ts +352 -0
  205. package/src/project-management.yaml +39 -740
  206. package/src/publish-body.d.mts +29 -0
  207. package/src/publish-body.mjs +74 -0
  208. package/src/publish-body.test.ts +74 -0
  209. package/src/publish-official-lib.test.ts +176 -0
  210. package/src/replicate-rvm.test.ts +504 -0
  211. package/src/research-base.yaml +119 -20
  212. package/src/research-decision.yaml +60 -3
  213. package/src/reserved-profile-slugs.test.ts +83 -0
  214. package/src/retired-goal-kinds.test.ts +58 -0
  215. package/src/role-reference-and-step-domain.test.ts +192 -0
  216. package/src/seed-obligation.test.ts +1 -6
  217. package/src/send-message-maps.test.ts +227 -0
  218. package/src/shared-role-ssot.test.ts +40 -2
  219. package/src/ship-a-change.template.test.ts +177 -0
  220. package/src/skills.test.ts +170 -0
  221. package/src/social.yaml +85 -27
  222. package/src/space-packages.freshness.test.ts +97 -0
  223. package/src/space-packages.generated.ts +40 -0
  224. package/src/space-packages.test.ts +125 -0
  225. package/src/space-packages.ts +73 -0
  226. package/src/start.test.ts +188 -0
  227. package/src/task-intents.test.ts +293 -0
  228. package/src/templates.ts +20851 -21496
  229. package/src/types.ts +443 -60
  230. package/src/unipile-publish-post.test.ts +162 -0
  231. package/src/validate-flow.drift.test.ts +2 -3
  232. package/src/validate-flow.ts +22 -2
  233. package/src/validate-onboarding.test.ts +59 -0
  234. package/src/validate.test.ts +71 -4
  235. package/src/validate.ts +1335 -7
  236. package/src/wire-core.ts +231 -0
  237. package/src/wire.test.ts +159 -0
  238. package/src/wire.ts +107 -0
  239. package/src/agent-fleet.yaml +0 -1645
  240. package/src/business-developer.template.test.ts +0 -351
  241. package/src/business-developer.yaml +0 -860
  242. package/src/content-studio.yaml +0 -361
  243. package/src/dev-dashboard.yaml +0 -990
  244. package/src/outreach-comms.yaml +0 -486
  245. package/src/the-arch.yaml +0 -201
@@ -0,0 +1,186 @@
1
+ /**
2
+ * The founder's decisions of 2026-10-12 (pod file 00c1b2fe-ef98-41fa-86cf-801dfab12cd1, decisions wave),
3
+ * pinned against the REAL YAML corpus (`readYamlTemplates`, the source the
4
+ * build and the publish both read) and the real validator.
5
+ *
6
+ * 1. Business Developer folds into CRM: "Qualify a lead" runs on the `lead`
7
+ * ROLE's leadStage; "Advance a commercial proposal" is gone; the Synap-fee
8
+ * and commission model went to the private arch-sales, which was retired
9
+ * the same day with the-arch (The Arch is no longer in use);
10
+ * business-developer
11
+ * is a tombstone pointing at crm, and nothing installs it any more.
12
+ * 4. "Deliver a deliverable" starts when a deliverable ENTERS in-progress.
13
+ * 5. Billing: per-engagement milestone invoices, and a monthly recurring
14
+ * process on revenue-stream whose schedule the pod can actually create.
15
+ * 6. A new brand voice guide is a draft.
16
+ *
17
+ * Does NOT cover: runtime behaviour on a pod (a role activator firing on
18
+ * attach, the facet status write) — that is the backend lane's (R1).
19
+ */
20
+
21
+ import { describe, expect, it } from "vitest";
22
+ import { readYamlTemplates } from "./generate.mjs";
23
+ import type { WorkspaceYaml } from "./types.js";
24
+ import { validateTemplate } from "./validate.js";
25
+ import { loadRetired } from "../scripts/catalog-lib.mjs";
26
+
27
+ const corpus = readYamlTemplates().map((t) => t.data as WorkspaceYaml);
28
+ const tpl = (slug: string, c: WorkspaceYaml[] = corpus) => {
29
+ const t = c.find((x) => x.meta.slug === slug);
30
+ if (!t) throw new Error(`template ${slug} not in the corpus`);
31
+ return t;
32
+ };
33
+ const profileOf = (t: WorkspaceYaml, slug: string) => t.profiles.find((p) => p.slug === slug);
34
+ const propOf = (t: WorkspaceYaml, profile: string, prop: string) =>
35
+ profileOf(t, profile)?.properties.find((p) => p.slug === prop);
36
+ const playbookOf = (t: WorkspaceYaml, name: string) => t.playbooks?.find((p) => p.name === name);
37
+ const errorsOf = (t: WorkspaceYaml, c: WorkspaceYaml[] = corpus) =>
38
+ validateTemplate(t, { corpus: c }).errors.map((e) => `${e.rule} ${e.path}`);
39
+
40
+ describe("decision 1 — Business Developer folds into CRM", () => {
41
+ it("non-vacuity: the corpus is the real one", () => {
42
+ expect(corpus.length).toBeGreaterThanOrEqual(25);
43
+ expect(corpus.some((t) => t.meta.slug === "crm")).toBe(true);
44
+ });
45
+
46
+ it("business-developer is a tombstone replaced by crm, and no live template depends on it", async () => {
47
+ expect(corpus.some((t) => t.meta.slug === "business-developer")).toBe(false);
48
+ const { retired } = await loadRetired();
49
+ expect(retired.find((r) => r.slug === "business-developer")).toMatchObject({ kind: "workspace", replacedBy: "crm" });
50
+ const dependents = corpus.filter((t) => (t.dependencies ?? []).some((d) => d.slug === "business-developer"));
51
+ expect(dependents.map((t) => t.meta.slug)).toEqual([]);
52
+ });
53
+
54
+ it("'Qualify a lead' is a process on the lead ROLE's leadStage, started by a person", () => {
55
+ const crm = tpl("crm");
56
+ const pb = playbookOf(crm, "Qualify a lead");
57
+ expect(pb?.subjectProfile).toMatchObject({ profileSlug: "lead", statusProperty: "leadStage" });
58
+ // Manual only since 2026-10-27 (founder): a `created` worker ran once per
59
+ // lead and burned the budget; "Score a list" scores a whole list in one run.
60
+ expect(pb?.activators).toEqual([{ on: "manual", mode: "propose" }]);
61
+ // The subject is a ROLE, declared pod-wide by foundation.
62
+ expect(profileOf(tpl("foundation"), "lead")).toMatchObject({ profileKind: "role", scope: "shared" });
63
+ // Every leadStage value is a stage's status or the person's.
64
+ const owned = new Set([...(pb?.stages ?? []).map((s) => s.subjectStatus), ...(pb?.humanOnlyStatuses ?? [])]);
65
+ expect([...owned].filter(Boolean).sort()).toEqual([...(propOf(tpl("foundation"), "lead", "leadStage")?.enumValues ?? [])].sort());
66
+ // Only the last stage, reached when the person chose pursue, moves the lead
67
+ // to qualified. It opens no deal (CRM build plan A4, 2026-10-22): a deal is
68
+ // opened when a person records a booked meeting, which converts the lead.
69
+ expect(pb?.stages?.at(-1)).toMatchObject({ key: "pursue", subjectStatus: "qualified" });
70
+ expect(pb?.stages?.flatMap((s) => s.expectedOutputs ?? [])).toEqual([]);
71
+ expect(pb?.expectedOutputs).toBeUndefined();
72
+ });
73
+
74
+ it("the validator reads a role subject's statuses from the role (a wrong one is refused)", () => {
75
+ expect(errorsOf(tpl("crm"))).toEqual([]);
76
+ const c = structuredClone(corpus);
77
+ const pb = playbookOf(tpl("crm", c), "Qualify a lead")!;
78
+ pb.stages![0]!.subjectStatus = "won"; // a deal status, not a leadStage one
79
+ expect(errorsOf(tpl("crm", c), c)).toContain('stage-subject-status-unknown playbooks."Qualify a lead".stages.research.subjectStatus');
80
+ });
81
+
82
+ it("'Advance a commercial proposal' is gone everywhere; CRM's 'Win a deal' remains", () => {
83
+ const names = corpus.flatMap((t) => (t.playbooks ?? []).map((p) => p.name));
84
+ expect(names).not.toContain("Advance a commercial proposal");
85
+ expect(names).not.toContain("Qualify a CRM lead");
86
+ expect(playbookOf(tpl("crm"), "Win a deal")).toBeDefined();
87
+ });
88
+
89
+ it("CRM carries the moved lead/client fields, the campaign kind and member_of", () => {
90
+ const crm = tpl("crm");
91
+ for (const p of ["icpScore", "qualificationNotes", "nextAction", "outreachChannels", "convertedAt", "disqualificationReason"])
92
+ expect(propOf(crm, "lead", p), `lead.${p}`).toBeDefined();
93
+ for (const p of ["becameClientAt", "handoffStatus", "handoffDestination", "handoffNotes"])
94
+ expect(propOf(crm, "client", p), `client.${p}`).toBeDefined();
95
+ expect(profileOf(crm, "campaign")?.semanticSlug).toBe("marketing.campaign");
96
+ expect(crm.entityLinks?.filter((l) => l.type === "member_of").map((l) => l.sourceProfileSlug)).toEqual(["person", "company"]);
97
+ });
98
+
99
+ it("the Synap-fee and commission model left the catalog with arch-sales and the-arch, both tombstones", async () => {
100
+ const FEE = ["synapFeeModel", "synapFixedFee", "synapSuccessFeeRate", "synapFeeAmount", "sourcedByUserId", "commissionEntitlementId", "commissionStatus"];
101
+ const { retired } = await loadRetired();
102
+ for (const slug of ["arch-sales", "the-arch"]) {
103
+ expect(corpus.some((t) => t.meta.slug === slug), `${slug} is live`).toBe(false);
104
+ const stone = retired.find((r) => r.slug === slug);
105
+ expect(stone, `${slug} tombstone`).toMatchObject({ kind: "workspace", since: "2026-10-12" });
106
+ expect(stone?.replacedBy, `${slug} has no replacement`).toBeUndefined();
107
+ const dependents = corpus.filter((t) => (t.dependencies ?? []).some((d) => d.slug === slug));
108
+ expect(dependents.map((t) => t.meta.slug), `dependents of ${slug}`).toEqual([]);
109
+ }
110
+ // No live template's deal carries the fee model any more (networking's
111
+ // `intro` keeps its own copy of the fee vocabulary: a separate decision).
112
+ for (const p of FEE) {
113
+ const holders = corpus.filter((t) => !!propOf(t, "deal", p)).map((t) => t.meta.slug);
114
+ expect(holders, `deal.${p}`).toEqual([]);
115
+ }
116
+ const holders = (pred: (t: WorkspaceYaml) => boolean) => corpus.filter(pred).map((t) => t.meta.slug);
117
+ expect(holders((t) => !!profileOf(t, "crm-commission-entitlement"))).toEqual([]);
118
+ expect(holders((t) => (t.automations ?? []).some((a) => a.name === "Win and hand off"))).toEqual([]);
119
+ });
120
+ });
121
+
122
+ describe("decision 4 — 'Deliver a deliverable' starts when the deliverable enters in-progress", () => {
123
+ const pb = playbookOf(tpl("operations"), "Deliver a deliverable")!;
124
+
125
+ it("its one activator is entering in-progress", () => {
126
+ expect(pb.activators).toEqual([{ on: "enters_status", status: "in-progress", mode: "propose" }]);
127
+ });
128
+
129
+ it("no stage writes not-started (a run started at in-progress never moves the deliverable back)", () => {
130
+ expect(pb.stages?.[0]?.subjectStatus).toBeUndefined();
131
+ expect(pb.stages?.map((s) => s.subjectStatus)).not.toContain("not-started");
132
+ // `cancelled` (0.20.0, plan aac47c4a W5) is a person's call too: no stage writes it.
133
+ expect(pb.humanOnlyStatuses).toEqual(["not-started", "cancelled"]);
134
+ expect(pb.stages?.map((s) => s.subjectStatus)).not.toContain("cancelled");
135
+ expect(errorsOf(tpl("operations"))).toEqual([]);
136
+ });
137
+ });
138
+
139
+ describe("decision 5 — billing", () => {
140
+ it("an engagement chooses at-close (default) or per-deliverable billing", () => {
141
+ expect(propOf(tpl("operations"), "engagement", "engagement-billing-mode")).toMatchObject({
142
+ inputType: "select",
143
+ enumValues: ["at-close", "per-deliverable"],
144
+ constraints: { defaultValue: "at-close" },
145
+ });
146
+ });
147
+
148
+ it("an accepted deliverable may produce its milestone invoice (an optional slot)", () => {
149
+ const accepted = playbookOf(tpl("operations"), "Deliver a deliverable")!.stages!.find((s) => s.key === "accepted");
150
+ expect(accepted?.expectedOutputs?.map((o) => o.kind)).toEqual(["invoice"]);
151
+ // Owed by nobody (synap-backend 30a9ab85): an at-close run never shows it pending.
152
+ expect(accepted?.expectedOutputs?.[0]?.optional).toBe(true);
153
+ expect(accepted?.goal).toMatch(/per deliverable/);
154
+ });
155
+
156
+ it("'Bill a recurring fee' runs monthly over active monthly revenue streams, with a filter the pod keeps", () => {
157
+ const finance = tpl("finance");
158
+ const pb = playbookOf(finance, "Bill a recurring fee")!;
159
+ expect(pb.subjectProfile?.profileSlug).toBe("revenue-stream");
160
+ expect(pb.schedule).toMatchObject({ cron: "0 8 1 * *", enabled: true });
161
+ expect(pb.metadata).toMatchObject({ governance: { forceProposeWrites: true } });
162
+ // The pod's cron door keeps ONLY a string filter (readSubjectProfile), and
163
+ // refuses an unfiltered AI fan-out, so an object here = no schedule at all.
164
+ const filter = pb.subjectProfile?.filter;
165
+ expect(typeof filter).toBe("string");
166
+ const parsed = JSON.parse(filter as string) as Record<string, string>;
167
+ expect(Object.keys(parsed).length).toBeGreaterThan(0);
168
+ // Reachability: every filtered key is a property of revenue-stream and every value one of its options.
169
+ for (const [key, value] of Object.entries(parsed)) {
170
+ const prop = propOf(finance, "revenue-stream", key.replace(/^properties\./, ""));
171
+ expect(prop?.enumValues, key).toContain(value);
172
+ }
173
+ expect(pb.expectedOutputs?.map((o) => o.kind)).toEqual(["invoice"]);
174
+ expect(errorsOf(finance)).toEqual([]);
175
+ });
176
+
177
+ it("the drafted invoice is taken over by 'Bill a client' (its created activator)", () => {
178
+ expect(playbookOf(tpl("finance"), "Bill a client")?.activators).toContainEqual({ on: "created", mode: "propose" });
179
+ });
180
+ });
181
+
182
+ describe("settled item — a new brand voice guide is a draft", () => {
183
+ it("voice-status defaults to draft", () => {
184
+ expect(propOf(tpl("brand-library"), "brand-voice-guide", "voice-status")?.constraints).toEqual({ defaultValue: "draft" });
185
+ });
186
+ });
package/src/define.ts CHANGED
@@ -16,6 +16,8 @@ import type {
16
16
  WorkspacePrimarySurface,
17
17
  WorkspaceYaml, } from './types.js';
18
18
  import { getWorkspaceTemplate } from './templates.js';
19
+ import { resolveLifecyclePresets } from './lifecycle-presets.js';
20
+ import { spacePackageCells, type SpacePackageCellWire } from './space-packages.js';
19
21
 
20
22
  /**
21
23
  * A `TemplateAutomation` as the apply door expects it: `flowDefinition`, not
@@ -33,7 +35,8 @@ export interface PackageAutomationWire {
33
35
  status?: TemplateAutomation['status'];
34
36
  }
35
37
 
36
- function toAutomationWire(
38
+ /** The ONE automation projection (authored `flow` → wire `flowDefinition`), shared by workspaces and standalone automation packages. */
39
+ export function toAutomationWire(
37
40
  automations: TemplateAutomation[] | undefined
38
41
  ): PackageAutomationWire[] | undefined {
39
42
  return automations?.map((a) => ({
@@ -211,8 +214,9 @@ export function normalizeTemplateLayout(
211
214
  * `kind` is INFERRED from field presence when absent. `TemplateBentoBlock.kind`
212
215
  * is a required field in TypeScript, but the generated `templates.ts` casts the
213
216
  * parsed YAML `as unknown as Record<string, WorkspaceYaml>` — so the compiler
214
- * never checks the authored data against the type, and `agent-fleet.yaml`
215
- * authored 8 widget blocks with no `kind:` at all (bare `widgetType`). Routing
217
+ * never checks the authored data against the type, and the (since retired,
218
+ * R7 2026-09-27) `agent-fleet.yaml` authored 8 widget blocks with no `kind:`
219
+ * at all (bare `widgetType`). Routing
216
220
  * strictly on `kind` would silently DROP those — the exact failure mode this
217
221
  * function exists to end. Inferring by field mirrors what the pod itself does
218
222
  * (it splits by field and never reads `kind`), so the seam is total: a block
@@ -330,6 +334,20 @@ export interface WorkspaceDefinitionInput {
330
334
  workspaceType?: NonNullable<WorkspaceYaml['workspace']>['workspaceType'];
331
335
  workspaceVisibility?: string;
332
336
  workspaceCapabilities?: string[];
337
+ /**
338
+ * THE DECLARED INTENT SINK (Phase 3). `taskIntents` is authored at the TOP
339
+ * level of the template, not inside `workspace`, and is forwarded verbatim to
340
+ * `workspaces.createFromDefinition`, which persists it to
341
+ * `settings.taskIntents` — where the intent resolver joins it against what is
342
+ * actually installed. No shape transform: the authored form IS the wire form.
343
+ */
344
+ taskIntents?: WorkspaceYaml['taskIntents'];
345
+ /**
346
+ * What the space PRODUCES (`meta.intent`), forwarded verbatim; the pod
347
+ * projects it into `settings.intent`. Same top-level field on the package
348
+ * door.
349
+ */
350
+ intent?: WorkspaceYaml['meta']['intent'];
333
351
  sourceRoles?: NonNullable<WorkspaceYaml['workspace']>['sourceRoles'];
334
352
  defaultSources?: NonNullable<WorkspaceYaml['workspace']>['defaultSources'];
335
353
  /** Per-workspace onboarding context — written to settings.onboarding. */
@@ -378,6 +396,30 @@ function toWorkspacePlaybooks(
378
396
  }));
379
397
  }
380
398
 
399
+ /**
400
+ * A template by slug (the bundled corpus, already compiled) or as authored
401
+ * YAML, with its lifecycle presets compiled (`adopts:` → flat options,
402
+ * generated `enumValues`; lifecycle-presets.ts), so both install doors carry
403
+ * the flat declaration the pod stores. An overlay that does not resolve throws,
404
+ * like an unknown slug: `validateTemplate` names it first on every catalog path.
405
+ */
406
+ function compiledTemplate(slugOrYaml: string | WorkspaceYaml): WorkspaceYaml {
407
+ const tpl: WorkspaceYaml | undefined =
408
+ typeof slugOrYaml === 'string' ? getWorkspaceTemplate(slugOrYaml) : slugOrYaml;
409
+ if (!tpl) {
410
+ throw new Error(`Workspace template not found: ${String(slugOrYaml)}`);
411
+ }
412
+ const { template, problems } = resolveLifecyclePresets(tpl);
413
+ if (problems.length > 0) {
414
+ throw new Error(
415
+ `Template "${String(tpl.meta?.slug)}": lifecycle presets do not resolve — ${problems
416
+ .map((p) => `${p.path}: ${p.message}`)
417
+ .join('; ')}`
418
+ );
419
+ }
420
+ return template;
421
+ }
422
+
381
423
  /**
382
424
  * Resolve a template (by slug or YAML object) into the createFromDefinition
383
425
  * payload. Pass `nameOverride` to rename the workspace at creation time.
@@ -386,12 +428,7 @@ export function toWorkspaceDefinition(
386
428
  slugOrYaml: string | WorkspaceYaml,
387
429
  nameOverride?: string
388
430
  ): ToDefinitionResult {
389
- const tpl: WorkspaceYaml | undefined =
390
- typeof slugOrYaml === 'string' ? getWorkspaceTemplate(slugOrYaml) : slugOrYaml;
391
-
392
- if (!tpl) {
393
- throw new Error(`Workspace template not found: ${String(slugOrYaml)}`);
394
- }
431
+ const tpl = compiledTemplate(slugOrYaml);
395
432
 
396
433
  const ws = tpl.workspace;
397
434
  const name = nameOverride ?? ws.name;
@@ -432,6 +469,11 @@ export function toWorkspaceDefinition(
432
469
  workspaceType: ws.workspaceType,
433
470
  workspaceVisibility: ws.visibility,
434
471
  workspaceCapabilities: ws.capabilities,
472
+ // Forwarded VERBATIM from the template's TOP level (not `workspace`) — the
473
+ // pod persists it to `settings.taskIntents` and reports any gap against what
474
+ // the installed capabilities actually provide. Authored form IS wire form.
475
+ taskIntents: tpl.taskIntents,
476
+ intent: tpl.meta.intent,
435
477
  sourceRoles: ws.sourceRoles,
436
478
  defaultSources: ws.defaultSources,
437
479
  // Workspace creation persists its own compact playbook parameter shape.
@@ -500,7 +542,11 @@ type WorkspaceDefinitionForwarded =
500
542
  | 'actionPlacements' // → settings.actionPlacements via applyPackagePostWorkspace
501
543
  | 'automations' // → flowAutomations → shared applyPackagePostWorkspace door
502
544
  | 'onboarding'
503
- | 'dependencies';
545
+ | 'dependencies'
546
+ // Phase 3 gave the declaration a sink: `createWorkspaceFromDefinition`
547
+ // accepts `taskIntents` and persists it verbatim to `settings.taskIntents`,
548
+ // where the intent resolver joins it against what is actually installed.
549
+ | 'taskIntents';
504
550
 
505
551
  /**
506
552
  * Keys deliberately NOT forwarded by `toWorkspaceDefinition`. Each needs a
@@ -508,8 +554,21 @@ type WorkspaceDefinitionForwarded =
508
554
  * else is a real gap and should say so.
509
555
  */
510
556
  type WorkspaceDefinitionDropped =
557
+ // A POST-workspace layer (rule rows via the pod's rule door), carried on the
558
+ // PACKAGE definition (`toPackageDefinition`), not the workspace door's input.
559
+ // The workspace door has no rules field; the rule reconcile converges them.
560
+ | 'rules'
561
+ // Same layer as `rules`: the skill is LINKED to the space by
562
+ // `applyPackagePostWorkspace` (`skills.workspaceId` + `scope:"workspace"`),
563
+ // which owns the row. The workspace door has no skills field, so forwarding
564
+ // it here would be a no-op — it is carried on the PACKAGE definition instead.
565
+ | 'skills'
511
566
  // Normalized into `layoutConfig.primarySurface`.
512
567
  | 'app'
568
+ // Same layer as `skills`: built into frame cells that `applyPackagePostWorkspace`
569
+ // installs in the space. The workspace door has no cells field; carried on
570
+ // the PACKAGE definition as `cells[]` instead.
571
+ | 'spacePackage'
513
572
  // The door DOES accept `extends`, but no template declares it today (0/21).
514
573
  // Forwarding is a no-op until one does — wire it then.
515
574
  | 'extends';
@@ -543,6 +602,12 @@ void _workspaceDefinitionLedger;
543
602
  export interface PackageDefinitionOutput {
544
603
  _meta: {
545
604
  slug: string;
605
+ /** The PACKAGE's own name (meta.name). For an overlay this differs from
606
+ * `workspaceName`, which names the workspace it composes onto. */
607
+ name?: string;
608
+ /** The PACKAGE's own description (meta.description) — what the catalog
609
+ * lists. Top-level `description` stays the workspace's (install copy). */
610
+ description?: string;
546
611
  icon?: string;
547
612
  color?: string;
548
613
  domain?: string;
@@ -556,6 +621,25 @@ export interface PackageDefinitionOutput {
556
621
  workspaceSubtype?: string;
557
622
  workspaceVisibility?: string;
558
623
  workspaceCapabilities?: string[];
624
+ /**
625
+ * The DECLARED intents, carried to the marketplace/CLI install path too.
626
+ * Without it here, a workspace installed from the marketplace would LOSE its
627
+ * declaration while a bundled one kept it — the same field behaving
628
+ * differently per install route, which is precisely the fork this package
629
+ * exists to prevent.
630
+ */
631
+ taskIntents?: WorkspaceYaml['taskIntents'];
632
+ /**
633
+ * What the space PRODUCES (`meta.intent`), TOP-LEVEL like `taskIntents` —
634
+ * never in `_meta`, which the CP rebuilds from its row columns on serve
635
+ * (`normalizePublishedDefinition`), so a key there never reaches a pod.
636
+ */
637
+ intent?: WorkspaceYaml['meta']['intent'];
638
+ /**
639
+ * The onboarding start (`meta.start`), TOP-LEVEL for the same reason as
640
+ * `intent`. Package door only: the pod and the CP read starts off the package.
641
+ */
642
+ start?: WorkspaceYaml['meta']['start'];
559
643
  sourceRoles?: WorkspaceYaml['workspace']['sourceRoles'];
560
644
  defaultSources?: WorkspaceYaml['workspace']['defaultSources'];
561
645
  profiles: WorkspaceYaml['profiles'];
@@ -596,6 +680,24 @@ export interface PackageDefinitionOutput {
596
680
  */
597
681
  actionPlacements?: WorkspaceYaml['actionPlacements'];
598
682
  onboarding?: WorkspaceYaml['onboarding'];
683
+ /**
684
+ * Rules the template installs in its space — applied by the pod's
685
+ * `applyPackagePostWorkspace` through the ONE rule door and converged by the
686
+ * template-rule reconcile (three-way stamp on `metadata.rule.seed`).
687
+ */
688
+ rules?: WorkspaceYaml['rules'];
689
+ /**
690
+ * Skills the template declares for its space — linked to the new space by the
691
+ * pod's `applyPackagePostWorkspace` (approved skills only), which is why they
692
+ * travel on the PACKAGE definition and not the workspace door's input.
693
+ */
694
+ skills?: WorkspaceYaml['skills'];
695
+ /**
696
+ * The space package's built modules (`spacePackage` → `space-packages.ts`),
697
+ * installed by the pod's `applyPackagePostWorkspace` as sandboxed frame cells
698
+ * `cell:<_meta.slug>:<key>` — what the template's `app: {kind: stored}` opens.
699
+ */
700
+ cells?: SpacePackageCellWire[];
599
701
  /** Template-composition dependencies — resolved by the apply endpoint. */
600
702
  dependencies?: WorkspaceYaml['dependencies'];
601
703
  }
@@ -609,20 +711,18 @@ export interface PackageDefinitionOutput {
609
711
  export function toPackageDefinition(
610
712
  slugOrYaml: string | WorkspaceYaml
611
713
  ): PackageDefinitionOutput {
612
- const tpl: WorkspaceYaml | undefined =
613
- typeof slugOrYaml === 'string' ? getWorkspaceTemplate(slugOrYaml) : slugOrYaml;
614
-
615
- if (!tpl) {
616
- throw new Error(`Workspace template not found: ${String(slugOrYaml)}`);
617
- }
714
+ const tpl = compiledTemplate(slugOrYaml);
618
715
 
619
716
  const ws = tpl.workspace;
620
717
  // Same routing as the tRPC door — the union is split by `kind`, never dumped
621
718
  // whole into one field. See `splitBentoBlocks`.
622
719
  const bento = splitBentoBlocks(tpl.bento?.blocks);
720
+ const cells = spacePackageCells(tpl);
623
721
  return {
624
722
  _meta: {
625
723
  slug: tpl.meta.slug,
724
+ name: tpl.meta.name,
725
+ description: tpl.meta.description,
626
726
  icon: tpl.meta.icon,
627
727
  color: tpl.meta.color,
628
728
  domain: tpl.meta.domain,
@@ -635,6 +735,11 @@ export function toPackageDefinition(
635
735
  workspaceSubtype: ws.subtype,
636
736
  workspaceVisibility: ws.visibility,
637
737
  workspaceCapabilities: ws.capabilities,
738
+ // Same declaration, same wire field, on the marketplace path — see the
739
+ // `taskIntents` note on the PackageDefinition type above.
740
+ taskIntents: tpl.taskIntents,
741
+ ...(tpl.meta.intent ? { intent: tpl.meta.intent } : {}),
742
+ ...(tpl.meta.start ? { start: tpl.meta.start } : {}),
638
743
  sourceRoles: ws.sourceRoles,
639
744
  defaultSources: ws.defaultSources,
640
745
  profiles: tpl.profiles,
@@ -655,6 +760,9 @@ export function toPackageDefinition(
655
760
  automations: toAutomationWire(tpl.automations),
656
761
  actionPlacements: tpl.actionPlacements,
657
762
  onboarding: tpl.onboarding,
763
+ ...(tpl.rules?.length ? { rules: tpl.rules } : {}),
764
+ ...(tpl.skills?.length ? { skills: tpl.skills } : {}),
765
+ ...(cells ? { cells } : {}),
658
766
  dependencies: tpl.dependencies,
659
767
  };
660
768
  }
@@ -686,6 +794,9 @@ type PackageDefinitionForwarded =
686
794
  | 'automations' // → mapped to the apply door's flowDefinition wire shape
687
795
  | 'actionPlacements' // → carried verbatim (Hub inbound schema declares it — parity closed)
688
796
  | 'onboarding'
797
+ | 'rules' // → carried verbatim (applied post-workspace via the rule door)
798
+ | 'skills' // → carried verbatim (linked to the space post-workspace)
799
+ | 'spacePackage' // → cells (the built frame modules, installed post-workspace)
689
800
  | 'dependencies';
690
801
 
691
802
  /** Keys deliberately NOT forwarded by `toPackageDefinition`. */
@@ -696,7 +807,9 @@ type PackageDefinitionDropped =
696
807
  // (package-definition.ts:262), but `PackageDefinitionOutput` omits it. No
697
808
  // template declares it today (0/21), so this is latent, not live — wire it
698
809
  // when a template first uses one.
699
- | 'extends';
810
+ | 'extends'
811
+ // Phase 1 declaration with no sink yet — see `WorkspaceDefinitionDropped`.
812
+ | 'taskIntents';
700
813
 
701
814
  type UnclassifiedPackageDefinitionKey = Exclude<
702
815
  keyof WorkspaceYaml,
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The design loop on `ui-design` (Builder, "Design to implementation"):
3
+ * Generate writes 3 sibling variants, Critique judges them with the
4
+ * design-critique rubric before a person reviews.
5
+ *
6
+ * What this guards:
7
+ * 1. REACHABILITY: every property an `evidence` criterion of the process reads
8
+ * is a property `ui-design` declares (derived from the stages, not listed)
9
+ * — an evidence key with no property is a criterion that can never pass;
10
+ * 2. Critique sits between Generate and Review, writes no status (like
11
+ * Verify), is a check gate and loops to Generate;
12
+ * 3. CORPUS-WIDE: no process that PRODUCES things of its own subject kind
13
+ * starts `on: created` — each produced thing would propose another run of
14
+ * the same process (the variants of a design are ui-designs). The
15
+ * activator grammar has no filter to tell a request from its product.
16
+ * 4. the skills the goal tells the agent to load exist in the catalog.
17
+ *
18
+ * Not covered: the wording of the rubric, and whether the pod honours a
19
+ * `manual` worker — only the catalog side.
20
+ */
21
+ import { existsSync } from "node:fs";
22
+ import { join } from "node:path";
23
+ import { describe, expect, it } from "vitest";
24
+ import { WORKSPACE_TEMPLATES, getWorkspaceTemplate } from "./templates.js";
25
+
26
+ const builder = getWorkspaceTemplate("builder-workspace")!;
27
+ const pb = builder.playbooks!.find(
28
+ (p) => p.name === "Design to implementation"
29
+ )!;
30
+ const kind = builder.profiles.find((p) => p.slug === "ui-design")!;
31
+ type Stage = {
32
+ key: string;
33
+ subjectStatus?: string;
34
+ gate?: { kind: string };
35
+ onFail?: { toStage: string };
36
+ criteria?: { check?: { kind: string; evidenceKey?: string } }[];
37
+ expectedOutputs?: { kind: string }[];
38
+ };
39
+ const stages = (pb as unknown as { stages: Stage[] }).stages;
40
+
41
+ describe("Design to implementation — the design loop", () => {
42
+ it("every evidence key the process reads is a property ui-design declares", () => {
43
+ const keys = stages.flatMap((s) =>
44
+ (s.criteria ?? []).map((c) => c.check?.evidenceKey).filter(Boolean)
45
+ );
46
+ expect(keys.length).toBeGreaterThanOrEqual(4); // non-vacuity: design-json, critique, implementation-pr, implemented-story-id
47
+ const declared = new Set(kind.properties.map((p) => p.slug));
48
+ expect(keys.filter((k) => !declared.has(k!))).toEqual([]);
49
+ });
50
+
51
+ it("Critique sits between Generate and Review: no status, a check gate, rework to Generate", () => {
52
+ expect(stages.map((s) => s.key).slice(0, 3)).toEqual([
53
+ "generate",
54
+ "critique",
55
+ "review",
56
+ ]);
57
+ const critique = stages.find((s) => s.key === "critique")!;
58
+ expect(critique.subjectStatus).toBeUndefined();
59
+ expect(critique.gate).toEqual({ kind: "check" });
60
+ expect(critique.onFail).toEqual({ toStage: "generate" });
61
+ });
62
+
63
+ it("no process that produces its own subject kind starts on `created` (corpus-wide)", () => {
64
+ const selfProducing: string[] = [];
65
+ const offenders: string[] = [];
66
+ for (const t of Object.values(WORKSPACE_TEMPLATES))
67
+ for (const p of t.playbooks ?? []) {
68
+ const subject = p.subjectProfile?.profileSlug;
69
+ const ss = (p as unknown as { stages?: Stage[] }).stages ?? [];
70
+ const outs = [
71
+ ...ss.flatMap((s) => s.expectedOutputs ?? []),
72
+ ...((p as unknown as { expectedOutputs?: { kind: string }[] })
73
+ .expectedOutputs ?? []),
74
+ ];
75
+ if (!subject || !outs.some((o) => o.kind === subject)) continue;
76
+ selfProducing.push(`${t.meta.slug}:${p.name}`);
77
+ if ((p.activators ?? []).some((a) => a.on === "created"))
78
+ offenders.push(`${t.meta.slug}:${p.name}`);
79
+ }
80
+ expect(selfProducing).toContain(
81
+ "builder-workspace:Design to implementation"
82
+ ); // non-vacuity
83
+ expect(offenders).toEqual([]);
84
+ });
85
+
86
+ it("the skills the goal loads exist in the catalog", () => {
87
+ for (const slug of ["designing-with-real-components", "design-critique"]) {
88
+ expect(pb.goalTemplate).toContain(slug);
89
+ expect(
90
+ existsSync(join(__dirname, "..", "skills", slug, "SKILL.md"))
91
+ ).toBe(true);
92
+ }
93
+ });
94
+ });
@@ -0,0 +1,75 @@
1
+ /**
2
+ * A template that DECLARES entity kinds must be installable as a workspace
3
+ * that can HOLD them.
4
+ *
5
+ * The pod refuses to file entities into a workspace whose type is
6
+ * `operational` or `agent` (`isDomainHomeWorkspace`, synap-backend
7
+ * `packages/database/src/services/workspace-resolution-service.ts`; enforced at
8
+ * `routers/entities/create.ts` with DOMAIN_INTO_NON_DOMAIN_HOME_MESSAGE) and
9
+ * leaves it out of auto-placement and of the orient domain map. That rule is
10
+ * deliberate — for real admin/system surfaces. Finance, People (hr), Legal and
11
+ * Internal Runbook are DOMAINS that ship profiles (account, invoice, contract,
12
+ * …) yet were typed `operational`, so live 2026-09-25 no entity could be filed
13
+ * into Finance at all.
14
+ *
15
+ * Reads the YAML SOURCES (not the generated `templates.ts`), and derives the
16
+ * set by globbing every `src/*.yaml` — a new template joins by existing.
17
+ */
18
+ import { readdirSync, readFileSync } from "fs";
19
+ import { dirname, join } from "path";
20
+ import { fileURLToPath } from "url";
21
+ import { load } from "js-yaml";
22
+ import { describe, expect, it } from "vitest";
23
+
24
+ const HERE = dirname(fileURLToPath(import.meta.url));
25
+ const NON_DOMAIN_TYPES = new Set(["operational", "agent"]);
26
+
27
+ // Declares kinds AND is a non-domain type — an OPEN founder decision, not a
28
+ // silent pass. A ratchet: delete a line once the template is retyped (or its
29
+ // profiles move out); the test fails on a stale entry.
30
+ // Empty since R7 (2026-09-27): both former entries — dev-dashboard and
31
+ // agent-fleet — were retired as templates (option A); their useful views were
32
+ // folded into builder-workspace.
33
+ const KNOWN_CONTRADICTIONS = new Set<string>([]);
34
+
35
+ type Tpl = {
36
+ meta?: { slug?: string };
37
+ workspace?: { workspaceType?: string };
38
+ profiles?: unknown[];
39
+ };
40
+
41
+ const templates = readdirSync(HERE)
42
+ .filter((f) => f.endsWith(".yaml"))
43
+ .map((f) => load(readFileSync(join(HERE, f), "utf8")) as Tpl)
44
+ .filter((t) => t && typeof t === "object" && t.meta?.slug);
45
+
46
+ describe("a template that declares kinds can hold them", () => {
47
+ it("scans a plausible corpus (non-vacuity)", () => {
48
+ expect(templates.length).toBeGreaterThan(20);
49
+ const slugs = templates.map((t) => t.meta!.slug);
50
+ expect(slugs).toEqual(expect.arrayContaining(["finance", "crm"]));
51
+ });
52
+
53
+ it("no kind-declaring template is typed operational/agent (except the ratchet)", () => {
54
+ const offenders = templates
55
+ .filter(
56
+ (t) =>
57
+ (t.profiles?.length ?? 0) > 0 &&
58
+ NON_DOMAIN_TYPES.has(t.workspace?.workspaceType ?? "")
59
+ )
60
+ .map((t) => t.meta!.slug!)
61
+ .sort();
62
+ expect(offenders.filter((s) => !KNOWN_CONTRADICTIONS.has(s))).toEqual([]);
63
+ expect(
64
+ [...KNOWN_CONTRADICTIONS].filter((s) => !offenders.includes(s)),
65
+ "resolved — remove from KNOWN_CONTRADICTIONS"
66
+ ).toEqual([]);
67
+ });
68
+
69
+ it("the four domains named in the 2026-09-25 bug are domain homes", () => {
70
+ const bySlug = new Map(templates.map((t) => [t.meta!.slug, t]));
71
+ for (const slug of ["finance", "hr", "legal", "internal-runbook"]) {
72
+ expect(bySlug.get(slug)?.workspace?.workspaceType, slug).toBe("personal");
73
+ }
74
+ });
75
+ });