@synap-core/workspace-templates 0.12.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/README.md +483 -0
  2. package/automations/bookmark-enrichment.automation.json +321 -0
  3. package/automations/github-pr-events.automation.json +505 -0
  4. package/automations/mail-feed.automation.json +86 -0
  5. package/automations/ship-a-change-checks-green.automation.json +106 -0
  6. package/automations/ship-a-change-checks-red.automation.json +106 -0
  7. package/automations/ship-a-change-deployed.automation.json +56 -0
  8. package/automations/ship-a-change-pr-merged.automation.json +110 -0
  9. package/automations/ship-a-change-pr-opened.automation.json +104 -0
  10. package/automations/url-bookmark-capture.automation.json +132 -0
  11. package/capabilities/agency-skills.capability.json +157 -0
  12. package/capabilities/apify.capability.json +265 -0
  13. package/capabilities/apollo.capability.json +120 -0
  14. package/capabilities/arch-backend.capability.json +97 -0
  15. package/capabilities/arch-client-intelligence.capability.json +401 -0
  16. package/capabilities/backblaze-b2.capability.json +205 -0
  17. package/capabilities/cal-com.capability.json +266 -0
  18. package/capabilities/canva.capability.json +274 -0
  19. package/capabilities/claude-code-routines.capability.json +172 -0
  20. package/capabilities/claude-managed-agents.capability.json +215 -0
  21. package/capabilities/client-comms-report.capability.json +246 -0
  22. package/capabilities/content-idea-to-production.capability.json +29 -0
  23. package/capabilities/datagouv.capability.json +315 -0
  24. package/capabilities/discord-bot.capability.json +157 -0
  25. package/capabilities/dropbox.capability.json +281 -0
  26. package/capabilities/exa.capability.json +109 -0
  27. package/capabilities/fal-ai.capability.json +83 -0
  28. package/capabilities/fal-media-lab.capability.json +158 -0
  29. package/capabilities/fireflies.capability.json +191 -0
  30. package/capabilities/freellmapi.capability.json +114 -0
  31. package/capabilities/generic-apikey.capability.json +92 -0
  32. package/capabilities/github-merge.capability.json +99 -0
  33. package/capabilities/github-read.capability.json +325 -0
  34. package/capabilities/governance-tighten-recommender.capability.json +81 -0
  35. package/capabilities/jev.capability.json +226 -0
  36. package/capabilities/lead-conversion-enrollment.capability.json +246 -0
  37. package/capabilities/mail-feed.capability.json +277 -0
  38. package/capabilities/mailgun.capability.json +52 -0
  39. package/capabilities/nango-google.capability.json +639 -0
  40. package/capabilities/notion.capability.json +417 -0
  41. package/capabilities/omniroute.capability.json +137 -0
  42. package/capabilities/posthog-analytics.capability.json +258 -0
  43. package/capabilities/postiz.capability.json +467 -0
  44. package/capabilities/proactive-digest.capability.json +190 -0
  45. package/capabilities/proton.capability.json +288 -0
  46. package/capabilities/replicate-rvm.capability.json +97 -0
  47. package/capabilities/research-methods.capability.json +57 -0
  48. package/capabilities/shopify.capability.json +395 -0
  49. package/capabilities/slack.capability.json +369 -0
  50. package/capabilities/stripe.capability.json +420 -0
  51. package/capabilities/telegram-bridge.capability.json +56 -0
  52. package/capabilities/treg.capability.json +102 -0
  53. package/capabilities/unipile-linkedin.capability.json +471 -0
  54. package/capabilities/vercel.capability.json +424 -0
  55. package/dist/define-CPS0n8BV.d.ts +1331 -0
  56. package/dist/index.d.ts +174 -900
  57. package/dist/index.js +26148 -23931
  58. package/dist/index.js.map +1 -1
  59. package/dist/wire.d.ts +222 -0
  60. package/dist/wire.js +77 -0
  61. package/dist/wire.js.map +1 -0
  62. package/package.json +27 -4
  63. package/packs/bookmark-views-pack.json +33 -0
  64. package/packs/crm-pack.json +76 -0
  65. package/packs/customer-profile.json +28 -0
  66. package/packs/deep-research.playbook.json +30 -0
  67. package/packs/event-views-pack.json +33 -0
  68. package/packs/habit-profile.json +27 -0
  69. package/packs/hr-pack.json +54 -0
  70. package/packs/invoice-profile.json +28 -0
  71. package/packs/knowledge-base-pack.json +56 -0
  72. package/packs/meeting-profile.json +29 -0
  73. package/packs/product-profile.json +30 -0
  74. package/packs/project-management-pack.json +49 -0
  75. package/packs/recipe-profile.json +29 -0
  76. package/packs/task-views-pack.json +39 -0
  77. package/retired.yaml +136 -0
  78. package/skills/app-factory-build-slice/SKILL.md +70 -0
  79. package/skills/app-factory-build-slice.catalog.json +17 -0
  80. package/skills/app-factory-design-canvas/SKILL.md +96 -0
  81. package/skills/app-factory-design-canvas.catalog.json +17 -0
  82. package/skills/app-factory-design-review/SKILL.md +99 -0
  83. package/skills/app-factory-design-review.catalog.json +18 -0
  84. package/skills/app-factory-research/SKILL.md +123 -0
  85. package/skills/app-factory-research.catalog.json +17 -0
  86. package/skills/bookmark-enrichment/SKILL.md +40 -0
  87. package/skills/carousel-format/carousel-format.skill.json +18 -0
  88. package/skills/collage-broll/collage-broll.skill.json +20 -0
  89. package/skills/content-taste/content-taste.skill.json +18 -0
  90. package/skills/creative-director/creative-director.skill.json +18 -0
  91. package/skills/design-critique/SKILL.md +135 -0
  92. package/skills/design-critique.catalog.json +12 -0
  93. package/skills/designing-with-real-components/SKILL.md +140 -0
  94. package/skills/designing-with-real-components.catalog.json +12 -0
  95. package/skills/edit-loop/edit-loop.skill.json +25 -0
  96. package/skills/facecam-edit/facecam-edit.skill.json +24 -0
  97. package/skills/mapping-the-server/SKILL.md +54 -0
  98. package/skills/math-explainer-manimgx/math-explainer-manimgx.skill.json +20 -0
  99. package/skills/motion-quality-gates/motion-quality-gates.skill.json +20 -0
  100. package/skills/open-design-edit/open-design-edit.skill.json +24 -0
  101. package/skills/reading-client-progress/SKILL.md +65 -0
  102. package/skills/routing-to-channels/SKILL.md +54 -0
  103. package/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
  104. package/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
  105. package/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
  106. package/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
  107. package/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
  108. package/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
  109. package/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
  110. package/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
  111. package/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
  112. package/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
  113. package/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
  114. package/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
  115. package/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
  116. package/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
  117. package/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
  118. package/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
  119. package/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
  120. package/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
  121. package/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
  122. package/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
  123. package/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
  124. package/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
  125. package/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
  126. package/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
  127. package/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
  128. package/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
  129. package/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
  130. package/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
  131. package/skills/video-direction/video-direction.skill.json +20 -0
  132. package/skills/video-style-vocabulary/video-style-vocabulary.skill.json +20 -0
  133. package/skills/video-toolbox/video-toolbox.skill.json +20 -0
  134. package/src/__fixtures__/lifecycle-presets.flat-before.json +205 -0
  135. package/src/agency-os.yaml +122 -0
  136. package/src/agent-os-skill-slugs.test.ts +65 -0
  137. package/src/apify-social-reader.test.ts +317 -0
  138. package/src/base.yaml +85 -39
  139. package/src/blockchain-ecosystem.yaml +1 -0
  140. package/src/brand-library.yaml +525 -14
  141. package/src/builder-dev-fold.template.test.ts +113 -0
  142. package/src/builder.yaml +779 -91
  143. package/src/business-model-catalog-description.test.ts +47 -0
  144. package/src/business-model-grp.template.test.ts +214 -0
  145. package/src/business-model.yaml +296 -49
  146. package/src/catalog-safety.test.ts +198 -0
  147. package/src/catalog.template-items.test.ts +75 -0
  148. package/src/catalog.ts +66 -2
  149. package/src/concept-guard.test.ts +28 -8
  150. package/src/concept-guard.ts +2 -2
  151. package/src/content-creation.template.test.ts +52 -0
  152. package/src/content-creation.yaml +192 -0
  153. package/src/content-os.edit-loop.template.test.ts +210 -0
  154. package/src/content-os.publish-due-posts.template.test.ts +261 -0
  155. package/src/content-os.template.test.ts +322 -0
  156. package/src/content-os.video-takes.template.test.ts +338 -0
  157. package/src/content-os.work-a-take.template.test.ts +125 -0
  158. package/src/content-os.yaml +1830 -39
  159. package/src/content-studio-retired.template.test.ts +82 -0
  160. package/src/corpus-flow-validity.test.ts +4 -5
  161. package/src/crm-absorbs-outreach.template.test.ts +117 -0
  162. package/src/crm-build-plan.template.test.ts +128 -0
  163. package/src/crm-lists.template.test.ts +96 -0
  164. package/src/crm-messages-cadence.template.test.ts +164 -0
  165. package/src/crm-one-send-door.template.test.ts +65 -0
  166. package/src/crm.yaml +1456 -181
  167. package/src/decisions-wave.template.test.ts +186 -0
  168. package/src/define.ts +130 -17
  169. package/src/design-to-implementation.template.test.ts +94 -0
  170. package/src/domain-template-type.test.ts +75 -0
  171. package/src/ecosystem.yaml +27 -66
  172. package/src/enterprise-os.yaml +10 -134
  173. package/src/facecam-edit.audio-studio.test.ts +188 -0
  174. package/src/fal-generate-image.test.ts +200 -0
  175. package/src/fal-media-lab-pod-files.test.ts +180 -0
  176. package/src/finance.yaml +254 -1
  177. package/src/foundation.yaml +78 -38
  178. package/src/frame-host-modules.d.mts +6 -0
  179. package/src/frame-host-modules.mjs +58 -0
  180. package/src/generate.mjs +9 -3
  181. package/src/github-merge.test.ts +210 -0
  182. package/src/grants.yaml +46 -17
  183. package/src/hr.yaml +39 -6
  184. package/src/index.ts +36 -1
  185. package/src/intent.test.ts +161 -0
  186. package/src/internal-runbook.yaml +19 -1
  187. package/src/jev.test.ts +356 -0
  188. package/src/legal.yaml +33 -1
  189. package/src/life-os.yaml +51 -240
  190. package/src/lifecycle-coverage.test.ts +124 -0
  191. package/src/lifecycle-declarations.test.ts +252 -0
  192. package/src/lifecycle-presets.test.ts +468 -0
  193. package/src/lifecycle-presets.ts +494 -0
  194. package/src/lifecycle-stuck.test.ts +46 -0
  195. package/src/lifecycle-waits.parity.test.ts +319 -0
  196. package/src/marketing.yaml +108 -4
  197. package/src/meetings-knowledge.yaml +18 -0
  198. package/src/networking.yaml +23 -2
  199. package/src/operations.yaml +304 -4
  200. package/src/pipeline-views.template.test.ts +88 -0
  201. package/src/playbook-goal-placeholders.test.ts +57 -0
  202. package/src/postiz-contract.test.ts +134 -0
  203. package/src/presentation-declarations.test.ts +174 -0
  204. package/src/process-contract.validate.test.ts +352 -0
  205. package/src/project-management.yaml +39 -740
  206. package/src/publish-body.d.mts +29 -0
  207. package/src/publish-body.mjs +74 -0
  208. package/src/publish-body.test.ts +74 -0
  209. package/src/publish-official-lib.test.ts +176 -0
  210. package/src/replicate-rvm.test.ts +504 -0
  211. package/src/research-base.yaml +119 -20
  212. package/src/research-decision.yaml +60 -3
  213. package/src/reserved-profile-slugs.test.ts +83 -0
  214. package/src/retired-goal-kinds.test.ts +58 -0
  215. package/src/role-reference-and-step-domain.test.ts +192 -0
  216. package/src/seed-obligation.test.ts +1 -6
  217. package/src/send-message-maps.test.ts +227 -0
  218. package/src/shared-role-ssot.test.ts +40 -2
  219. package/src/ship-a-change.template.test.ts +177 -0
  220. package/src/skills.test.ts +170 -0
  221. package/src/social.yaml +85 -27
  222. package/src/space-packages.freshness.test.ts +97 -0
  223. package/src/space-packages.generated.ts +40 -0
  224. package/src/space-packages.test.ts +125 -0
  225. package/src/space-packages.ts +73 -0
  226. package/src/start.test.ts +188 -0
  227. package/src/task-intents.test.ts +293 -0
  228. package/src/templates.ts +20851 -21496
  229. package/src/types.ts +443 -60
  230. package/src/unipile-publish-post.test.ts +162 -0
  231. package/src/validate-flow.drift.test.ts +2 -3
  232. package/src/validate-flow.ts +22 -2
  233. package/src/validate-onboarding.test.ts +59 -0
  234. package/src/validate.test.ts +71 -4
  235. package/src/validate.ts +1335 -7
  236. package/src/wire-core.ts +231 -0
  237. package/src/wire.test.ts +159 -0
  238. package/src/wire.ts +107 -0
  239. package/src/agent-fleet.yaml +0 -1645
  240. package/src/business-developer.template.test.ts +0 -351
  241. package/src/business-developer.yaml +0 -860
  242. package/src/content-studio.yaml +0 -361
  243. package/src/dev-dashboard.yaml +0 -990
  244. package/src/outreach-comms.yaml +0 -486
  245. package/src/the-arch.yaml +0 -201
@@ -0,0 +1,319 @@
1
+ /**
2
+ * PARITY — a template's lifecycle option `dateBinding` and step `waits` are
3
+ * accepted by the catalog validator EXACTLY when the pod accepts them.
4
+ *
5
+ * Three bindings, each against the source of the repo that owns the rule:
6
+ * 1. VOCABULARY: the validator's mirrors (`LIFECYCLE_WAITS_ON`,
7
+ * `LIFECYCLE_WAIT_KINDS`) equal `@synap-core/types` (`HOLD_ON` without
8
+ * `unspecified`, `WAIT_KINDS`), imported at run time.
9
+ * 2. FIELD SET: `LIFECYCLE_OPTION_FIELDS` / `LIFECYCLE_OPTION_WAITS_FIELDS`
10
+ * equal the keys of `interface LifecycleOption` / `LifecycleOptionWaits` in
11
+ * synap-backend `packages/types/src/lifecycle/option.ts`, and the pod's
12
+ * parser carries every one of them through.
13
+ * 3. BEHAVIOUR: synap-backend's OWN parser (`parseLifecycleOptions`,
14
+ * `packages/database/src/utils/lifecycle-model.ts`, compiled here with
15
+ * esbuild and run against the types this package resolves) and
16
+ * `validateTemplate` give the same verdict on every row of a fixture table.
17
+ * A convergence check proves sameness, not correctness, so every row also
18
+ * names the verdict the rule should give (`ok`), read from the pod's rules,
19
+ * and both sides are held to it.
20
+ *
21
+ * The rows are chosen where a plausible wrong rule DISAGREES: `unspecified`
22
+ * (a validator that takes all of `HOLD_ON`), the 64/65-character slug (an
23
+ * off-by-one length), a done or lost step (a validator that forgets the
24
+ * category), an unknown `waits` field (a validator that ignores it), `null`
25
+ * (a validator that treats null as absent).
26
+ *
27
+ * NOT covered (stated, not implied): unknown TOP-LEVEL option fields (the pod
28
+ * ignores them and so does the validator; no row asserts it), `humanOnly`
29
+ * and `stuckAfterDays` (pinned by `lifecycle-stuck.test.ts`), and whether the
30
+ * named property exists on the kind (the pod does not check it either). The
31
+ * behaviour half skips, with the reason logged, when synap-backend is not a
32
+ * sibling checkout.
33
+ */
34
+ import { existsSync, readFileSync } from "node:fs";
35
+ import { dirname, resolve } from "node:path";
36
+ import { fileURLToPath } from "node:url";
37
+ import { transformSync } from "esbuild";
38
+ import { describe, expect, it } from "vitest";
39
+ import * as typesLifecycle from "@synap-core/types/lifecycle";
40
+ import * as typesUnits from "@synap-core/types/units";
41
+ import { stripComments } from "../../design-tokens/scripts/strip-comments.mjs";
42
+ import {
43
+ LIFECYCLE_OPTION_FIELDS,
44
+ LIFECYCLE_OPTION_WAITS_FIELDS,
45
+ LIFECYCLE_WAIT_KINDS,
46
+ LIFECYCLE_WAITS_ON,
47
+ validateTemplate,
48
+ } from "./validate.js";
49
+ import { resolveLifecyclePresets } from "./lifecycle-presets.js";
50
+ import type { WorkspaceYaml } from "./types.js";
51
+ import { readYamlTemplates } from "./generate.mjs";
52
+
53
+ const HERE = dirname(fileURLToPath(import.meta.url));
54
+ const BE = resolve(HERE, "../../../../synap-backend/packages");
55
+ const OPTION_TS = resolve(BE, "types/src/lifecycle/option.ts");
56
+ const MODEL_TS = resolve(BE, "database/src/utils/lifecycle-model.ts");
57
+ const DID_YOU_MEAN_TS = resolve(BE, "database/src/services/did-you-mean.ts");
58
+ const hasBackend = existsSync(OPTION_TS) && existsSync(MODEL_TS) && existsSync(DID_YOU_MEAN_TS);
59
+ if (!hasBackend) console.warn(`[lifecycle-waits parity] SKIP the backend half: ${MODEL_TS} or ${OPTION_TS} is absent`);
60
+
61
+ describe("lifecycle waits parity — the vocabulary", () => {
62
+ it("waits.on is HOLD_ON without unspecified", () => {
63
+ expect([...LIFECYCLE_WAITS_ON]).toEqual(typesLifecycle.HOLD_ON.filter((on) => on !== "unspecified"));
64
+ });
65
+ it("waits.waitKind is WAIT_KINDS", () => {
66
+ expect([...LIFECYCLE_WAIT_KINDS]).toEqual([...typesUnits.WAIT_KINDS]);
67
+ });
68
+ });
69
+
70
+ /** The keys of `export interface <name> {…}` (no nested braces in these two). */
71
+ function interfaceKeys(src: string, name: string): string[] {
72
+ const code = stripComments(src, "ts");
73
+ const at = code.indexOf(`export interface ${name} {`);
74
+ if (at < 0) throw new Error(`interface ${name} not found`);
75
+ const body = code.slice(code.indexOf("{", at) + 1, code.indexOf("}", at));
76
+ return [...body.matchAll(/^\s*([A-Za-z_]\w*)\??\s*:/gm)].map((m) => m[1]!);
77
+ }
78
+
79
+ type ParseLifecycleOptions = (raw: unknown) => { options: Array<Record<string, unknown>>; errors: string[] };
80
+
81
+ /**
82
+ * The pod's parser, compiled from its source and run against the types this
83
+ * package resolves. Every import it makes must be provided here: a new one
84
+ * throws, naming it, instead of being stubbed silently.
85
+ */
86
+ function loadBackendParser(): ParseLifecycleOptions {
87
+ const load = (file: string, deps: Record<string, unknown>): Record<string, unknown> => {
88
+ const { code } = transformSync(readFileSync(file, "utf8"), { loader: "ts", format: "cjs", target: "es2022" });
89
+ const mod = { exports: {} as Record<string, unknown> };
90
+ const req = (id: string) => {
91
+ if (Object.hasOwn(deps, id)) return deps[id];
92
+ throw new Error(`[parity] ${file} imports "${id}", which this harness does not provide`);
93
+ };
94
+ new Function("require", "module", "exports", code)(req, mod, mod.exports);
95
+ return mod.exports;
96
+ };
97
+ const didYouMean = load(DID_YOU_MEAN_TS, {});
98
+ const model = load(MODEL_TS, {
99
+ "@synap-core/types/lifecycle": typesLifecycle,
100
+ "@synap-core/types/units": typesUnits,
101
+ "../services/did-you-mean.js": didYouMean,
102
+ });
103
+ if (typeof model.parseLifecycleOptions !== "function")
104
+ throw new Error("[parity] lifecycle-model.ts no longer exports parseLifecycleOptions");
105
+ return model.parseLifecycleOptions as ParseLifecycleOptions;
106
+ }
107
+
108
+ describe.skipIf(!hasBackend)("lifecycle waits parity — the field set", () => {
109
+ it("the option and waits fields are the types' interfaces", () => {
110
+ const src = readFileSync(OPTION_TS, "utf8");
111
+ const option = interfaceKeys(src, "LifecycleOption");
112
+ const waits = interfaceKeys(src, "LifecycleOptionWaits");
113
+ // non-vacuity: the extraction still sees fields it must
114
+ expect(option).toContain("stuckAfterDays");
115
+ expect(waits).toContain("on");
116
+ expect([...option].sort()).toEqual([...LIFECYCLE_OPTION_FIELDS].sort());
117
+ expect([...waits].sort()).toEqual([...LIFECYCLE_OPTION_WAITS_FIELDS].sort());
118
+ });
119
+
120
+ it("the pod's parser carries every field the validator names", () => {
121
+ const parse = loadBackendParser();
122
+ const full = {
123
+ key: "sent",
124
+ label: "Sent",
125
+ category: "active",
126
+ tone: "info",
127
+ humanOnly: true,
128
+ stuckAfterDays: 7,
129
+ dateBinding: "dueDate",
130
+ waits: { on: "external", waitKind: "payment", refFrom: "clientId", dateBinding: "dueDate" },
131
+ };
132
+ const { options, errors } = parse([full]);
133
+ expect(errors).toEqual([]);
134
+ expect(Object.keys(options[0]!).sort()).toEqual([...LIFECYCLE_OPTION_FIELDS].sort());
135
+ expect(Object.keys(options[0]!.waits as object).sort()).toEqual([...LIFECYCLE_OPTION_WAITS_FIELDS].sort());
136
+ });
137
+ });
138
+
139
+ // ── the behaviour table ─────────────────────────────────────────────────────
140
+
141
+ const crm = readYamlTemplates()
142
+ .map((t) => t.data as WorkspaceYaml)
143
+ .find((t) => t.meta.slug === "crm")!;
144
+
145
+ /** crm's deal lifecycle: draft (open) · proposed, negotiating (active) · won (done) · lost (lost). */
146
+ function dealWith(key: string, extra: Record<string, unknown>): WorkspaceYaml {
147
+ const clone = structuredClone(crm);
148
+ const deal = clone.profiles.find((p) => p.slug === "deal")!;
149
+ deal.lifecycle!.options = deal.lifecycle!.options.map((o) => (o.key === key ? { ...o, ...extra } : o));
150
+ return clone;
151
+ }
152
+
153
+ const waitsErrors = (t: WorkspaceYaml) =>
154
+ validateTemplate(t).errors.filter((e) => /\.options\[\d+\]\.(dateBinding|waits)$/.test(e.path));
155
+
156
+ const SLUG_64 = `a${"b".repeat(63)}`;
157
+ const SLUG_65 = `a${"b".repeat(64)}`;
158
+
159
+ type Row = { name: string; key: string; extra: Record<string, unknown>; ok: boolean };
160
+ const ROWS: Row[] = [
161
+ { name: "dateBinding on an active step", key: "proposed", extra: { dateBinding: "dueDate" }, ok: true },
162
+ { name: "dateBinding on an open step, - and _", key: "draft", extra: { dateBinding: "next_action-at" }, ok: true },
163
+ { name: "dateBinding of 64 characters", key: "proposed", extra: { dateBinding: SLUG_64 }, ok: true },
164
+ { name: "dateBinding of 65 characters", key: "proposed", extra: { dateBinding: SLUG_65 }, ok: false },
165
+ { name: "dateBinding with a space", key: "proposed", extra: { dateBinding: "due date" }, ok: false },
166
+ { name: "dateBinding starting with a digit", key: "proposed", extra: { dateBinding: "1dueDate" }, ok: false },
167
+ { name: "dateBinding that is a number", key: "proposed", extra: { dateBinding: 42 }, ok: false },
168
+ { name: "dateBinding null", key: "proposed", extra: { dateBinding: null }, ok: false },
169
+ { name: "dateBinding on a done step", key: "won", extra: { dateBinding: "dueDate" }, ok: false },
170
+ { name: "waits on an active step", key: "proposed", extra: { waits: { on: "external" } }, ok: true },
171
+ {
172
+ name: "waits with every field",
173
+ key: "negotiating",
174
+ extra: { waits: { on: "nobody", waitKind: "payment", refFrom: "contactId", dateBinding: "nextActionAt" } },
175
+ ok: true,
176
+ },
177
+ { name: "waits on an open step", key: "draft", extra: { waits: { on: "date", dateBinding: "nextActionAt" } }, ok: true },
178
+ { name: "waits.on unspecified", key: "proposed", extra: { waits: { on: "unspecified" } }, ok: false },
179
+ { name: "waits.on unknown", key: "proposed", extra: { waits: { on: "client" } }, ok: false },
180
+ { name: "waits without on", key: "proposed", extra: { waits: { waitKind: "payment" } }, ok: false },
181
+ { name: "waits.waitKind unknown", key: "proposed", extra: { waits: { on: "person", waitKind: "bogus" } }, ok: false },
182
+ { name: "waits.refFrom with a space", key: "proposed", extra: { waits: { on: "person", refFrom: "contact id" } }, ok: false },
183
+ { name: "waits.dateBinding a number", key: "proposed", extra: { waits: { on: "date", dateBinding: 3 } }, ok: false },
184
+ { name: "waits with an unknown field", key: "proposed", extra: { waits: { on: "person", who: "x" } }, ok: false },
185
+ { name: "waits on a lost step", key: "lost", extra: { waits: { on: "person" } }, ok: false },
186
+ { name: "waits as a list", key: "proposed", extra: { waits: [] }, ok: false },
187
+ { name: "waits as a word", key: "proposed", extra: { waits: "external" }, ok: false },
188
+ { name: "waits null", key: "proposed", extra: { waits: null }, ok: false },
189
+ ];
190
+
191
+ describe("lifecycle waits parity — the table is discriminating", () => {
192
+ it("non-vacuity: the table holds accepted and refused rows of both fields, on every category", () => {
193
+ for (const field of ["dateBinding", "waits"]) {
194
+ const rows = ROWS.filter((r) => field in r.extra);
195
+ expect(rows.some((r) => r.ok), field).toBe(true);
196
+ expect(rows.some((r) => !r.ok), field).toBe(true);
197
+ }
198
+ expect(new Set(ROWS.map((r) => r.key))).toEqual(new Set(["draft", "proposed", "negotiating", "won", "lost"]));
199
+ });
200
+
201
+ it("the unmodified crm template has no waits errors (the clone is a clean base)", () => {
202
+ expect(waitsErrors(structuredClone(crm))).toEqual([]);
203
+ });
204
+ });
205
+
206
+ describe("lifecycle waits parity — the validator gives each row's verdict", () => {
207
+ it.each(ROWS)("$name → ok=$ok", ({ key, extra, ok }) => {
208
+ const errs = waitsErrors(dealWith(key, extra));
209
+ expect(errs.length === 0, errs.map((e) => e.message).join("; ")).toBe(ok);
210
+ });
211
+ });
212
+
213
+ describe.skipIf(!hasBackend)("lifecycle waits parity — the pod's parser gives the same verdict", () => {
214
+ const parse = hasBackend ? loadBackendParser() : null;
215
+ const dealOption = (key: string) => crm.profiles.find((p) => p.slug === "deal")!.lifecycle!.options.find((o) => o.key === key)!;
216
+
217
+ it.each(ROWS)("$name → ok=$ok", ({ key, extra, ok }) => {
218
+ const option = { ...dealOption(key), ...extra };
219
+ const { options, errors } = parse!([option]);
220
+ expect(errors.length === 0, errors.join("; ")).toBe(ok);
221
+ if (ok) {
222
+ // accepted means CARRIED, never silently dropped
223
+ for (const [field, value] of Object.entries(extra)) expect(options[0]![field]).toEqual(value);
224
+ }
225
+ });
226
+
227
+ it("the same words: a refused row says what the pod would say", () => {
228
+ for (const row of ROWS.filter((r) => !r.ok)) {
229
+ const option = { ...dealOption(row.key), ...row.extra };
230
+ const pod = parse!([option]).errors;
231
+ const ours = waitsErrors(dealWith(row.key, row.extra)).map((e) => e.message);
232
+ expect(ours, row.name).toEqual(pod);
233
+ }
234
+ });
235
+ });
236
+
237
+ // ── presets carry them ──────────────────────────────────────────────────────
238
+
239
+ describe("lifecycle presets carry dateBinding and waits", () => {
240
+ /** crm, plus a kind `invoice` declaring `lifecycle` (a real template, so the validator runs whole). */
241
+ const withLifecycle = (lifecycle: Record<string, unknown>): WorkspaceYaml => {
242
+ const clone = structuredClone(crm);
243
+ clone.profiles.push({
244
+ slug: "invoice",
245
+ displayName: "Invoice",
246
+ properties: [
247
+ { slug: "invoice-status", label: "Status", valueType: "string", inputType: "select" },
248
+ { slug: "dueDate", label: "Due date", valueType: "date", inputType: "date" },
249
+ ],
250
+ lifecycle,
251
+ } as unknown as WorkspaceYaml["profiles"][number]);
252
+ return clone;
253
+ };
254
+ const resolved = (lifecycle: Record<string, unknown>) => {
255
+ const { template, problems } = resolveLifecyclePresets(withLifecycle(lifecycle));
256
+ const invoice = template.profiles.find((p) => p.slug === "invoice")!;
257
+ return { options: invoice.lifecycle!.options, problems };
258
+ };
259
+
260
+ it("per resulting key, and on an added step, as given", () => {
261
+ const waits = { on: "external", waitKind: "payment" };
262
+ const { options, problems } = resolved({
263
+ property: "invoice-status",
264
+ adopts: "agreement",
265
+ dateBinding: { sent: "dueDate" },
266
+ waits: { sent: waits },
267
+ add: [{ key: "reminded", category: "active", after: "sent", waits: { on: "date", dateBinding: "dueDate" } }],
268
+ });
269
+ expect(problems).toEqual([]);
270
+ const sent = options.find((o) => o.key === "sent")!;
271
+ expect(sent.dateBinding).toBe("dueDate");
272
+ expect(sent.waits).toEqual(waits);
273
+ expect(sent.stuckAfterDays).toBe(7);
274
+ expect(options.find((o) => o.key === "reminded")!.waits).toEqual({ on: "date", dateBinding: "dueDate" });
275
+ // a step the overlay did not name carries neither
276
+ expect("waits" in options.find((o) => o.key === "draft")!).toBe(false);
277
+ });
278
+
279
+ it("a flat lifecycle keeps them untouched", () => {
280
+ const flat = {
281
+ property: "invoice-status",
282
+ options: [
283
+ { key: "draft", category: "open" },
284
+ { key: "sent", category: "active", dateBinding: "dueDate", waits: { on: "external" } },
285
+ { key: "paid", category: "done" },
286
+ ],
287
+ };
288
+ expect(resolved(flat).options[1]).toEqual(flat.options[1]);
289
+ });
290
+
291
+ it("refuses an override of the wrong type, and an unknown step", () => {
292
+ const { problems } = resolved({
293
+ property: "invoice-status",
294
+ adopts: "agreement",
295
+ dateBinding: { sent: 3, nope: "dueDate" },
296
+ waits: { sent: "external" },
297
+ });
298
+ expect(problems.map((p) => p.path)).toEqual([
299
+ "profiles.invoice.lifecycle.dateBinding.sent",
300
+ "profiles.invoice.lifecycle.dateBinding.nope",
301
+ "profiles.invoice.lifecycle.waits.sent",
302
+ ]);
303
+ });
304
+
305
+ it("the validator judges what the presets carried", () => {
306
+ const tpl = withLifecycle({
307
+ property: "invoice-status",
308
+ adopts: "agreement",
309
+ waits: { sent: { on: "unspecified" }, fulfilled: { on: "person" } },
310
+ });
311
+ const errs = validateTemplate(tpl).errors.filter(
312
+ (e) => e.path.startsWith("profiles.invoice.") && e.path.endsWith(".waits")
313
+ );
314
+ expect(errs.map((e) => e.message)).toEqual([
315
+ 'option "sent" waits.on must be one of person | agent | external | thing | decision | date | nobody',
316
+ 'option "fulfilled" is done: a finished thing waits on nothing (waits is for open, active or paused options)',
317
+ ]);
318
+ });
319
+ });
@@ -10,6 +10,11 @@ meta:
10
10
  color: "#E91E8C"
11
11
  tags: [marketing, crm, outreach, leads, campaigns]
12
12
  isPublic: true
13
+ # What this space PRODUCES (homes): the purpose word, then its kinds/roles.
14
+ intent:
15
+ label: Marketing
16
+ produces:
17
+ - profile: campaign
13
18
 
14
19
  # SSOT ordering guarantee — this template REFERENCES foundation's pod-wide
15
20
  # shared `lead` role. The apply engine seeds a shared profile's pod-wide BASE
@@ -47,6 +52,15 @@ profiles:
47
52
  color: pink
48
53
  description: "A marketing campaign — email, social, event, content, or partnership"
49
54
  semanticSlug: marketing.campaign
55
+ # The ONE lifecycle (work model L1): every option's category.
56
+ lifecycle:
57
+ property: campaign-status
58
+ options:
59
+ - { key: planning, category: open }
60
+ - { key: active, category: active }
61
+ - { key: paused, category: paused, humanOnly: true }
62
+ - { key: completed, category: done }
63
+ - { key: cancelled, category: lost, humanOnly: true }
50
64
  properties:
51
65
  - slug: campaign-status
52
66
  label: Status
@@ -96,7 +110,7 @@ profiles:
96
110
  # `campaign-channel` (the delivery medium). scoping/outreach/live have no
97
111
  # channel equivalent, so folding them into `campaign-channel` would lose
98
112
  # them — they get their own def. Kept verbatim across the three campaign
99
- # declarations (business-developer / content-os / here) so there is no drift.
113
+ # declarations (crm / content-os / here) so there is no drift.
100
114
  - slug: campaign-type
101
115
  label: Type
102
116
  valueType: string
@@ -256,10 +270,13 @@ views:
256
270
  description: "Marketing contacts in a table."
257
271
  - name: Lead Pipeline
258
272
  slug: lead-pipeline
259
- type: kanban
273
+ # The ONE board (work model L3, D3): columns ARE the lead role's declared
274
+ # lifecycle (foundation's leadStage), moves go through the one mover.
275
+ type: pipeline
260
276
  scopeProfileSlug: lead
261
- groupBy: leadStage
262
- description: "Lead pipeline kanban by stage."
277
+ config:
278
+ profileSlug: lead
279
+ description: "Leads in the columns of their stage."
263
280
 
264
281
  onboarding:
265
282
  goal: >
@@ -311,10 +328,12 @@ onboarding:
311
328
  leaking, and where the next dollar should go.
312
329
  collect:
313
330
  - profileSlug: campaign
331
+ min: 1
314
332
  what: The campaigns live or planned right now — one per real initiative, with its channel and goal.
315
333
  cardinality: several
316
334
  keyFields: [campaign-channel, campaign-goal, campaign-status]
317
335
  - profileSlug: lead
336
+ min: 1
318
337
  what: A few representative leads already in motion, placed at their true pipeline stage.
319
338
  cardinality: several
320
339
  keyFields: [leadStage, lead-source, lead-company]
@@ -362,3 +381,88 @@ bento:
362
381
  - kind: view
363
382
  viewName: Lead Pipeline
364
383
  pos: { x: 6, y: 0, w: 6, h: 5 }
384
+
385
+ playbooks:
386
+ - name: Run a campaign
387
+ description: Take a campaign from brief to results — goal, audience, plan, content, launch with a person's approval, and a report on the leads it brought.
388
+ goalTemplate: >-
389
+ Run this campaign ({campaign}); each stage moves its status. BRIEF: write
390
+ the campaign's goal as a number, its target audience, channel, budget,
391
+ start and end dates, from the brand kit and the agency's offers. PLAN:
392
+ list the pieces to make and the outreach to send, each with a date. MAKE:
393
+ create each piece as a post at the Idea status in Content OS and each
394
+ outreach sequence as an outreach campaign, so their own processes
395
+ produce them. LAUNCH: a person approves the launch; nothing is published,
396
+ sent or paid for before that. REPORT: after the end date, record the lead
397
+ count and conversion rate, link every new lead the campaign brought, and
398
+ write what worked and what to change. Pausing or cancelling is a
399
+ person's call.
400
+ subjectProfile:
401
+ profileSlug: campaign
402
+ statusProperty: campaign-status
403
+ humanOnlyStatuses: [paused, cancelled]
404
+ activators:
405
+ - { on: created, mode: propose }
406
+ params:
407
+ - { name: campaign, type: entity, label: Campaign, required: true }
408
+ executor: is-agent
409
+ status: active
410
+ requiredIntents: [publish_post, send_message, list_records]
411
+ expectedOutputs:
412
+ - { kind: post, label: Campaign pieces, relationToSubject: relates_to }
413
+ - { kind: person, label: Leads from the campaign, relationToSubject: relates_to }
414
+ - { kind: document, label: Campaign report }
415
+ stages:
416
+ - key: brief
417
+ name: Brief
418
+ category: planned
419
+ subjectStatus: planning
420
+ goal: Write the goal as a number, the audience, channel, budget and dates.
421
+ criteria:
422
+ - key: campaign-goal-number
423
+ statement: The campaign's goal is a number with a unit (leads, sign-ups, revenue).
424
+ check: { kind: judge }
425
+ - key: campaign-audience
426
+ statement: The target audience is written and matches an audience in Foundation.
427
+ check: { kind: judge }
428
+ - key: campaign-dates-budget
429
+ statement: The start date, end date and budget are set.
430
+ check: { kind: judge }
431
+ - key: plan
432
+ name: Plan
433
+ category: planned
434
+ goal: List every piece and outreach to send, each with a date.
435
+ gate: { kind: check }
436
+ onFail: { toStage: brief }
437
+ criteria:
438
+ - key: campaign-plan-dated
439
+ statement: Every planned piece and outreach has a date inside the campaign window.
440
+ check: { kind: judge }
441
+ - key: make
442
+ name: Make
443
+ category: started
444
+ goal: Create each piece as a post at Idea and each outreach sequence as an outreach campaign.
445
+ criteria:
446
+ - key: campaign-pieces-created
447
+ statement: Every planned piece exists as a post or an outreach campaign linked to this campaign.
448
+ check: { kind: judge }
449
+ - key: launch
450
+ name: Launch
451
+ category: started
452
+ subjectStatus: active
453
+ goal: A person approves the launch; then the pieces go out on their dates.
454
+ gate: { kind: human }
455
+ onFail: { toStage: make }
456
+ indefinite: true
457
+ - key: report
458
+ name: Report
459
+ category: completed
460
+ subjectStatus: completed
461
+ goal: Record lead count and conversion rate, link the leads, and write what to change.
462
+ criteria:
463
+ - key: campaign-results
464
+ statement: The lead count and conversion rate are set.
465
+ check: { kind: judge }
466
+ - key: campaign-learnings
467
+ statement: The report says what worked and what to change next time.
468
+ check: { kind: judge }
@@ -13,6 +13,9 @@ meta:
13
13
  color: "#8B5CF6"
14
14
  tags: [meetings, notes, tasks, transcripts, calendar]
15
15
  isPublic: true
16
+ # A LIBRARY: it produces nothing a home counts (homes).
17
+ intent:
18
+ label: Meetings
16
19
 
17
20
  workspace:
18
21
  name: Meetings & Knowledge
@@ -226,6 +229,21 @@ integrations:
226
229
  - templateKey: fireflies
227
230
  - templateKey: nango-google
228
231
 
232
+ # ── taskIntents — WHAT THIS WORKSPACE NEEDS TO FUNCTION ───────────────────
233
+ # Closed vocabulary: `@synap-core/types/capability-intents` (pod migrations
234
+ # 0283/0284). DERIVED from the declared intents of this template's own
235
+ # `integrations` capability packs — not hand-guessed. Checked at authoring
236
+ # time by `validateTemplate` (`task-intent-unknown`) and by
237
+ # `src/task-intents.test.ts`.
238
+ taskIntents:
239
+ - capture_into_pod
240
+ - search_external
241
+ - fetch_record
242
+ - list_records
243
+ - send_message
244
+ - schedule_event
245
+ - manage_file
246
+
229
247
  onboarding:
230
248
  goal: >
231
249
  Get recent meetings into the pod so summaries, action items, and attendees
@@ -77,6 +77,15 @@ profiles:
77
77
  color: amber
78
78
  description: A client's stated ask — the demand The Arch brokers a provider against.
79
79
  scope: WORKSPACE
80
+ # The ONE lifecycle (work model L1): every option's category.
81
+ lifecycle:
82
+ property: needStatus
83
+ options:
84
+ - { key: open, category: open }
85
+ - { key: matching, category: active }
86
+ - { key: intro-made, category: active }
87
+ - { key: satisfied, category: done }
88
+ - { key: dropped, category: lost }
80
89
  properties:
81
90
  - slug: needStatus
82
91
  label: Status
@@ -133,6 +142,17 @@ profiles:
133
142
  color: violet
134
143
  description: The middleman brokerage artifact — connects a requester to a provider for a need, and bills the match as a deal.
135
144
  scope: WORKSPACE
145
+ # The ONE lifecycle (work model L1): every option's category.
146
+ lifecycle:
147
+ property: introStage
148
+ options:
149
+ - { key: identified, category: open }
150
+ - { key: proposed, category: active }
151
+ - { key: approved, category: active }
152
+ - { key: introduced, category: active }
153
+ - { key: in-discussion, category: active }
154
+ - { key: signed, category: done }
155
+ - { key: dead, category: lost }
136
156
  properties:
137
157
  - slug: introStage
138
158
  label: Stage
@@ -155,8 +175,9 @@ profiles:
155
175
  valueType: entity_id
156
176
  inputType: entity_select
157
177
  targetProfileSlug: need
158
- # ── Fee vocabulary — REUSED VERBATIM from business-developer.yaml's `deal`
159
- # commercial contract, so an intro bills exactly like a CRM deal does. ──
178
+ # ── Fee vocabulary — REUSED VERBATIM from the `deal` overlay of the retired
179
+ # arch-sales (retired-sources/arch-sales.yaml), so an intro bills exactly
180
+ # like a CRM deal did. ──
160
181
  - slug: synapFeeModel
161
182
  label: Synap fee model
162
183
  valueType: string