@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
@@ -58,6 +58,15 @@ profiles:
58
58
  icon: git-branch
59
59
  color: violet
60
60
  description: A candidate option under consideration — argued for and against by sources.
61
+ # The ONE lifecycle (work model L1): every option's category.
62
+ lifecycle:
63
+ property: stance
64
+ options:
65
+ - { key: proposed, category: open }
66
+ - { key: supported, category: done }
67
+ - { key: contested, category: active }
68
+ - { key: refuted, category: lost }
69
+ - { key: superseded, category: lost }
61
70
  properties:
62
71
  - slug: stance
63
72
  label: Stance
@@ -131,9 +140,10 @@ playbooks:
131
140
  sources with a reason — never drop the counter-evidence). (4) Weigh the
132
141
  options against explicit, named criteria (cost, reversibility, risk, time,
133
142
  evidence strength). (5) Record the recommendation as a `decision` entity
134
- that `resolves` the question — its rationale explains why it won, and its
135
- `alternatives` field preserves the options that lost and WHY. (6) Note what
136
- evidence would change the decision. Every write is proposal-gated.
143
+ at the proposed status that `resolves` the question — its rationale
144
+ explains why it won, and its `alternatives` field preserves the options
145
+ that lost and WHY. (6) Note what evidence would change the decision.
146
+ Accepting the decision is a person's call. Every write is proposal-gated.
137
147
  subjectProfile:
138
148
  profileSlug: question
139
149
  params:
@@ -145,6 +155,44 @@ playbooks:
145
155
  - exa_search
146
156
  - entity.create
147
157
  status: active
158
+ expectedOutputs:
159
+ - { kind: decision, label: Recommended decision, relationToSubject: resolves }
160
+
161
+ - name: Decide
162
+ description: Put a proposed decision in front of a person with its options and evidence, and record what they chose.
163
+ goalTemplate: >-
164
+ Bring this decision ({decision}) to a person. Summarise the question it
165
+ resolves, the options that were considered, the evidence for and
166
+ against each, and the recommendation, in one screen. A person accepts or
167
+ rejects it. When a later decision replaces it, the person marks it
168
+ superseded.
169
+ subjectProfile:
170
+ profileSlug: decision
171
+ statusProperty: decisionStatus
172
+ humanOnlyStatuses: [superseded, rejected]
173
+ activators:
174
+ - { on: created, mode: propose }
175
+ params:
176
+ - { name: decision, type: entity, label: Decision, required: true }
177
+ executor: is-agent
178
+ status: active
179
+ stages:
180
+ - key: prepare
181
+ name: Prepare
182
+ category: started
183
+ subjectStatus: proposed
184
+ goal: Summarise the question, the options, the evidence and the recommendation in one screen.
185
+ criteria:
186
+ - key: decision-options-listed
187
+ statement: The decision lists every option considered and why each lost or won.
188
+ check: { kind: judge }
189
+ - key: accept
190
+ name: Accept
191
+ category: completed
192
+ subjectStatus: accepted
193
+ goal: A person accepts the decision.
194
+ gate: { kind: human }
195
+ onFail: { toStage: prepare }
148
196
 
149
197
  actionPlacements:
150
198
  - profileSlug: question
@@ -156,3 +204,12 @@ actionPlacements:
156
204
  integrations:
157
205
  - templateKey: research-methods
158
206
  - templateKey: exa
207
+
208
+ # ── taskIntents — WHAT THIS WORKSPACE NEEDS TO FUNCTION ───────────────────
209
+ # Closed vocabulary: `@synap-core/types/capability-intents` (pod migrations
210
+ # 0283/0284). DERIVED from the declared intents of this template's own
211
+ # `integrations` capability packs — not hand-guessed. Checked at authoring
212
+ # time by `validateTemplate` (`task-intent-unknown`) and by
213
+ # `src/task-intents.test.ts`.
214
+ taskIntents:
215
+ - search_external
@@ -0,0 +1,83 @@
1
+ /**
2
+ * A template may never declare a profile on a slug the pod reserves.
3
+ *
4
+ * `project` was declared by builder, life-os and project-management (and the
5
+ * project-management pack) long after migration 0151 moved projects to their
6
+ * own table — every such declaration is refused by `ProfileRepository.create`
7
+ * on install, so it was dead schema at best. `validateTemplate` now refuses it
8
+ * at authoring time (`profile-slug-reserved`), and the catalog validator runs
9
+ * `validateTemplate` for every workspace and pack.
10
+ *
11
+ * The mirror check reads the pod's own reservation module from the sibling
12
+ * synap-backend checkout and skips, with the reason logged, when it is absent.
13
+ */
14
+
15
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
16
+ import { dirname, join, resolve } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { describe, expect, it } from "vitest";
19
+ import { RESERVED_PROFILE_SLUGS, validateTemplate } from "./validate.js";
20
+ import { readYamlTemplates } from "./generate.mjs";
21
+ import type { WorkspaceYaml } from "./types.js";
22
+
23
+ const PKG = resolve(dirname(fileURLToPath(import.meta.url)), "..");
24
+ const BACKEND_RESERVED = resolve(
25
+ PKG,
26
+ "../../../synap-backend/packages/database/src/utils/reserved-profile-slugs.ts"
27
+ );
28
+
29
+ const template = (profiles: WorkspaceYaml["profiles"]): WorkspaceYaml =>
30
+ ({
31
+ meta: { slug: "reserved-probe", name: "Probe", description: "d", icon: "x", color: "violet" },
32
+ workspace: { name: "Probe", description: "d" },
33
+ profiles,
34
+ }) as unknown as WorkspaceYaml;
35
+
36
+ const reservedErrors = (tpl: WorkspaceYaml) =>
37
+ validateTemplate(tpl).errors.filter((e) => e.rule === "profile-slug-reserved");
38
+
39
+ describe("profile-slug-reserved", () => {
40
+ it("refuses a declared `project` profile, case- and whitespace-insensitively", () => {
41
+ for (const slug of ["project", "Projects", " project "]) {
42
+ const errors = reservedErrors(
43
+ template([{ slug, displayName: "Project", properties: [] }] as unknown as WorkspaceYaml["profiles"])
44
+ );
45
+ expect(errors, slug).toHaveLength(1);
46
+ expect(errors[0].message).toContain("projects table");
47
+ }
48
+ });
49
+
50
+ it("leaves a free slug that merely contains the word alone", () => {
51
+ const errors = reservedErrors(
52
+ template([{ slug: "grant-project", displayName: "Grant project", properties: [] }] as unknown as WorkspaceYaml["profiles"])
53
+ );
54
+ expect(errors).toEqual([]);
55
+ });
56
+
57
+ it("no shipped template or pack declares a reserved slug", () => {
58
+ const yamls = readYamlTemplates().map((t) => ({ file: `src/${t.file}`, data: t.data }));
59
+ const packs = readdirSync(join(PKG, "packs"))
60
+ .filter((f) => f.endsWith(".json"))
61
+ .map((f) => ({ file: `packs/${f}`, data: JSON.parse(readFileSync(join(PKG, "packs", f), "utf8")) }));
62
+ // Non-vacuity: the scan sees the corpus, and the corpus declares profiles.
63
+ expect(yamls.length).toBeGreaterThan(20);
64
+ expect(packs.length).toBeGreaterThan(2);
65
+ const declared = [...yamls, ...packs].flatMap((t) => (t.data?.profiles ?? []).map((p: { slug: string }) => p.slug));
66
+ expect(declared.length).toBeGreaterThan(100);
67
+
68
+ const offenders = [...yamls, ...packs].flatMap((t) =>
69
+ reservedErrors(t.data as WorkspaceYaml).map((e) => `${t.file}: ${e.path}`)
70
+ );
71
+ expect(offenders).toEqual([]);
72
+ });
73
+ });
74
+
75
+ describe.skipIf(!existsSync(BACKEND_RESERVED))("RESERVED_PROFILE_SLUGS mirrors the pod's reservation", () => {
76
+ if (!existsSync(BACKEND_RESERVED))
77
+ console.warn(`[reserved-profile-slugs] SKIP: no synap-backend checkout at ${BACKEND_RESERVED}`);
78
+
79
+ it("is the same set as @synap/database reservedProfileSlugs()", async () => {
80
+ const mod = (await import(BACKEND_RESERVED)) as { reservedProfileSlugs: () => readonly string[] };
81
+ expect([...RESERVED_PROFILE_SLUGS].sort()).toEqual([...mod.reservedProfileSlugs()].sort());
82
+ });
83
+ });
@@ -0,0 +1,58 @@
1
+ /**
2
+ * No catalog template declares a goal-like kind (decision D2, 2026-10-23).
3
+ *
4
+ * "Goal" is the user's word for a track (`project_tracks`). The catalog used to
5
+ * ship three entity kinds that read as goals: Life OS `goal`, Enterprise OS
6
+ * `objective`, Project Management `okr`. A pod that installed two of them
7
+ * showed two "Goals". New installs no longer create them; a pod that already
8
+ * has them keeps its entities (profiles are never removed from an installed
9
+ * pod) until the "Make it a goal" conversion is offered.
10
+ *
11
+ * `retired.yaml` tombstones whole templates, not a kind inside a live template,
12
+ * so this list is the kind-level record. A template that needs the idea of a
13
+ * goal links to the pod's goals (tracks); it never re-declares the kind.
14
+ */
15
+
16
+ import { readdirSync, readFileSync } from "node:fs";
17
+ import { dirname, join, resolve } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+ import { describe, expect, it } from "vitest";
20
+ import { readYamlTemplates } from "./generate.mjs";
21
+
22
+ const PKG = resolve(dirname(fileURLToPath(import.meta.url)), "..");
23
+
24
+ const RETIRED_GOAL_KINDS = ["goal", "objective", "okr"] as const;
25
+
26
+ type Declared = { slug?: string };
27
+ type Corpus = { file: string; data: { profiles?: Declared[] } };
28
+
29
+ /** `file: slug` for every declared profile whose slug is a retired goal kind. */
30
+ const retiredKindsDeclared = (corpus: Corpus[]): string[] =>
31
+ corpus.flatMap((t) =>
32
+ (t.data?.profiles ?? [])
33
+ .filter((p) => (RETIRED_GOAL_KINDS as readonly string[]).includes(String(p.slug).trim().toLowerCase()))
34
+ .map((p) => `${t.file}: ${p.slug}`)
35
+ );
36
+
37
+ describe("retired goal-like kinds", () => {
38
+ it("the scan catches a template that declares one (so the guard is not vacuous)", () => {
39
+ const probe: Corpus[] = [
40
+ { file: "probe.yaml", data: { profiles: [{ slug: "task" }, { slug: " OKR " }] } },
41
+ { file: "probe2.yaml", data: { profiles: [{ slug: "goal" }, { slug: "objective" }] } },
42
+ ];
43
+ expect(retiredKindsDeclared(probe)).toEqual(["probe.yaml: OKR ", "probe2.yaml: goal", "probe2.yaml: objective"]);
44
+ });
45
+
46
+ it("no shipped workspace template or pack declares one", () => {
47
+ const yamls: Corpus[] = readYamlTemplates().map((t) => ({ file: `src/${t.file}`, data: t.data }));
48
+ const packs: Corpus[] = readdirSync(join(PKG, "packs"))
49
+ .filter((f) => f.endsWith(".json"))
50
+ .map((f) => ({ file: `packs/${f}`, data: JSON.parse(readFileSync(join(PKG, "packs", f), "utf8")) }));
51
+ // Non-vacuity: the scan reads the corpus, and the corpus declares profiles.
52
+ expect(yamls.length).toBeGreaterThan(20);
53
+ const declared = [...yamls, ...packs].flatMap((t) => t.data?.profiles ?? []);
54
+ expect(declared.length).toBeGreaterThan(100);
55
+
56
+ expect(retiredKindsDeclared([...yamls, ...packs])).toEqual([]);
57
+ });
58
+ });
@@ -0,0 +1,192 @@
1
+ /**
2
+ * W4a (concept consolidation) validator rules — each driven with a template
3
+ * built to BREAK it, plus the real corpus passing it.
4
+ *
5
+ * shared-role-reference-no-foundation a template re-declaring/referencing a
6
+ * foundation shared role must reach foundation through its dependencies.
7
+ * shared-role-reference-unknown a reference (no applicableKinds) must
8
+ * name a role foundation actually defines.
9
+ * stage-domain-shape / -unknown / -not-a-domain a step's `domain` is a slug
10
+ * of a workspace template that OWNS a workspace.
11
+ * pack-requires-pack D8: a pack lists workspace templates.
12
+ *
13
+ * NOT covered: that the pod honours a reference (resolves it to the pod-wide
14
+ * row, grants access) — that is the backend apply door, not this package.
15
+ */
16
+
17
+ import { describe, expect, it } from "vitest";
18
+ import { validateTemplate } from "./validate.js";
19
+ import { WORKSPACE_TEMPLATES } from "./templates.js";
20
+ import type { WorkspaceYaml } from "./types.js";
21
+
22
+ const corpus: WorkspaceYaml[] = Object.values(WORKSPACE_TEMPLATES);
23
+
24
+ const tpl = (over: Record<string, unknown>): WorkspaceYaml =>
25
+ ({
26
+ meta: {
27
+ slug: "acme",
28
+ name: "Acme",
29
+ description: "d",
30
+ icon: "x",
31
+ color: "#000000",
32
+ },
33
+ workspace: { name: "Acme", description: "d" },
34
+ profiles: [{ slug: "gadget", displayName: "Gadget", properties: [] }],
35
+ ...over,
36
+ }) as unknown as WorkspaceYaml;
37
+
38
+ const rules = (t: WorkspaceYaml, prefix: string) =>
39
+ validateTemplate(t, { corpus })
40
+ .errors.filter((e) => e.rule.startsWith(prefix))
41
+ .map((e) => e.rule);
42
+
43
+ const leadReference = {
44
+ slug: "lead",
45
+ displayName: "Lead",
46
+ scope: "shared",
47
+ profileKind: "role",
48
+ properties: [],
49
+ };
50
+
51
+ describe("shared-role references need foundation in the dependency chain", () => {
52
+ it("non-vacuity: foundation defines the shared roles the rule reads", () => {
53
+ const roles = (WORKSPACE_TEMPLATES["foundation"]?.profiles ?? []).filter(
54
+ (p) => p.scope === "shared" && p.profileKind === "role"
55
+ );
56
+ expect(roles.map((p) => p.slug)).toEqual(
57
+ expect.arrayContaining(["client", "partner", "lead"])
58
+ );
59
+ });
60
+
61
+ it("a reference with NO foundation in its chain is refused", () => {
62
+ expect(rules(tpl({ profiles: [leadReference] }), "shared-role-reference")).toEqual([
63
+ "shared-role-reference-no-foundation",
64
+ ]);
65
+ });
66
+
67
+ it("a FULL re-declaration of a foundation role without foundation is refused too", () => {
68
+ const full = { ...leadReference, applicableKinds: ["company", "person"] };
69
+ expect(rules(tpl({ profiles: [full] }), "shared-role-reference")).toEqual([
70
+ "shared-role-reference-no-foundation",
71
+ ]);
72
+ });
73
+
74
+ it("foundation reached TRANSITIVELY (via crm) satisfies it", () => {
75
+ const t = tpl({
76
+ profiles: [leadReference],
77
+ dependencies: [{ slug: "crm", kind: "workspace", relation: "require" }],
78
+ });
79
+ expect(rules(t, "shared-role-reference")).toEqual([]);
80
+ });
81
+
82
+ it("a reference to a role foundation does not define is refused", () => {
83
+ const t = tpl({
84
+ profiles: [{ ...leadReference, slug: "investor", displayName: "Investor" }],
85
+ dependencies: [{ slug: "foundation", kind: "workspace", relation: "require" }],
86
+ });
87
+ expect(rules(t, "shared-role-reference")).toEqual(["shared-role-reference-unknown"]);
88
+ });
89
+
90
+ it("a shared role the template OWNS (full body, not foundation's) is left alone", () => {
91
+ const own = {
92
+ slug: "competitor",
93
+ displayName: "Competitor",
94
+ scope: "shared",
95
+ profileKind: "role",
96
+ applicableKinds: ["company"],
97
+ properties: [],
98
+ };
99
+ expect(rules(tpl({ profiles: [own] }), "shared-role-reference")).toEqual([]);
100
+ });
101
+
102
+ it("crm and ecosystem now carry references, not bodies", () => {
103
+ for (const slug of ["crm", "ecosystem"]) {
104
+ const roles = (WORKSPACE_TEMPLATES[slug]?.profiles ?? []).filter(
105
+ (p) =>
106
+ p.scope === "shared" &&
107
+ p.profileKind === "role" &&
108
+ ["client", "partner", "lead"].includes(p.slug)
109
+ );
110
+ expect(roles.length, `${slug} references no foundation role`).toBeGreaterThan(0);
111
+ for (const r of roles) {
112
+ expect(r.applicableKinds, `${slug}.${r.slug} restates applicableKinds`).toBeUndefined();
113
+ expect(r.description, `${slug}.${r.slug} restates description`).toBeUndefined();
114
+ expect(r.icon, `${slug}.${r.slug} restates icon`).toBeUndefined();
115
+ }
116
+ }
117
+ });
118
+ });
119
+
120
+ const withStep = (domain: unknown) =>
121
+ tpl({
122
+ playbooks: [
123
+ {
124
+ name: "M",
125
+ goalTemplate: "g",
126
+ scope: "project",
127
+ stages: [{ key: "s", name: "S", category: "started", domain }],
128
+ },
129
+ ],
130
+ });
131
+
132
+ describe("step domains name a workspace template that owns a workspace", () => {
133
+ it("a real domain template passes", () => {
134
+ expect(rules(withStep("finance"), "stage-domain")).toEqual([]);
135
+ });
136
+ it("a workspace id is refused", () => {
137
+ expect(rules(withStep("8f894661-db21-4f6d-ba30-5334f7b67bef"), "stage-domain")).toEqual([
138
+ "stage-domain-shape",
139
+ ]);
140
+ });
141
+ it("a display name is refused", () => {
142
+ expect(rules(withStep("Foundation"), "stage-domain")).toEqual(["stage-domain-shape"]);
143
+ });
144
+ it("a slug no template carries is refused", () => {
145
+ expect(rules(withStep("no-such-domain"), "stage-domain")).toEqual(["stage-domain-unknown"]);
146
+ });
147
+ it("an overlay's slug is refused (it never owns a workspace)", () => {
148
+ expect(rules(withStep("business-model"), "stage-domain")).toEqual([
149
+ "stage-domain-not-a-domain",
150
+ ]);
151
+ });
152
+ it("a pack's slug is refused", () => {
153
+ expect(rules(withStep("enterprise-os"), "stage-domain")).toEqual([
154
+ "stage-domain-not-a-domain",
155
+ ]);
156
+ });
157
+ });
158
+
159
+ describe("D8: a pack lists workspace templates, never a pack", () => {
160
+ const pack = (deps: unknown[]) =>
161
+ tpl({ meta: { slug: "p", name: "P", description: "d", icon: "x", color: "#000000", tags: ["suite"] }, dependencies: deps });
162
+ it("pack → pack is refused", () => {
163
+ expect(
164
+ rules(pack([{ slug: "enterprise-os", kind: "workspace", relation: "require" }]), "pack-")
165
+ ).toEqual(["pack-requires-pack"]);
166
+ });
167
+ it("pack → workspace templates passes", () => {
168
+ expect(
169
+ rules(pack([{ slug: "foundation", kind: "workspace", relation: "require" }]), "pack-")
170
+ ).toEqual([]);
171
+ });
172
+ });
173
+
174
+ describe("the real corpus passes every W4a rule", () => {
175
+ it("no template trips a W4a rule", () => {
176
+ expect(corpus.length).toBeGreaterThan(20);
177
+ const hits = corpus.flatMap((t) =>
178
+ validateTemplate(t, { corpus })
179
+ .errors.filter((e) =>
180
+ /^(shared-role-reference|stage-domain|pack-requires-pack)/.test(e.rule)
181
+ )
182
+ .map((e) => `${t.meta?.slug}: ${e.rule} ${e.path}`)
183
+ );
184
+ expect(hits).toEqual([]);
185
+ });
186
+ it("non-vacuity: the corpus HAS step domains and role references to check", () => {
187
+ const domains = corpus.flatMap((t) =>
188
+ (t.playbooks ?? []).flatMap((p) => (p.stages ?? []).map((s) => s.domain))
189
+ ).filter(Boolean);
190
+ expect(domains.length).toBeGreaterThanOrEqual(12); // business-model 6 + grants 6
191
+ });
192
+ });
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Seed-obligation guard.
3
+ *
4
+ * THE BUG THIS EXISTS TO STOP, stated plainly: the applier has supported
5
+ * `seedEntities` and `suggestedRelations` since
6
+ * `create-workspace-from-definition.ts` learned to read them, and for 31 of 31
7
+ * shipped templates that support was exercised exactly zero times. The
8
+ * observable result is a workspace that installs 15 views over 0 entities —
9
+ * every board empty, every chart blank, and the user left to guess what the
10
+ * pack was for. Two real workspaces in the field reproduced this identically,
11
+ * which is the signature of a template defect rather than user behaviour.
12
+ *
13
+ * The rule: a template that ships VIEWS over profiles it OWNS must also ship
14
+ * at least one seed entity, so the workspace opens with something in it.
15
+ *
16
+ * The escape hatch is deliberate and narrow. `SEEDLESS_BY_DESIGN` lists the
17
+ * templates that predate this guard, each with the reason it is still empty.
18
+ * Adding a line there is legal; adding one silently is not, because the list is
19
+ * asserted to be exact — a template that gains seeds must leave the list in the
20
+ * same commit. That is what turns this from a lint into a shrinking backlog.
21
+ *
22
+ * What a seed is NOT: filler. Seeding three fake companies teaches nothing and
23
+ * gets deleted in the first minute. Seed the QUESTIONS, the stages, the
24
+ * checklist — the reusable part of the method. See `business-model.yaml`, which
25
+ * seeds the nine interrogations rather than an example business.
26
+ */
27
+
28
+ import { describe, expect, it } from "vitest";
29
+ import { WORKSPACE_TEMPLATES } from "./templates.js";
30
+ import type { WorkspaceYaml } from "./types.js";
31
+
32
+ /**
33
+ * Templates that ship views over their own profiles and NO seed entities.
34
+ *
35
+ * Every entry is a workspace that currently installs empty. Keep the reason
36
+ * honest — "not done yet" is a valid reason and a useful one, because it reads
37
+ * as debt instead of as a decision.
38
+ */
39
+ const SEEDLESS_BY_DESIGN: Record<string, string> = {
40
+ base: "Pod substrate, not a workspace a user opens. Seeds would land in every workspace.",
41
+ "blockchain-ecosystem": "Pre-dates this guard; needs a seed pass.",
42
+ "brand-library": "Pre-dates this guard; brand assets are inherently per-company.",
43
+ "builder-workspace": "Pre-dates this guard; needs a seed pass.",
44
+ "civic-france": "Pre-dates this guard; needs a seed pass.",
45
+ communication: "Pre-dates this guard; needs a seed pass.",
46
+ "content-os": "Pre-dates this guard; needs a seed pass.",
47
+ "content-creation": "Suite command tower; seeds belong on the constituent workspaces (content-os), same as enterprise-os.",
48
+ crm: "Pre-dates this guard; needs a seed pass.",
49
+ ecosystem: "Pre-dates this guard; needs a seed pass.",
50
+ "enterprise-os": "Pre-dates this guard; needs a seed pass.",
51
+ finance:
52
+ "Pre-dates this guard. Confirmed empty in the field: 15 views, 6 profiles, 0 entities. First candidate for a seed pass — a chart of accounts and a metric set are genuinely reusable.",
53
+ foundation:
54
+ "Pre-dates this guard. Strategic DNA is company-specific, but the INTERROGATION is not — see business-model.yaml, which seeds it as an overlay.",
55
+ grants: "Pre-dates this guard; needs a seed pass.",
56
+ hr: "Pre-dates this guard; needs a seed pass.",
57
+ "internal-runbook": "Pre-dates this guard; needs a seed pass.",
58
+ legal: "Pre-dates this guard; needs a seed pass.",
59
+ "life-os": "Pre-dates this guard; needs a seed pass.",
60
+ "marketing-campaign": "Pre-dates this guard; needs a seed pass.",
61
+ "meetings-knowledge": "Pre-dates this guard; needs a seed pass.",
62
+ networking: "Pre-dates this guard; needs a seed pass.",
63
+ operations: "Pre-dates this guard; needs a seed pass.",
64
+ "project-management": "Pre-dates this guard; needs a seed pass.",
65
+ "research-base":
66
+ "Pre-dates this guard. Radar reproduced the empty-workspace pathology on top of this substrate; seed pass wanted.",
67
+ "research-decision": "Overlay on research-base; the method is a playbook, not entities.",
68
+ social: "Pre-dates this guard; needs a seed pass.",
69
+ };
70
+
71
+ /** Slugs of profiles this template declares itself (owned or re-declared). */
72
+ function declaredProfileSlugs(tpl: WorkspaceYaml): Set<string> {
73
+ const profiles = (tpl as { profiles?: Array<{ slug?: string }> }).profiles ?? [];
74
+ return new Set(profiles.map((p) => p.slug).filter((s): s is string => Boolean(s)));
75
+ }
76
+
77
+ /** Views whose scope is a profile the template declares. */
78
+ function viewsOverOwnProfiles(tpl: WorkspaceYaml): number {
79
+ const declared = declaredProfileSlugs(tpl);
80
+ const views =
81
+ (tpl as { views?: Array<{ scopeProfileSlug?: string; scopeProfileSlugs?: string[] }> })
82
+ .views ?? [];
83
+ return views.filter((v) => {
84
+ const scoped = [v.scopeProfileSlug, ...(v.scopeProfileSlugs ?? [])].filter(Boolean);
85
+ return scoped.some((s) => declared.has(s as string));
86
+ }).length;
87
+ }
88
+
89
+ function seedCount(tpl: WorkspaceYaml): number {
90
+ const t = tpl as {
91
+ seedEntities?: unknown[];
92
+ suggestedEntities?: unknown[];
93
+ };
94
+ return (t.seedEntities?.length ?? 0) + (t.suggestedEntities?.length ?? 0);
95
+ }
96
+
97
+ const slugOf = (slug: string, tpl: WorkspaceYaml) =>
98
+ (tpl as { meta?: { slug?: string } }).meta?.slug ?? slug;
99
+
100
+ describe("workspace-templates — seed obligation (corpus)", () => {
101
+ it("every template with views over its own profiles ships seeds, or is declared seedless", () => {
102
+ const undeclared: string[] = [];
103
+
104
+ for (const [key, tpl] of Object.entries(WORKSPACE_TEMPLATES)) {
105
+ const slug = slugOf(key, tpl);
106
+ if (viewsOverOwnProfiles(tpl) === 0) continue;
107
+ if (seedCount(tpl) > 0) continue;
108
+ if (slug in SEEDLESS_BY_DESIGN) continue;
109
+ undeclared.push(
110
+ `${slug}: ships ${viewsOverOwnProfiles(tpl)} view(s) over its own profiles and 0 seed entities. ` +
111
+ `Add seedEntities (seed the METHOD, not fake data), or add "${slug}" to SEEDLESS_BY_DESIGN with a reason.`
112
+ );
113
+ }
114
+
115
+ expect(
116
+ undeclared,
117
+ `Templates would install a workspace of empty views:\n - ${undeclared.join("\n - ")}`
118
+ ).toEqual([]);
119
+ });
120
+
121
+ it("the seedless allowlist is exact — a template that gains seeds must leave it", () => {
122
+ const stale: string[] = [];
123
+
124
+ for (const [key, tpl] of Object.entries(WORKSPACE_TEMPLATES)) {
125
+ const slug = slugOf(key, tpl);
126
+ if (!(slug in SEEDLESS_BY_DESIGN)) continue;
127
+ if (seedCount(tpl) > 0) {
128
+ stale.push(`${slug}: now seeds ${seedCount(tpl)} entity(ies) — remove it from SEEDLESS_BY_DESIGN`);
129
+ }
130
+ }
131
+
132
+ const corpusSlugs = new Set(
133
+ Object.entries(WORKSPACE_TEMPLATES).map(([key, tpl]) => slugOf(key, tpl))
134
+ );
135
+ for (const slug of Object.keys(SEEDLESS_BY_DESIGN)) {
136
+ if (!corpusSlugs.has(slug)) {
137
+ stale.push(`${slug}: listed in SEEDLESS_BY_DESIGN but no such template exists`);
138
+ }
139
+ }
140
+
141
+ expect(stale, `Stale seedless allowlist:\n - ${stale.join("\n - ")}`).toEqual([]);
142
+ });
143
+
144
+ it("every seeded relation names entities seeded in the same template", () => {
145
+ // A suggested relation whose ref matches no seeded title fails SILENTLY at
146
+ // apply time — the workspace installs, the edge does not, and nothing says
147
+ // so. Catch the typo here instead of in a user's pod.
148
+ const dangling: string[] = [];
149
+
150
+ for (const [key, tpl] of Object.entries(WORKSPACE_TEMPLATES)) {
151
+ const slug = slugOf(key, tpl);
152
+ const t = tpl as {
153
+ seedEntities?: Array<{ title?: string }>;
154
+ suggestedEntities?: Array<{ title?: string }>;
155
+ suggestedRelations?: Array<{ sourceRef?: string; targetRef?: string; type?: string }>;
156
+ };
157
+ const rels = t.suggestedRelations ?? [];
158
+ if (rels.length === 0) continue;
159
+
160
+ const titles = new Set(
161
+ [...(t.seedEntities ?? []), ...(t.suggestedEntities ?? [])]
162
+ .map((e) => e.title)
163
+ .filter((x): x is string => Boolean(x))
164
+ );
165
+
166
+ for (const rel of rels) {
167
+ for (const ref of [rel.sourceRef, rel.targetRef]) {
168
+ if (!ref || titles.has(ref)) continue;
169
+ dangling.push(`${slug}: relation "${rel.type}" references unseeded entity "${ref}"`);
170
+ }
171
+ }
172
+ }
173
+
174
+ expect(dangling, `Dangling seeded relations:\n - ${dangling.join("\n - ")}`).toEqual([]);
175
+ });
176
+ });