@synap-core/workspace-templates 0.11.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/README.md +483 -0
  2. package/automations/bookmark-enrichment.automation.json +321 -0
  3. package/automations/github-pr-events.automation.json +505 -0
  4. package/automations/mail-feed.automation.json +86 -0
  5. package/automations/ship-a-change-checks-green.automation.json +106 -0
  6. package/automations/ship-a-change-checks-red.automation.json +106 -0
  7. package/automations/ship-a-change-deployed.automation.json +56 -0
  8. package/automations/ship-a-change-pr-merged.automation.json +110 -0
  9. package/automations/ship-a-change-pr-opened.automation.json +104 -0
  10. package/automations/url-bookmark-capture.automation.json +132 -0
  11. package/capabilities/agency-skills.capability.json +157 -0
  12. package/capabilities/apify.capability.json +265 -0
  13. package/capabilities/apollo.capability.json +120 -0
  14. package/capabilities/arch-backend.capability.json +97 -0
  15. package/capabilities/arch-client-intelligence.capability.json +401 -0
  16. package/capabilities/backblaze-b2.capability.json +205 -0
  17. package/capabilities/cal-com.capability.json +266 -0
  18. package/capabilities/canva.capability.json +274 -0
  19. package/capabilities/claude-code-routines.capability.json +172 -0
  20. package/capabilities/claude-managed-agents.capability.json +215 -0
  21. package/capabilities/client-comms-report.capability.json +246 -0
  22. package/capabilities/content-idea-to-production.capability.json +29 -0
  23. package/capabilities/datagouv.capability.json +315 -0
  24. package/capabilities/discord-bot.capability.json +157 -0
  25. package/capabilities/dropbox.capability.json +281 -0
  26. package/capabilities/exa.capability.json +109 -0
  27. package/capabilities/fal-ai.capability.json +83 -0
  28. package/capabilities/fal-media-lab.capability.json +158 -0
  29. package/capabilities/fireflies.capability.json +191 -0
  30. package/capabilities/freellmapi.capability.json +114 -0
  31. package/capabilities/generic-apikey.capability.json +92 -0
  32. package/capabilities/github-merge.capability.json +99 -0
  33. package/capabilities/github-read.capability.json +325 -0
  34. package/capabilities/governance-tighten-recommender.capability.json +81 -0
  35. package/capabilities/jev.capability.json +226 -0
  36. package/capabilities/lead-conversion-enrollment.capability.json +246 -0
  37. package/capabilities/mail-feed.capability.json +277 -0
  38. package/capabilities/mailgun.capability.json +52 -0
  39. package/capabilities/nango-google.capability.json +639 -0
  40. package/capabilities/notion.capability.json +417 -0
  41. package/capabilities/omniroute.capability.json +137 -0
  42. package/capabilities/posthog-analytics.capability.json +258 -0
  43. package/capabilities/postiz.capability.json +467 -0
  44. package/capabilities/proactive-digest.capability.json +190 -0
  45. package/capabilities/proton.capability.json +288 -0
  46. package/capabilities/replicate-rvm.capability.json +97 -0
  47. package/capabilities/research-methods.capability.json +57 -0
  48. package/capabilities/shopify.capability.json +395 -0
  49. package/capabilities/slack.capability.json +369 -0
  50. package/capabilities/stripe.capability.json +420 -0
  51. package/capabilities/telegram-bridge.capability.json +56 -0
  52. package/capabilities/treg.capability.json +102 -0
  53. package/capabilities/unipile-linkedin.capability.json +471 -0
  54. package/capabilities/vercel.capability.json +424 -0
  55. package/dist/define-CPS0n8BV.d.ts +1331 -0
  56. package/dist/index.d.ts +189 -900
  57. package/dist/index.js +24654 -21639
  58. package/dist/index.js.map +1 -1
  59. package/dist/wire.d.ts +222 -0
  60. package/dist/wire.js +77 -0
  61. package/dist/wire.js.map +1 -0
  62. package/package.json +41 -27
  63. package/packs/bookmark-views-pack.json +33 -0
  64. package/packs/crm-pack.json +76 -0
  65. package/packs/customer-profile.json +28 -0
  66. package/packs/deep-research.playbook.json +30 -0
  67. package/packs/event-views-pack.json +33 -0
  68. package/packs/habit-profile.json +27 -0
  69. package/packs/hr-pack.json +54 -0
  70. package/packs/invoice-profile.json +28 -0
  71. package/packs/knowledge-base-pack.json +56 -0
  72. package/packs/meeting-profile.json +29 -0
  73. package/packs/product-profile.json +30 -0
  74. package/packs/project-management-pack.json +49 -0
  75. package/packs/recipe-profile.json +29 -0
  76. package/packs/task-views-pack.json +39 -0
  77. package/retired.yaml +136 -0
  78. package/skills/app-factory-build-slice/SKILL.md +70 -0
  79. package/skills/app-factory-build-slice.catalog.json +17 -0
  80. package/skills/app-factory-design-canvas/SKILL.md +96 -0
  81. package/skills/app-factory-design-canvas.catalog.json +17 -0
  82. package/skills/app-factory-design-review/SKILL.md +99 -0
  83. package/skills/app-factory-design-review.catalog.json +18 -0
  84. package/skills/app-factory-research/SKILL.md +123 -0
  85. package/skills/app-factory-research.catalog.json +17 -0
  86. package/skills/bookmark-enrichment/SKILL.md +40 -0
  87. package/skills/carousel-format/carousel-format.skill.json +18 -0
  88. package/skills/collage-broll/collage-broll.skill.json +20 -0
  89. package/skills/content-taste/content-taste.skill.json +18 -0
  90. package/skills/creative-director/creative-director.skill.json +18 -0
  91. package/skills/design-critique/SKILL.md +135 -0
  92. package/skills/design-critique.catalog.json +12 -0
  93. package/skills/designing-with-real-components/SKILL.md +140 -0
  94. package/skills/designing-with-real-components.catalog.json +12 -0
  95. package/skills/edit-loop/edit-loop.skill.json +25 -0
  96. package/skills/facecam-edit/facecam-edit.skill.json +24 -0
  97. package/skills/mapping-the-server/SKILL.md +54 -0
  98. package/skills/math-explainer-manimgx/math-explainer-manimgx.skill.json +20 -0
  99. package/skills/motion-quality-gates/motion-quality-gates.skill.json +20 -0
  100. package/skills/open-design-edit/open-design-edit.skill.json +24 -0
  101. package/skills/reading-client-progress/SKILL.md +65 -0
  102. package/skills/routing-to-channels/SKILL.md +54 -0
  103. package/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
  104. package/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
  105. package/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
  106. package/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
  107. package/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
  108. package/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
  109. package/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
  110. package/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
  111. package/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
  112. package/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
  113. package/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
  114. package/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
  115. package/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
  116. package/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
  117. package/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
  118. package/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
  119. package/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
  120. package/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
  121. package/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
  122. package/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
  123. package/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
  124. package/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
  125. package/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
  126. package/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
  127. package/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
  128. package/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
  129. package/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
  130. package/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
  131. package/skills/video-direction/video-direction.skill.json +20 -0
  132. package/skills/video-style-vocabulary/video-style-vocabulary.skill.json +20 -0
  133. package/skills/video-toolbox/video-toolbox.skill.json +20 -0
  134. package/src/__fixtures__/lifecycle-presets.flat-before.json +205 -0
  135. package/src/agency-os.yaml +122 -0
  136. package/src/agent-os-skill-slugs.test.ts +65 -0
  137. package/src/apify-social-reader.test.ts +317 -0
  138. package/src/base.template.test.ts +25 -19
  139. package/src/base.yaml +95 -39
  140. package/src/blockchain-ecosystem.yaml +1 -0
  141. package/src/brand-library.yaml +525 -14
  142. package/src/builder-dev-fold.template.test.ts +113 -0
  143. package/src/builder.yaml +798 -92
  144. package/src/business-model-catalog-description.test.ts +47 -0
  145. package/src/business-model-grp.template.test.ts +214 -0
  146. package/src/business-model.yaml +598 -0
  147. package/src/catalog-safety.test.ts +198 -0
  148. package/src/catalog.template-items.test.ts +75 -0
  149. package/src/catalog.ts +85 -2
  150. package/src/civic-france.yaml +102 -0
  151. package/src/concept-guard.test.ts +28 -8
  152. package/src/concept-guard.ts +2 -2
  153. package/src/content-creation.template.test.ts +52 -0
  154. package/src/content-creation.yaml +192 -0
  155. package/src/content-os.edit-loop.template.test.ts +210 -0
  156. package/src/content-os.publish-due-posts.template.test.ts +261 -0
  157. package/src/content-os.template.test.ts +322 -0
  158. package/src/content-os.video-takes.template.test.ts +338 -0
  159. package/src/content-os.work-a-take.template.test.ts +125 -0
  160. package/src/content-os.yaml +1830 -39
  161. package/src/content-studio-retired.template.test.ts +82 -0
  162. package/src/corpus-flow-validity.test.ts +4 -5
  163. package/src/crm-absorbs-outreach.template.test.ts +117 -0
  164. package/src/crm-build-plan.template.test.ts +128 -0
  165. package/src/crm-lists.template.test.ts +96 -0
  166. package/src/crm-messages-cadence.template.test.ts +164 -0
  167. package/src/crm-one-send-door.template.test.ts +65 -0
  168. package/src/crm.yaml +1456 -181
  169. package/src/decisions-wave.template.test.ts +186 -0
  170. package/src/define.ts +130 -17
  171. package/src/design-to-implementation.template.test.ts +94 -0
  172. package/src/dist-freshness.test.ts +97 -0
  173. package/src/domain-template-type.test.ts +75 -0
  174. package/src/ecosystem.yaml +27 -66
  175. package/src/enterprise-os.yaml +10 -134
  176. package/src/facecam-edit.audio-studio.test.ts +188 -0
  177. package/src/fal-generate-image.test.ts +200 -0
  178. package/src/fal-media-lab-pod-files.test.ts +180 -0
  179. package/src/finance.yaml +254 -1
  180. package/src/foundation.yaml +130 -28
  181. package/src/frame-host-modules.d.mts +6 -0
  182. package/src/frame-host-modules.mjs +58 -0
  183. package/src/generate.mjs +9 -3
  184. package/src/github-merge.test.ts +210 -0
  185. package/src/grants.yaml +57 -17
  186. package/src/hr.yaml +39 -6
  187. package/src/index.ts +36 -1
  188. package/src/intent.test.ts +161 -0
  189. package/src/internal-runbook.yaml +19 -1
  190. package/src/jev.test.ts +356 -0
  191. package/src/legal.yaml +33 -1
  192. package/src/life-os.yaml +65 -240
  193. package/src/lifecycle-coverage.test.ts +124 -0
  194. package/src/lifecycle-declarations.test.ts +252 -0
  195. package/src/lifecycle-presets.test.ts +468 -0
  196. package/src/lifecycle-presets.ts +494 -0
  197. package/src/lifecycle-stuck.test.ts +46 -0
  198. package/src/lifecycle-waits.parity.test.ts +319 -0
  199. package/src/marketing.yaml +108 -4
  200. package/src/meetings-knowledge.yaml +18 -0
  201. package/src/networking.yaml +23 -2
  202. package/src/operations.yaml +304 -4
  203. package/src/pipeline-views.template.test.ts +88 -0
  204. package/src/playbook-goal-placeholders.test.ts +57 -0
  205. package/src/postiz-contract.test.ts +134 -0
  206. package/src/presentation-declarations.test.ts +174 -0
  207. package/src/process-contract.validate.test.ts +352 -0
  208. package/src/project-management.yaml +39 -740
  209. package/src/publish-body.d.mts +29 -0
  210. package/src/publish-body.mjs +74 -0
  211. package/src/publish-body.test.ts +74 -0
  212. package/src/publish-official-lib.test.ts +176 -0
  213. package/src/replicate-rvm.test.ts +504 -0
  214. package/src/research-base.yaml +119 -20
  215. package/src/research-decision.yaml +60 -3
  216. package/src/reserved-profile-slugs.test.ts +83 -0
  217. package/src/retired-goal-kinds.test.ts +58 -0
  218. package/src/role-reference-and-step-domain.test.ts +192 -0
  219. package/src/seed-obligation.test.ts +176 -0
  220. package/src/send-message-maps.test.ts +227 -0
  221. package/src/shared-role-ssot.test.ts +42 -1
  222. package/src/ship-a-change.template.test.ts +177 -0
  223. package/src/skills.test.ts +170 -0
  224. package/src/social.yaml +85 -27
  225. package/src/space-packages.freshness.test.ts +97 -0
  226. package/src/space-packages.generated.ts +40 -0
  227. package/src/space-packages.test.ts +125 -0
  228. package/src/space-packages.ts +73 -0
  229. package/src/start.test.ts +188 -0
  230. package/src/task-intents.test.ts +293 -0
  231. package/src/templates.ts +21639 -21522
  232. package/src/types.ts +443 -60
  233. package/src/unipile-publish-post.test.ts +162 -0
  234. package/src/validate-flow.drift.test.ts +2 -3
  235. package/src/validate-flow.ts +32 -2
  236. package/src/validate-onboarding.test.ts +59 -0
  237. package/src/validate.test.ts +156 -3
  238. package/src/validate.ts +1384 -7
  239. package/src/wire-core.ts +231 -0
  240. package/src/wire.test.ts +159 -0
  241. package/src/wire.ts +107 -0
  242. package/src/__fixtures__/report-automation-flow.json +0 -393
  243. package/src/agent-fleet.yaml +0 -1645
  244. package/src/business-developer.template.test.ts +0 -351
  245. package/src/business-developer.yaml +0 -860
  246. package/src/content-studio.yaml +0 -361
  247. package/src/dev-dashboard.yaml +0 -990
  248. package/src/outreach-comms.yaml +0 -486
  249. package/src/the-arch.yaml +0 -201
@@ -0,0 +1,468 @@
1
+ /**
2
+ * Lifecycle presets (`lifecycle.adopts`, plan aac47c4a W5): the ten bases
3
+ * compile, at template time, into the flat declaration the pod stores.
4
+ *
5
+ * (1) BYTE-IDENTICAL CONVERSION. Five lifecycles were rewritten from a flat
6
+ * `options` list + a hand-kept `enumValues` copy into `adopts:` overlays.
7
+ * `__fixtures__/lifecycle-presets.flat-before.json` holds each one's flat
8
+ * lifecycle and select property exactly as main declared them before
9
+ * (generated from `git show 1d590b166:…`, not typed by hand). The test
10
+ * compiles today's YAML and requires the same JSON, byte for byte (key
11
+ * order included: it is what `src/templates.ts` and dist/ carry).
12
+ * (2) The compiler's overlay operations and the problems it reports.
13
+ * (3) The three doors that read authored YAML compile it: the validator, both
14
+ * define converters, and the generator (through readYamlTemplates).
15
+ */
16
+ import { readFileSync } from "node:fs";
17
+ import { dirname, join } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+ import { load } from "js-yaml";
20
+ import { describe, expect, it } from "vitest";
21
+ import { readYamlTemplates } from "./generate.mjs";
22
+ import {
23
+ BASE_LIFECYCLES,
24
+ resolveLifecyclePresets,
25
+ } from "./lifecycle-presets.js";
26
+ import { toPackageDefinition, toWorkspaceDefinition } from "./define.js";
27
+ import { LIFECYCLE_CATEGORIES, validateTemplate } from "./validate.js";
28
+ import type {
29
+ TemplateLifecycle,
30
+ TemplateProfile,
31
+ TemplateProperty,
32
+ WorkspaceYaml,
33
+ } from "./types.js";
34
+
35
+ const HERE = dirname(fileURLToPath(import.meta.url));
36
+ const before = JSON.parse(
37
+ readFileSync(
38
+ join(HERE, "__fixtures__/lifecycle-presets.flat-before.json"),
39
+ "utf8"
40
+ )
41
+ ) as {
42
+ profiles: Array<{
43
+ template: string;
44
+ profile: string;
45
+ lifecycle: TemplateLifecycle;
46
+ property: TemplateProperty;
47
+ }>;
48
+ };
49
+ const compiled = new Map(
50
+ readYamlTemplates().map((t) => [t.slug, t.data as WorkspaceYaml])
51
+ );
52
+ const authored = (slug: string) =>
53
+ load(readFileSync(join(HERE, `${slug}.yaml`), "utf8")) as WorkspaceYaml;
54
+ const profileOf = (tpl: WorkspaceYaml, slug: string) =>
55
+ tpl.profiles.find((p) => p.slug === slug)!;
56
+
57
+ describe("lifecycle presets — the converted lifecycles compile byte-identical", () => {
58
+ it("non-vacuity: five conversions, each authored with adopts and no enumValues", () => {
59
+ expect(before.profiles).toHaveLength(5);
60
+ for (const b of before.profiles) {
61
+ const raw = profileOf(authored(b.template), b.profile);
62
+ expect(raw.lifecycle, `${b.template}/${b.profile}`).toHaveProperty(
63
+ "adopts"
64
+ );
65
+ expect(raw.lifecycle, `${b.template}/${b.profile}`).not.toHaveProperty(
66
+ "options"
67
+ );
68
+ expect(
69
+ raw.properties.find((p) => p.slug === b.lifecycle.property)
70
+ ).not.toHaveProperty("enumValues");
71
+ }
72
+ });
73
+
74
+ // The ONE intended change since the conversion: the muted tone the presets
75
+ // give a lost step that is a decision (UX review 2026-10-29). Everything
76
+ // else is main's flat declaration, byte for byte.
77
+ const TONE_SINCE: Record<string, Record<string, string>> = {
78
+ "brand-color": { deprecated: "textMuted" },
79
+ "brand-asset": { deprecated: "textMuted" },
80
+ "brand-reference": { archived: "textMuted" },
81
+ };
82
+ const withTone = (b: (typeof before.profiles)[number]) => ({
83
+ ...b.lifecycle,
84
+ options: b.lifecycle.options.map((o) => {
85
+ const tone = TONE_SINCE[b.profile]?.[o.key];
86
+ if (!tone) return o;
87
+ const { key, category, ...rest } = o;
88
+ return { key, category, tone, ...rest };
89
+ }),
90
+ });
91
+
92
+ for (const b of before.profiles) {
93
+ it(`${b.template}/${b.profile}: lifecycle and select equal main's flat declaration`, () => {
94
+ const profile = profileOf(compiled.get(b.template)!, b.profile);
95
+ expect(JSON.stringify(profile.lifecycle)).toBe(
96
+ JSON.stringify(withTone(b))
97
+ );
98
+ expect(
99
+ JSON.stringify(
100
+ profile.properties.find((p) => p.slug === b.property.slug)
101
+ )
102
+ ).toBe(JSON.stringify(b.property));
103
+ });
104
+ }
105
+ });
106
+
107
+ describe("lifecycle presets — the ten bases", () => {
108
+ it("a lost step that is a decision is muted; a failure keeps the lost default (no tone)", () => {
109
+ const muted = Object.entries(BASE_LIFECYCLES).flatMap(([name, steps]) =>
110
+ steps
111
+ .filter((s) => "tone" in s && s.tone === "textMuted")
112
+ .map((s) => `${name}.${s.key}`)
113
+ );
114
+ expect(muted.sort()).toEqual([
115
+ "asset.deprecated",
116
+ "decision.superseded",
117
+ "decision.withdrawn",
118
+ "publication.archived",
119
+ "run.cancelled",
120
+ "work-item.cancelled",
121
+ ]);
122
+ for (const [name, key] of [
123
+ ["run", "failed"],
124
+ ["pipeline", "lost"],
125
+ ["relationship", "churned"],
126
+ ] as const)
127
+ expect(
128
+ BASE_LIFECYCLES[name].find((s) => s.key === key),
129
+ `${name}.${key}`
130
+ ).not.toHaveProperty("tone");
131
+ for (const steps of Object.values(BASE_LIFECYCLES))
132
+ for (const s of steps) if ("tone" in s) expect(s.category).toBe("lost");
133
+ });
134
+
135
+ it("are ten, each with unique keys, known categories and a done step", () => {
136
+ expect(Object.keys(BASE_LIFECYCLES)).toHaveLength(10);
137
+ for (const [name, steps] of Object.entries(BASE_LIFECYCLES)) {
138
+ const keys = steps.map((s) => s.key);
139
+ expect(new Set(keys).size, name).toBe(keys.length);
140
+ for (const s of steps)
141
+ expect(LIFECYCLE_CATEGORIES, `${name}.${s.key}`).toContain(s.category);
142
+ expect(
143
+ steps.some((s) => s.category === "done"),
144
+ name
145
+ ).toBe(true);
146
+ // Picker order follows the categories: open → active → paused → done → lost.
147
+ const ranks = steps.map((s) => LIFECYCLE_CATEGORIES.indexOf(s.category));
148
+ expect(ranks, name).toEqual([...ranks].sort((a, b) => a - b));
149
+ }
150
+ });
151
+
152
+ it("each base alone validates as a lifecycle", () => {
153
+ for (const name of Object.keys(BASE_LIFECYCLES)) {
154
+ const { errors } = validateTemplate(
155
+ templateWith({ property: "status", adopts: name })
156
+ );
157
+ expect(errors, name).toEqual([]);
158
+ }
159
+ });
160
+ });
161
+
162
+ /** A minimal valid template whose one profile carries `lifecycle` on a select without enumValues. */
163
+ function templateWith(
164
+ lifecycle: unknown,
165
+ property: Partial<TemplateProperty> = {}
166
+ ): WorkspaceYaml {
167
+ return {
168
+ meta: {
169
+ slug: "preset-probe",
170
+ name: "Probe",
171
+ description: "d",
172
+ icon: "box",
173
+ color: "blue",
174
+ },
175
+ workspace: { name: "Probe", description: "d" },
176
+ profiles: [
177
+ {
178
+ slug: "probe",
179
+ displayName: "Probe",
180
+ lifecycle,
181
+ properties: [
182
+ {
183
+ slug: "title",
184
+ label: "Title",
185
+ valueType: "string",
186
+ inputType: "text",
187
+ },
188
+ {
189
+ slug: "status",
190
+ label: "Status",
191
+ valueType: "string",
192
+ inputType: "select",
193
+ constraints: { defaultValue: "x" },
194
+ ...property,
195
+ },
196
+ ],
197
+ } as unknown as TemplateProfile,
198
+ ],
199
+ } as unknown as WorkspaceYaml;
200
+ }
201
+ const resolvedProbe = (
202
+ lifecycle: unknown,
203
+ property?: Partial<TemplateProperty>
204
+ ) => {
205
+ const r = resolveLifecyclePresets(templateWith(lifecycle, property));
206
+ return { ...r, profile: r.template.profiles[0] };
207
+ };
208
+
209
+ describe("lifecycle presets — overlay operations", () => {
210
+ it("adopts a base as is and generates enumValues right after inputType", () => {
211
+ const { profile, problems } = resolvedProbe({
212
+ property: "status",
213
+ adopts: "relationship",
214
+ });
215
+ expect(problems).toEqual([]);
216
+ expect(profile.lifecycle).toEqual({
217
+ property: "status",
218
+ options: [...BASE_LIFECYCLES.relationship],
219
+ });
220
+ expect(Object.keys(profile.properties[1])).toEqual([
221
+ "slug",
222
+ "label",
223
+ "valueType",
224
+ "inputType",
225
+ "enumValues",
226
+ "constraints",
227
+ ]);
228
+ expect(profile.properties[1].enumValues).toEqual([
229
+ "prospect",
230
+ "active",
231
+ "paused",
232
+ "former",
233
+ "churned",
234
+ ]);
235
+ });
236
+
237
+ it("renames, drops, adds at a position, and overrides humanOnly and stuckAfterDays", () => {
238
+ const { profile, problems } = resolvedProbe({
239
+ property: "status",
240
+ adopts: "agreement",
241
+ rename: { fulfilled: "ended" },
242
+ drop: ["in-progress", "expired"],
243
+ add: [
244
+ { key: "signed", category: "active", after: "sent" },
245
+ { key: "renewing", category: "paused" },
246
+ { key: "first", category: "open", before: "draft" },
247
+ ],
248
+ humanOnly: { void: false, signed: true },
249
+ stuckAfterDays: { sent: 14 },
250
+ });
251
+ expect(problems).toEqual([]);
252
+ expect(profile.lifecycle!.options).toEqual([
253
+ { key: "first", category: "open" },
254
+ { key: "draft", category: "open" },
255
+ { key: "sent", category: "active", stuckAfterDays: 14 },
256
+ { key: "signed", category: "active", humanOnly: true },
257
+ { key: "on-hold", category: "paused" },
258
+ { key: "renewing", category: "paused" },
259
+ { key: "ended", category: "done" },
260
+ { key: "declined", category: "lost", humanOnly: true },
261
+ { key: "void", category: "lost" },
262
+ ]);
263
+ });
264
+
265
+ it("leaves a flat lifecycle that carries its enumValues as the same object (idempotent)", () => {
266
+ const tpl = compiled.get("crm")!;
267
+ expect(resolveLifecyclePresets(tpl).template).toBe(tpl);
268
+ const once = resolveLifecyclePresets(
269
+ templateWith({ property: "status", adopts: "asset" })
270
+ ).template;
271
+ expect(resolveLifecyclePresets(once).template).toBe(once);
272
+ });
273
+
274
+ it("generates enumValues for a FLAT lifecycle too", () => {
275
+ const flat = {
276
+ property: "status",
277
+ options: [
278
+ { key: "draft", category: "open" },
279
+ { key: "done", category: "done" },
280
+ ],
281
+ };
282
+ const { profile, problems } = resolvedProbe(flat);
283
+ expect(problems).toEqual([]);
284
+ expect(profile.lifecycle).toBe(profile.lifecycle); // kept as authored
285
+ expect(profile.properties[1].enumValues).toEqual(["draft", "done"]);
286
+ expect(validateTemplate(templateWith(flat)).errors).toEqual([]);
287
+ });
288
+
289
+ it("generates nothing for a malformed option list; the validator names it", () => {
290
+ const malformed = {
291
+ property: "status",
292
+ options: [{ key: "draft", category: "open" }, { category: "done" }],
293
+ };
294
+ const { profile } = resolvedProbe(malformed);
295
+ expect(profile.properties[1]).not.toHaveProperty("enumValues");
296
+ expect(
297
+ validateTemplate(templateWith(malformed)).errors.map((e) => e.rule)
298
+ ).toContain("lifecycle-property-unknown");
299
+ });
300
+
301
+ it("an adopts lifecycle naming no select property: no enumValues, the validator refuses it", () => {
302
+ const lifecycle = { property: "nope", adopts: "asset" };
303
+ const { profile, problems } = resolvedProbe(lifecycle);
304
+ expect(problems).toEqual([]);
305
+ expect(profile.properties[1]).not.toHaveProperty("enumValues");
306
+ expect(
307
+ validateTemplate(templateWith(lifecycle)).errors.map((e) => e.rule)
308
+ ).toEqual(["lifecycle-property-unknown"]);
309
+ });
310
+
311
+ it("keeps enumValues the YAML writes (the validator then requires them equal)", () => {
312
+ const { profile } = resolvedProbe(
313
+ { property: "status", adopts: "asset" },
314
+ { enumValues: ["draft"] }
315
+ );
316
+ expect(profile.properties[1].enumValues).toEqual(["draft"]);
317
+ const { errors } = validateTemplate(
318
+ templateWith(
319
+ { property: "status", adopts: "asset" },
320
+ { enumValues: ["draft"] }
321
+ )
322
+ );
323
+ expect(errors.map((e) => e.rule)).toEqual(["lifecycle-options-mismatch"]);
324
+ });
325
+
326
+ it.each([
327
+ [
328
+ { adopts: "contract" },
329
+ "profiles.probe.lifecycle.adopts",
330
+ /not a base lifecycle/,
331
+ ],
332
+ [
333
+ { adopts: "asset", options: [] },
334
+ "profiles.probe.lifecycle.options",
335
+ /adopts a base or lists its options/,
336
+ ],
337
+ [
338
+ { adopts: "asset", renames: {} },
339
+ "profiles.probe.lifecycle.renames",
340
+ /not a lifecycle overlay key/,
341
+ ],
342
+ [
343
+ { adopts: "asset", rename: { doing: "x" } },
344
+ "profiles.probe.lifecycle.rename.doing",
345
+ /not a step of the asset base/,
346
+ ],
347
+ [
348
+ { adopts: "asset", drop: ["on-hold"] },
349
+ "profiles.probe.lifecycle.drop[0]",
350
+ /not a step of the asset base/,
351
+ ],
352
+ [
353
+ { adopts: "asset", drop: ["approved"] },
354
+ "profiles.probe.lifecycle",
355
+ /at least one done step/,
356
+ ],
357
+ [
358
+ { adopts: "asset", rename: { draft: "approved" } },
359
+ "profiles.probe.lifecycle",
360
+ /appears twice/,
361
+ ],
362
+ [
363
+ {
364
+ adopts: "asset",
365
+ add: [{ key: "x", category: "active", after: "nope" }],
366
+ },
367
+ "profiles.probe.lifecycle.add[0].after",
368
+ /not a step/,
369
+ ],
370
+ [
371
+ { adopts: "asset", humanOnly: { nope: true } },
372
+ "profiles.probe.lifecycle.humanOnly.nope",
373
+ /not a step/,
374
+ ],
375
+ [
376
+ {
377
+ adopts: "asset",
378
+ add: [{ key: "x", category: "active", humanonly: true }],
379
+ },
380
+ "profiles.probe.lifecycle.add[0]",
381
+ /"humanonly" is not a step field/,
382
+ ],
383
+ [
384
+ {
385
+ adopts: "asset",
386
+ rename: { "in-review": "studied" },
387
+ drop: ["in-review"],
388
+ },
389
+ "profiles.probe.lifecycle.drop[0]",
390
+ /both renamed and dropped/,
391
+ ],
392
+ [
393
+ {
394
+ adopts: "asset",
395
+ add: [
396
+ { key: "x", category: "active", after: "draft", before: "approved" },
397
+ ],
398
+ },
399
+ "profiles.probe.lifecycle.add[0]",
400
+ /after OR before/,
401
+ ],
402
+ [
403
+ { adopts: "asset", add: [{ key: "approved", category: "active" }] },
404
+ "profiles.probe.lifecycle",
405
+ /"approved" appears twice/,
406
+ ],
407
+ [
408
+ { adopts: "asset", humanOnly: { draft: "yes" } },
409
+ "profiles.probe.lifecycle.humanOnly.draft",
410
+ /must be a boolean/,
411
+ ],
412
+ ])("reports %j", (overlay, path, message) => {
413
+ const lifecycle = { property: "status", ...overlay };
414
+ const { problems, profile } = resolvedProbe(lifecycle);
415
+ expect(problems.map((p) => p.path)).toContain(path);
416
+ expect(problems.find((p) => p.path === path)!.message).toMatch(message);
417
+ expect(profile.lifecycle).toEqual(lifecycle); // left as authored
418
+ const { ok, errors } = validateTemplate(templateWith(lifecycle));
419
+ expect(ok).toBe(false);
420
+ expect(
421
+ errors.some((e) => e.rule === "lifecycle-adopts" && e.path === path)
422
+ ).toBe(true);
423
+ });
424
+ });
425
+
426
+ describe("lifecycle presets — every door compiles authored YAML", () => {
427
+ const tpl = templateWith({
428
+ property: "status",
429
+ adopts: "publication",
430
+ drop: ["on-hold"],
431
+ });
432
+ const flat = BASE_LIFECYCLES.publication.filter((s) => s.key !== "on-hold");
433
+
434
+ it("validateTemplate accepts an adopts lifecycle with no enumValues", () => {
435
+ expect(validateTemplate(tpl).errors).toEqual([]);
436
+ });
437
+
438
+ it("both define converters carry the flat declaration and the generated enumValues", () => {
439
+ for (const profiles of [
440
+ toPackageDefinition(tpl).profiles,
441
+ toWorkspaceDefinition(tpl).definition.profiles,
442
+ ]) {
443
+ const p = (profiles as TemplateProfile[])[0];
444
+ expect(p.lifecycle).toEqual({ property: "status", options: flat });
445
+ expect(p.properties[1].enumValues).toEqual(flat.map((s) => s.key));
446
+ }
447
+ });
448
+
449
+ it("a define converter refuses an overlay that does not resolve", () => {
450
+ expect(() =>
451
+ toPackageDefinition(templateWith({ property: "status", adopts: "nope" }))
452
+ ).toThrow(/lifecycle presets do not resolve/);
453
+ });
454
+
455
+ it("the validator compiles a raw corpus too (no false lifecycle-diverges against a flat twin)", () => {
456
+ const foundation = authored("foundation");
457
+ expect(profileOf(foundation, "client").lifecycle).toHaveProperty("adopts");
458
+ const compiledFoundation = compiled.get("foundation")!;
459
+ const twin = {
460
+ ...compiledFoundation,
461
+ meta: { ...compiledFoundation.meta, slug: "foundation-twin" },
462
+ };
463
+ const diverges = validateTemplate(twin, {
464
+ corpus: [foundation, twin],
465
+ }).errors.filter((e) => e.rule === "lifecycle-diverges");
466
+ expect(diverges).toEqual([]);
467
+ });
468
+ });