@synap-core/workspace-templates 0.12.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.md +509 -0
  2. package/automations/bookmark-enrichment.automation.json +321 -0
  3. package/automations/github-pr-events.automation.json +505 -0
  4. package/automations/mail-feed.automation.json +86 -0
  5. package/automations/ship-a-change-checks-green.automation.json +106 -0
  6. package/automations/ship-a-change-checks-red.automation.json +106 -0
  7. package/automations/ship-a-change-deployed.automation.json +56 -0
  8. package/automations/ship-a-change-pr-merged.automation.json +110 -0
  9. package/automations/ship-a-change-pr-opened.automation.json +104 -0
  10. package/automations/url-bookmark-capture.automation.json +132 -0
  11. package/capabilities/agency-skills.capability.json +157 -0
  12. package/capabilities/apify.capability.json +265 -0
  13. package/capabilities/apollo.capability.json +120 -0
  14. package/capabilities/arch-backend.capability.json +97 -0
  15. package/capabilities/arch-client-intelligence.capability.json +401 -0
  16. package/capabilities/backblaze-b2.capability.json +205 -0
  17. package/capabilities/cal-com.capability.json +266 -0
  18. package/capabilities/canva.capability.json +274 -0
  19. package/capabilities/claude-code-routines.capability.json +172 -0
  20. package/capabilities/claude-managed-agents.capability.json +215 -0
  21. package/capabilities/client-comms-report.capability.json +246 -0
  22. package/capabilities/content-idea-to-production.capability.json +29 -0
  23. package/capabilities/datagouv.capability.json +315 -0
  24. package/capabilities/discord-bot.capability.json +157 -0
  25. package/capabilities/dropbox.capability.json +281 -0
  26. package/capabilities/exa.capability.json +109 -0
  27. package/capabilities/fal-ai.capability.json +83 -0
  28. package/capabilities/fal-media-lab.capability.json +158 -0
  29. package/capabilities/fireflies.capability.json +191 -0
  30. package/capabilities/freellmapi.capability.json +114 -0
  31. package/capabilities/generic-apikey.capability.json +92 -0
  32. package/capabilities/github-merge.capability.json +99 -0
  33. package/capabilities/github-read.capability.json +325 -0
  34. package/capabilities/governance-tighten-recommender.capability.json +81 -0
  35. package/capabilities/jev.capability.json +226 -0
  36. package/capabilities/lead-conversion-enrollment.capability.json +246 -0
  37. package/capabilities/mail-feed.capability.json +277 -0
  38. package/capabilities/mailgun.capability.json +52 -0
  39. package/capabilities/nango-google.capability.json +639 -0
  40. package/capabilities/notion.capability.json +417 -0
  41. package/capabilities/omniroute.capability.json +137 -0
  42. package/capabilities/posthog-analytics.capability.json +258 -0
  43. package/capabilities/postiz.capability.json +467 -0
  44. package/capabilities/proactive-digest.capability.json +190 -0
  45. package/capabilities/proton.capability.json +288 -0
  46. package/capabilities/replicate-rvm.capability.json +97 -0
  47. package/capabilities/research-methods.capability.json +57 -0
  48. package/capabilities/shopify.capability.json +395 -0
  49. package/capabilities/slack.capability.json +369 -0
  50. package/capabilities/stripe.capability.json +1496 -0
  51. package/capabilities/telegram-bridge.capability.json +56 -0
  52. package/capabilities/treg.capability.json +102 -0
  53. package/capabilities/unipile-linkedin.capability.json +494 -0
  54. package/capabilities/vercel.capability.json +424 -0
  55. package/dist/define-DqoItRiR.d.ts +1370 -0
  56. package/dist/index.d.ts +174 -900
  57. package/dist/index.js +26447 -24075
  58. package/dist/index.js.map +1 -1
  59. package/dist/wire.d.ts +222 -0
  60. package/dist/wire.js +77 -0
  61. package/dist/wire.js.map +1 -0
  62. package/package.json +27 -4
  63. package/retired.yaml +136 -0
  64. package/skills/app-factory-build-slice/SKILL.md +70 -0
  65. package/skills/app-factory-build-slice.catalog.json +17 -0
  66. package/skills/app-factory-design-canvas/SKILL.md +96 -0
  67. package/skills/app-factory-design-canvas.catalog.json +17 -0
  68. package/skills/app-factory-design-review/SKILL.md +105 -0
  69. package/skills/app-factory-design-review.catalog.json +18 -0
  70. package/skills/app-factory-research/SKILL.md +123 -0
  71. package/skills/app-factory-research.catalog.json +17 -0
  72. package/skills/bookmark-enrichment/SKILL.md +40 -0
  73. package/skills/carousel-format/carousel-format.skill.json +18 -0
  74. package/skills/collage-broll/collage-broll.skill.json +20 -0
  75. package/skills/content-taste/content-taste.skill.json +18 -0
  76. package/skills/creative-director/creative-director.skill.json +18 -0
  77. package/skills/design-critique/SKILL.md +138 -0
  78. package/skills/design-critique.catalog.json +12 -0
  79. package/skills/designing-with-real-components/SKILL.md +140 -0
  80. package/skills/designing-with-real-components.catalog.json +12 -0
  81. package/skills/edit-loop/edit-loop.skill.json +25 -0
  82. package/skills/facecam-edit/facecam-edit.skill.json +24 -0
  83. package/skills/mapping-the-server/SKILL.md +54 -0
  84. package/skills/math-explainer-manimgx/math-explainer-manimgx.skill.json +20 -0
  85. package/skills/motion-quality-gates/motion-quality-gates.skill.json +20 -0
  86. package/skills/open-design-edit/open-design-edit.skill.json +24 -0
  87. package/skills/reading-client-progress/SKILL.md +65 -0
  88. package/skills/routing-to-channels/SKILL.md +54 -0
  89. package/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
  90. package/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
  91. package/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
  92. package/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
  93. package/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
  94. package/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
  95. package/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
  96. package/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
  97. package/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
  98. package/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
  99. package/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
  100. package/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
  101. package/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
  102. package/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
  103. package/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
  104. package/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
  105. package/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
  106. package/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
  107. package/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
  108. package/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
  109. package/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
  110. package/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
  111. package/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
  112. package/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
  113. package/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
  114. package/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
  115. package/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
  116. package/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
  117. package/skills/video-direction/video-direction.skill.json +20 -0
  118. package/skills/video-style-vocabulary/video-style-vocabulary.skill.json +20 -0
  119. package/skills/video-toolbox/video-toolbox.skill.json +20 -0
  120. package/src/__fixtures__/lifecycle-presets.flat-before.json +205 -0
  121. package/src/agency-os.yaml +122 -0
  122. package/src/agent-os-skill-slugs.test.ts +65 -0
  123. package/src/apify-social-reader.test.ts +317 -0
  124. package/src/base.yaml +85 -39
  125. package/src/blockchain-ecosystem.yaml +1 -0
  126. package/src/brand-library.yaml +525 -14
  127. package/src/builder-dev-fold.template.test.ts +113 -0
  128. package/src/builder.yaml +779 -91
  129. package/src/business-model-catalog-description.test.ts +47 -0
  130. package/src/business-model-grp.template.test.ts +214 -0
  131. package/src/business-model.yaml +296 -49
  132. package/src/catalog-safety.test.ts +198 -0
  133. package/src/catalog.template-items.test.ts +75 -0
  134. package/src/catalog.ts +66 -2
  135. package/src/concept-guard.test.ts +28 -8
  136. package/src/concept-guard.ts +2 -2
  137. package/src/content-creation.template.test.ts +52 -0
  138. package/src/content-creation.yaml +192 -0
  139. package/src/content-os.edit-loop.template.test.ts +210 -0
  140. package/src/content-os.publish-due-posts.template.test.ts +261 -0
  141. package/src/content-os.template.test.ts +349 -0
  142. package/src/content-os.video-takes.template.test.ts +338 -0
  143. package/src/content-os.work-a-take.template.test.ts +125 -0
  144. package/src/content-os.yaml +1873 -39
  145. package/src/content-studio-retired.template.test.ts +82 -0
  146. package/src/corpus-flow-validity.test.ts +4 -5
  147. package/src/crm-absorbs-outreach.template.test.ts +117 -0
  148. package/src/crm-build-plan.template.test.ts +128 -0
  149. package/src/crm-lists.template.test.ts +96 -0
  150. package/src/crm-messages-cadence.template.test.ts +246 -0
  151. package/src/crm-one-send-door.template.test.ts +65 -0
  152. package/src/crm.yaml +1563 -181
  153. package/src/decisions-wave.template.test.ts +186 -0
  154. package/src/define.ts +146 -17
  155. package/src/design-to-implementation.template.test.ts +94 -0
  156. package/src/domain-template-type.test.ts +75 -0
  157. package/src/ecosystem.yaml +27 -66
  158. package/src/enterprise-os.yaml +10 -134
  159. package/src/facecam-edit.audio-studio.test.ts +188 -0
  160. package/src/fal-generate-image.test.ts +200 -0
  161. package/src/fal-media-lab-pod-files.test.ts +180 -0
  162. package/src/finance.yaml +254 -1
  163. package/src/foundation.yaml +78 -38
  164. package/src/frame-host-modules.d.mts +6 -0
  165. package/src/frame-host-modules.mjs +58 -0
  166. package/src/generate.mjs +9 -3
  167. package/src/github-merge.test.ts +210 -0
  168. package/src/grants.yaml +46 -17
  169. package/src/hr.yaml +39 -6
  170. package/src/index.ts +36 -1
  171. package/src/intent.test.ts +161 -0
  172. package/src/internal-runbook.yaml +19 -1
  173. package/src/jev.test.ts +356 -0
  174. package/src/legal.yaml +33 -1
  175. package/src/life-os.yaml +51 -240
  176. package/src/lifecycle-coverage.test.ts +124 -0
  177. package/src/lifecycle-declarations.test.ts +252 -0
  178. package/src/lifecycle-presets.test.ts +468 -0
  179. package/src/lifecycle-presets.ts +494 -0
  180. package/src/lifecycle-stuck.test.ts +46 -0
  181. package/src/lifecycle-waits.parity.test.ts +319 -0
  182. package/src/marketing.yaml +108 -4
  183. package/src/meetings-knowledge.yaml +18 -0
  184. package/src/networking.yaml +23 -2
  185. package/src/operations.yaml +304 -4
  186. package/src/pipeline-views.template.test.ts +88 -0
  187. package/src/playbook-goal-placeholders.test.ts +57 -0
  188. package/src/postiz-contract.test.ts +134 -0
  189. package/src/presentation-declarations.test.ts +174 -0
  190. package/src/process-contract.validate.test.ts +352 -0
  191. package/src/project-management.yaml +39 -740
  192. package/src/publish-body.d.mts +29 -0
  193. package/src/publish-body.mjs +74 -0
  194. package/src/publish-body.test.ts +74 -0
  195. package/src/publish-official-lib.test.ts +176 -0
  196. package/src/replicate-rvm.test.ts +504 -0
  197. package/src/research-base.yaml +119 -20
  198. package/src/research-decision.yaml +60 -3
  199. package/src/reserved-profile-slugs.test.ts +81 -0
  200. package/src/retired-goal-kinds.test.ts +52 -0
  201. package/src/role-reference-and-step-domain.test.ts +192 -0
  202. package/src/seed-obligation.test.ts +1 -6
  203. package/src/send-message-maps.test.ts +227 -0
  204. package/src/shared-role-ssot.test.ts +40 -2
  205. package/src/ship-a-change.template.test.ts +177 -0
  206. package/src/skills.test.ts +170 -0
  207. package/src/social.yaml +85 -27
  208. package/src/space-packages.freshness.test.ts +97 -0
  209. package/src/space-packages.generated.ts +40 -0
  210. package/src/space-packages.test.ts +125 -0
  211. package/src/space-packages.ts +73 -0
  212. package/src/start.test.ts +188 -0
  213. package/src/stripe-capability.test.ts +192 -0
  214. package/src/task-intents.test.ts +293 -0
  215. package/src/template-documents.test.ts +158 -0
  216. package/src/templates.ts +20907 -21464
  217. package/src/types.ts +475 -61
  218. package/src/unipile-linkedin-find-profile.test.ts +183 -0
  219. package/src/unipile-publish-post.test.ts +162 -0
  220. package/src/validate-flow.drift.test.ts +2 -3
  221. package/src/validate-flow.ts +22 -2
  222. package/src/validate-onboarding.test.ts +59 -0
  223. package/src/validate.test.ts +71 -4
  224. package/src/validate.ts +1416 -7
  225. package/src/wire-core.ts +231 -0
  226. package/src/wire.test.ts +159 -0
  227. package/src/wire.ts +107 -0
  228. package/views/bookmark-views-pack.view.json +39 -0
  229. package/views/event-views-pack.view.json +45 -0
  230. package/views/task-views-pack.view.json +53 -0
  231. package/src/agent-fleet.yaml +0 -1645
  232. package/src/business-developer.template.test.ts +0 -351
  233. package/src/business-developer.yaml +0 -860
  234. package/src/content-studio.yaml +0 -361
  235. package/src/dev-dashboard.yaml +0 -990
  236. package/src/outreach-comms.yaml +0 -486
  237. package/src/the-arch.yaml +0 -201
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Lifecycle DECLARATIONS (work model L1) — the catalog declares each kind's and
3
+ * role's ONE lifecycle, so no board, pipeline or governance rung has to guess
4
+ * what a status means from its name.
5
+ *
6
+ * Pinned:
7
+ * - the validator's category list equals `@synap-core/types/lifecycle`;
8
+ * - the mandatory declarations (deal, lead, client, the post that Produce
9
+ * Content drives) carry the founder-confirmed categories;
10
+ * - COVERAGE, derived: every non-system kind a playbook moves through
11
+ * `subjectProfile.statusProperty` declares that property as its lifecycle;
12
+ * - `humanOnly` = the binding playbooks' `humanOnlyStatuses` that stop, pause
13
+ * or end the thing (category paused/done/lost) — never an invented one —
14
+ * plus the exact ledger of founder-decided ACTIVE person-only options
15
+ * (`PERSON_ONLY_ACTIVE`);
16
+ * - the whole corpus validates with zero `lifecycle-*` errors, and each rule
17
+ * fires on a broken clone (self-checks).
18
+ */
19
+ import { describe, expect, it } from "vitest";
20
+ import { LIFECYCLE_CATEGORIES as TYPES_CATEGORIES } from "@synap-core/types/lifecycle";
21
+ import { LIFECYCLE_CATEGORIES, SYSTEM_PROFILES, validateTemplate } from "./validate.js";
22
+ import type { TemplateLifecycle, WorkspaceYaml } from "./types.js";
23
+ import { readYamlTemplates } from "./generate.mjs";
24
+
25
+ const corpus = readYamlTemplates().map((t) => t.data as WorkspaceYaml);
26
+ const tpl = (slug: string) => corpus.find((t) => t.meta.slug === slug)!;
27
+ const lifecycleOf = (t: WorkspaceYaml, profile: string): TemplateLifecycle | undefined =>
28
+ t.profiles.find((p) => p.slug === profile)?.lifecycle;
29
+ const categories = (lc: TemplateLifecycle | undefined) =>
30
+ Object.fromEntries((lc?.options ?? []).map((o) => [o.key, o.category]));
31
+ const lifecycleErrors = (t: WorkspaceYaml, c: WorkspaceYaml[] = corpus) =>
32
+ validateTemplate(t, { corpus: c }).errors.filter((e) => e.rule.startsWith("lifecycle-"));
33
+
34
+ describe("lifecycle declarations — the category vocabulary", () => {
35
+ it("the validator's mirror equals @synap-core/types/lifecycle", () => {
36
+ expect([...LIFECYCLE_CATEGORIES]).toEqual([...TYPES_CATEGORIES]);
37
+ });
38
+ });
39
+
40
+ describe("lifecycle declarations — the mandatory four", () => {
41
+ it("deal = commercialStage; lost is a person's call", () => {
42
+ for (const slug of ["crm"]) {
43
+ const lc = lifecycleOf(tpl(slug), "deal");
44
+ expect(lc?.property, slug).toBe("commercialStage");
45
+ expect(categories(lc)).toEqual({ draft: "open", proposed: "active", negotiating: "active", won: "done", lost: "lost" });
46
+ expect(lc?.options.filter((o) => o.humanOnly).map((o) => o.key)).toEqual(["lost"]);
47
+ }
48
+ });
49
+
50
+ it("lead = leadStage with the W15 categories", () => {
51
+ const lc = lifecycleOf(tpl("foundation"), "lead");
52
+ expect(lc?.property).toBe("leadStage");
53
+ expect(categories(lc)).toEqual({
54
+ new: "open",
55
+ researching: "open",
56
+ outreach: "open",
57
+ engaged: "active",
58
+ qualified: "active",
59
+ nurturing: "active",
60
+ converted: "done",
61
+ disqualified: "lost",
62
+ });
63
+ });
64
+
65
+ it("client = clientStatus, churned is a lost status (never a role)", () => {
66
+ for (const slug of ["foundation", "grants"]) {
67
+ const lc = lifecycleOf(tpl(slug), "client");
68
+ expect(lc?.property, slug).toBe("clientStatus");
69
+ expect(categories(lc).churned, slug).toBe("lost");
70
+ }
71
+ });
72
+
73
+ it("the post Produce Content drives declares post-status", () => {
74
+ const produce = tpl("content-os").playbooks!.find((p) => p.name === "Produce Content")!;
75
+ expect(produce.subjectProfile).toMatchObject({ profileSlug: "post", statusProperty: "post-status" });
76
+ const lc = lifecycleOf(tpl("content-os"), "post");
77
+ expect(lc?.property).toBe("post-status");
78
+ expect(categories(lc)).toMatchObject({ Idea: "open", Published: "done", Archived: "lost" });
79
+ });
80
+ });
81
+
82
+ /**
83
+ * Every (kind, statusProperty, playbook) a catalog playbook moves. `withSystem`
84
+ * false drops the pod's system kinds (task, decision, question…), whose
85
+ * lifecycle the pod seeds itself (`ensure-system-lifecycles.ts`) — except
86
+ * `deal`, a system kind whose lifecycle the CRM template owns.
87
+ */
88
+ const TEMPLATE_OWNED_SYSTEM_LIFECYCLES = new Set(["deal"]);
89
+ function playbookBindings({ withSystem = false } = {}) {
90
+ return corpus.flatMap((t) =>
91
+ (t.playbooks ?? [])
92
+ .filter(
93
+ (pb) =>
94
+ pb.subjectProfile?.statusProperty &&
95
+ (withSystem ||
96
+ !SYSTEM_PROFILES.has(pb.subjectProfile.profileSlug) ||
97
+ TEMPLATE_OWNED_SYSTEM_LIFECYCLES.has(pb.subjectProfile.profileSlug))
98
+ )
99
+ .map((pb) => ({ template: t, pb, kind: pb.subjectProfile!.profileSlug, property: pb.subjectProfile!.statusProperty! }))
100
+ );
101
+ }
102
+
103
+ describe("lifecycle declarations — the dev lifecycles (founder, 2026-10-13)", () => {
104
+ const shape = (profile: string) =>
105
+ lifecycleOf(tpl("builder-workspace"), profile)?.options.map((o) => [o.label, o.category, Boolean(o.humanOnly)]);
106
+
107
+ it("feature = featureStatus: Idea · Spec · Building · Review · Shipped · Dropped", () => {
108
+ expect(lifecycleOf(tpl("builder-workspace"), "devplane_feature")?.property).toBe("featureStatus");
109
+ expect(shape("devplane_feature")).toEqual([
110
+ ["Idea", "open", false],
111
+ ["Spec", "active", false],
112
+ ["Building", "active", false],
113
+ ["Review", "active", false],
114
+ ["Shipped", "done", false],
115
+ ["Dropped", "lost", true],
116
+ ]);
117
+ });
118
+
119
+ it("story = storyStatus: Draft · Ready · In feature · Done · Dropped", () => {
120
+ expect(lifecycleOf(tpl("builder-workspace"), "devplane_story")?.property).toBe("storyStatus");
121
+ expect(shape("devplane_story")).toEqual([
122
+ ["Draft", "open", false],
123
+ ["Ready", "active", false],
124
+ ["In feature", "active", false],
125
+ ["Done", "done", false],
126
+ ["Dropped", "lost", true],
127
+ ]);
128
+ });
129
+ });
130
+
131
+ describe("lifecycle declarations — coverage (derived from the playbooks)", () => {
132
+ it("non-vacuity: the scan sees the bindings it must", () => {
133
+ const kinds = new Set(playbookBindings().map((b) => b.kind));
134
+ expect(kinds.size).toBeGreaterThanOrEqual(14);
135
+ expect(kinds.has("post")).toBe(true);
136
+ expect(kinds.has("deal")).toBe(true);
137
+ });
138
+
139
+ it("every kind a playbook moves declares that property as its lifecycle, wherever it is declared", () => {
140
+ const missing: string[] = [];
141
+ for (const b of playbookBindings())
142
+ for (const t of corpus)
143
+ for (const p of t.profiles.filter((x) => x.slug === b.kind && x.properties.some((pr) => pr.slug === b.property)))
144
+ if (p.lifecycle?.property !== b.property) missing.push(`${t.meta.slug}:${b.kind}.${b.property}`);
145
+ expect([...new Set(missing)]).toEqual([]);
146
+ });
147
+
148
+ /**
149
+ * ACTIVE options a person must set, each a founder decision — the only place
150
+ * `humanOnly` may sit outside paused/done/lost. Still never invented: each
151
+ * must ALSO be in its binding playbook's `humanOnlyStatuses` (the rule below
152
+ * reads both). EXACT: an entry that is no longer humanOnly turns this red.
153
+ * - ui-design.approved — the design review is a person's call; a coding
154
+ * agent implements only from an approved design (DESIGN-DEV-XP-PLAN P6,
155
+ * founder-approved 2026-10-12).
156
+ * - outreach-touch.sent — the agent drafts, a person sends (founder decision
157
+ * 2026-10-20; the CRM absorbed Outreach & Comms 2026-10-23): no agent ever
158
+ * marks a message sent, and the reply reader starts only once a person did.
159
+ */
160
+ const PERSON_ONLY_ACTIVE = new Set(["builder-workspace:ui-design.approved", "crm:outreach-touch.sent"]);
161
+
162
+ /**
163
+ * Person-only options the FOUNDER decided on a lifecycle no playbook moves
164
+ * yet (so no `humanOnlyStatuses` can carry them). Each one a dated decision;
165
+ * EXACT, like the ledger above.
166
+ * - devplane_feature.cancelled / devplane_story.dropped — "Dropped (lost,
167
+ * humanOnly)", object-first homes decision 2026-10-13.
168
+ */
169
+ const FOUNDER_HUMAN_ONLY = new Set([
170
+ "builder-workspace:devplane_feature.cancelled",
171
+ "builder-workspace:devplane_story.dropped",
172
+ ]);
173
+
174
+ it("humanOnly = the binding playbooks' humanOnlyStatuses that pause or end the thing", () => {
175
+ const drift: string[] = [];
176
+ const activeSeen: string[] = [];
177
+ const founderSeen: string[] = [];
178
+ for (const t of corpus)
179
+ for (const p of t.profiles) {
180
+ if (!p.lifecycle) continue;
181
+ const ho = new Set(
182
+ playbookBindings({ withSystem: true })
183
+ .filter((b) => b.kind === p.slug && b.property === p.lifecycle!.property)
184
+ .flatMap((b) => b.pb.humanOnlyStatuses ?? [])
185
+ );
186
+ for (const o of p.lifecycle.options) {
187
+ const id = `${t.meta.slug}:${p.slug}.${o.key}`;
188
+ const want =
189
+ (ho.has(o.key) && (["paused", "done", "lost"].includes(o.category) || PERSON_ONLY_ACTIVE.has(id))) ||
190
+ FOUNDER_HUMAN_ONLY.has(id);
191
+ if (want && PERSON_ONLY_ACTIVE.has(id)) activeSeen.push(id);
192
+ if (o.humanOnly && FOUNDER_HUMAN_ONLY.has(id)) founderSeen.push(id);
193
+ if (Boolean(o.humanOnly) !== want) drift.push(`${id} humanOnly=${Boolean(o.humanOnly)}`);
194
+ }
195
+ }
196
+ expect(drift).toEqual([]);
197
+ expect(activeSeen.sort()).toEqual([...PERSON_ONLY_ACTIVE].sort());
198
+ expect(founderSeen.sort()).toEqual([...FOUNDER_HUMAN_ONLY].sort());
199
+ // non-vacuity: the rule marks real options
200
+ expect(lifecycleOf(tpl("content-os"), "post")?.options.find((o) => o.key === "Archived")?.humanOnly).toBe(true);
201
+ });
202
+ });
203
+
204
+ describe("lifecycle declarations — the validator", () => {
205
+ it("the corpus has zero lifecycle errors", () => {
206
+ expect(corpus.flatMap((t) => lifecycleErrors(t).map((e) => `${t.meta.slug} ${e.rule}: ${e.message}`))).toEqual([]);
207
+ });
208
+
209
+ const clone = () => structuredClone(corpus);
210
+ const crmIn = (c: WorkspaceYaml[]) => c.find((t) => t.meta.slug === "crm")!;
211
+ const dealIn = (c: WorkspaceYaml[]) => crmIn(c).profiles.find((p) => p.slug === "deal")!;
212
+
213
+ it("self-check: options must be the enumValues, same order", () => {
214
+ const c = clone();
215
+ dealIn(c).lifecycle!.options.reverse();
216
+ expect(lifecycleErrors(crmIn(c), c).map((e) => e.rule)).toContain("lifecycle-options-mismatch");
217
+ });
218
+
219
+ it("self-check: an unknown category and an unknown property are refused", () => {
220
+ const c = clone();
221
+ (dealIn(c).lifecycle!.options[0] as { category: string }).category = "closed";
222
+ expect(lifecycleErrors(crmIn(c), c).map((e) => e.rule)).toContain("lifecycle-category");
223
+ dealIn(c).lifecycle!.property = "nope";
224
+ expect(lifecycleErrors(crmIn(c), c).map((e) => e.rule)).toContain("lifecycle-property-unknown");
225
+ });
226
+
227
+ it("self-check: two templates declaring one profile differently diverge", () => {
228
+ // `campaign` is declared with its lifecycle by marketing, content-os and crm
229
+ // (deal is declared with one only by crm since business-developer folded in).
230
+ const campaignIn = (c: WorkspaceYaml[], slug: string) =>
231
+ c.find((t) => t.meta.slug === slug)!.profiles.find((p) => p.slug === "campaign")!;
232
+ const c = clone();
233
+ campaignIn(c, "crm").lifecycle!.options[1].category = "open";
234
+ const marketing = c.find((t) => t.meta.slug === "marketing-campaign")!;
235
+ expect(lifecycleErrors(marketing, c).map((e) => e.rule)).toContain("lifecycle-diverges");
236
+ // dropping it from one declaration diverges too (reported where it is missing)
237
+ const c2 = clone();
238
+ delete campaignIn(c2, "crm").lifecycle;
239
+ expect(lifecycleErrors(crmIn(c2), c2).map((e) => e.rule)).toContain("lifecycle-diverges");
240
+ });
241
+
242
+ it("self-check: a playbook moving another property than the lifecycle is refused", () => {
243
+ const c = clone();
244
+ const produce = c.find((t) => t.meta.slug === "content-os")!.playbooks!.find((p) => p.name === "Produce Content")!;
245
+ const post = c.find((t) => t.meta.slug === "content-os")!.profiles.find((p) => p.slug === "post")!;
246
+ post.lifecycle!.property = "platform";
247
+ produce.subjectProfile!.statusProperty = "post-status";
248
+ expect(lifecycleErrors(c.find((t) => t.meta.slug === "content-os")!, c).map((e) => e.rule)).toContain(
249
+ "lifecycle-status-property"
250
+ );
251
+ });
252
+ });