@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,29 @@
1
+ /** Types for `publish-body.mjs` — the official-template publish body builder. */
2
+ import type { PackageDefinitionOutput } from './define';
3
+
4
+ export interface OfficialPublishBody {
5
+ slug: string;
6
+ displayName: string;
7
+ description?: string;
8
+ isPublic: boolean;
9
+ tags: string[];
10
+ category: 'workspace';
11
+ requiredTier?: string;
12
+ vendorId: string;
13
+ /** `toPackageDefinition()` minus `_meta`, plus `_meta.icon`/`color`/`domain` mirrored on top. */
14
+ definition: Omit<PackageDefinitionOutput, '_meta'> & {
15
+ icon?: string;
16
+ color?: string;
17
+ domain?: string;
18
+ /** Which build of this package generated the row — the catalog winner signal. */
19
+ sourcePackage: { name: string; version: string };
20
+ };
21
+ }
22
+
23
+ /** This package's own `{ name, version }` (from its package.json). */
24
+ export declare const OFFICIAL_SOURCE_PACKAGE: Readonly<{ name: string; version: string }>;
25
+
26
+ export declare function toOfficialPublishBody(
27
+ pd: PackageDefinitionOutput,
28
+ vendorId: string,
29
+ ): OfficialPublishBody;
@@ -0,0 +1,74 @@
1
+ import { readFileSync } from 'node:fs';
2
+
3
+ /**
4
+ * Provenance every official row carries: WHICH build of this package generated
5
+ * it. Read from this package's own `package.json`, so a caller can never forget
6
+ * or misstate it.
7
+ *
8
+ * WHY: clients (CLI, browser) merge the catalog with their own frozen bundle
9
+ * through `mergeCatalog`, where the bundle wins unless the remote row's
10
+ * `definition.sourcePackage.version` is STRICTLY newer. A row with no
11
+ * `sourcePackage` therefore lost to every stale client bundle — the CLI then
12
+ * installed its old copy of content-os (no `primarySurface`) onto a live space.
13
+ * Bump this package's version whenever template content changes, so a fresh
14
+ * official row outranks every older bundle (`synap-market` skill).
15
+ *
16
+ * The CP excludes `sourcePackage` from the content hash (`templateContentHash`),
17
+ * so a version bump alone never changes a row's `h-` version.
18
+ */
19
+ export const OFFICIAL_SOURCE_PACKAGE = Object.freeze(
20
+ (() => {
21
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
22
+ if (!pkg.name || !pkg.version) throw new Error('workspace-templates package.json has no name/version');
23
+ return { name: pkg.name, version: pkg.version };
24
+ })(),
25
+ );
26
+
27
+ /**
28
+ * The ONE builder of the `POST {CP}/api/packages` body for an official
29
+ * workspace template. `scripts/publish-official-lib.mjs` (the ONE official-publish
30
+ * implementation, behind `synap market publish --official` and the CI wrapper
31
+ * `scripts/publish-official.mjs`) calls it; `publish-body.test.ts` drives it from the REAL `toPackageDefinition`
32
+ * output so the wire shape is tested, not re-typed.
33
+ *
34
+ * WHY icon/color/domain ARE MIRRORED ONTO `definition`
35
+ * ====================================================
36
+ * `toPackageDefinition` carries the template's identity (icon, colour, domain)
37
+ * ONLY in `_meta`, and `_meta` is hoisted to row columns here (slug, name,
38
+ * tags…), so it does not travel inside `definition`. The CP door reads the
39
+ * row's `icon`/`domain` columns and the stored `definition.icon`/`.color` from
40
+ * the definition's TOP-LEVEL fields (`publishPackageCore` +
41
+ * `normalizePublishedDefinition`); with nothing there it fell back to
42
+ * `icon: "layers"` / `color: "#6366F1"` and a NULL icon column — which is how
43
+ * 26/28 official catalog rows lost their own icon and colour. The CLI
44
+ * (`synap-cli/src/lib/cp-packages.ts`) already mirrors them; this is the same
45
+ * shape.
46
+ *
47
+ * @param {import('./define').PackageDefinitionOutput} pd `toPackageDefinition()` output
48
+ * @param {string} vendorId the `synap-official` vendor id
49
+ */
50
+ export function toOfficialPublishBody(pd, vendorId) {
51
+ const { _meta, ...rest } = pd;
52
+ const definition = {
53
+ ...rest,
54
+ ...(_meta.icon !== undefined ? { icon: _meta.icon } : {}),
55
+ ...(_meta.color !== undefined ? { color: _meta.color } : {}),
56
+ ...(_meta.domain !== undefined ? { domain: _meta.domain } : {}),
57
+ sourcePackage: { ...OFFICIAL_SOURCE_PACKAGE },
58
+ };
59
+ return {
60
+ slug: _meta.slug,
61
+ // The package's own name — an overlay's workspaceName is its TARGET's name
62
+ // (business-model composes onto Foundation/Strategy and was listed as "Foundation").
63
+ displayName: _meta.name ?? pd.workspaceName,
64
+ // Same reason: the catalog lists the PACKAGE's description (meta), not the
65
+ // overlay target's workspace description ("Adds … to Foundation").
66
+ description: _meta.description ?? pd.description,
67
+ isPublic: _meta.isPublic ?? true,
68
+ tags: _meta.tags ?? [],
69
+ category: 'workspace',
70
+ ...(_meta.requiredTier ? { requiredTier: _meta.requiredTier } : {}),
71
+ vendorId,
72
+ definition,
73
+ };
74
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The official publish body carries each template's OWN identity.
3
+ *
4
+ * Defect this guards: `publish-official.mjs` posted `toPackageDefinition()`
5
+ * minus `_meta` with nothing mirrored, so the CP door found no top-level
6
+ * `icon`/`color` and stored `icon: "layers"` / `color: "#6366F1"` with a NULL
7
+ * icon column — 26/28 official catalog rows lost their icon and colour.
8
+ *
9
+ * Driven from the REAL corpus (every `src/*.yaml`) through the REAL
10
+ * `toPackageDefinition` into the REAL body builder the script calls — nothing
11
+ * hand-built between them. The CP half of the seam (body → publishPackageCore →
12
+ * stored row) is `synap-control-plane-api/src/seeds/publish-package-core.icon.test.ts`.
13
+ */
14
+
15
+ import { describe, expect, it } from 'vitest';
16
+ import { readYamlTemplates } from './generate.mjs';
17
+ import { toOfficialPublishBody } from './publish-body.mjs';
18
+ import { toPackageDefinition } from './define';
19
+ import type { WorkspaceYaml } from './types';
20
+ import { readFileSync } from 'node:fs';
21
+ import { mergeCatalog } from './catalog';
22
+
23
+ const corpus = readYamlTemplates();
24
+
25
+ describe('toOfficialPublishBody', () => {
26
+ it('reads a plausible corpus (non-vacuity)', () => {
27
+ expect(corpus.length).toBeGreaterThanOrEqual(25);
28
+ });
29
+
30
+ it("mirrors every template's own icon / colour / domain onto the definition", () => {
31
+ const wrong: string[] = [];
32
+ let withIcon = 0;
33
+ for (const t of corpus) {
34
+ const tpl = t.data as WorkspaceYaml;
35
+ const body = toOfficialPublishBody(toPackageDefinition(tpl), 'vendor-x');
36
+ const meta = tpl.meta as { icon?: string; color?: string; domain?: string };
37
+ if (meta.icon) withIcon++;
38
+ if (body.definition.icon !== meta.icon) wrong.push(`${t.slug}.icon`);
39
+ if (body.definition.color !== meta.color) wrong.push(`${t.slug}.color`);
40
+ if (body.definition.domain !== meta.domain) wrong.push(`${t.slug}.domain`);
41
+ // `_meta` is hoisted to columns, never sent inside the definition.
42
+ expect('_meta' in body.definition).toBe(false);
43
+ expect(body.slug).toBe(tpl.meta.slug);
44
+ expect(body.vendorId).toBe('vendor-x');
45
+ }
46
+ expect(wrong).toEqual([]);
47
+ // Non-vacuity: the corpus actually declares icons, so the equality above
48
+ // cannot pass by comparing undefined to undefined everywhere.
49
+ expect(withIcon).toBe(corpus.length);
50
+ });
51
+
52
+ // Incident 2026-10-06: official rows carried NO `definition.sourcePackage`,
53
+ // so `mergeCatalog` let every stale client bundle win (the CLI installed its
54
+ // old content-os — no primarySurface — onto a live space).
55
+ it("stamps this package's own name + version as definition.sourcePackage", () => {
56
+ const own = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
57
+ expect(own.version).toMatch(/^\d+\.\d+\.\d+/);
58
+ for (const t of corpus) {
59
+ const body = toOfficialPublishBody(toPackageDefinition(t.data as WorkspaceYaml), 'vendor-x');
60
+ expect(body.definition.sourcePackage).toEqual({ name: '@synap-core/workspace-templates', version: own.version });
61
+ }
62
+ });
63
+
64
+ it('a published row outranks an OLDER client bundle through the real merge door', () => {
65
+ const tpl = corpus.find((t) => t.slug === 'content-os')!.data as WorkspaceYaml;
66
+ const body = toOfficialPublishBody(toPackageDefinition(tpl), 'vendor-x');
67
+ const merged = mergeCatalog({
68
+ bundled: [tpl],
69
+ remote: [{ slug: body.slug, name: body.displayName, definition: body.definition }],
70
+ bundleVersion: '0.0.1',
71
+ });
72
+ expect(merged.entries.find((e) => e.slug === 'content-os')?.source).toBe('remote');
73
+ });
74
+ });
@@ -0,0 +1,176 @@
1
+ /**
2
+ * The ONE official-publish implementation (`scripts/publish-official-lib.mjs`),
3
+ * driven from the REAL corpus + REAL `toOfficialPublishBody` through an injected
4
+ * fetch. Nothing between the YAML and the wire is hand-built.
5
+ *
6
+ * Pins: the preflight refuses a non-official account BEFORE any write; the plan
7
+ * is read-only; a failed live read is a refusal, never "new"; the POST carries
8
+ * the official vendorId and `sourcePackage`; the diff names added/removed
9
+ * profiles and fields; a stamp the CP did not store is reported.
10
+ */
11
+
12
+ import { describe, expect, it } from "vitest";
13
+ import {
14
+ OfficialPublishError,
15
+ diffDefinitions,
16
+ executeOfficialPublish,
17
+ planOfficialPublish,
18
+ } from "../scripts/publish-official-lib.mjs";
19
+ import { OFFICIAL_SOURCE_PACKAGE } from "./publish-body.mjs";
20
+ import { cpFetch } from "../scripts/catalog-lib.mjs";
21
+ import { OFFICIAL, fakeCp, isWrite } from "../scripts/__fixtures__/fake-cp.mjs";
22
+ import { readYamlTemplates } from "./generate.mjs";
23
+
24
+ const CP = "https://cp.test";
25
+
26
+ describe("planOfficialPublish — preflight", () => {
27
+ it("refuses an account whose vendor is not synap-official, and writes nothing", async () => {
28
+ const { calls, fetchImpl } = fakeCp({ vendor: { id: "v-1", slug: "antoine" } });
29
+ await expect(
30
+ planOfficialPublish({ cpUrl: CP, token: "t", slugs: ["content-os"], fetchImpl }),
31
+ ).rejects.toMatchObject({ code: "not-official-publisher" });
32
+ expect(calls.filter((c) => c.method !== "GET")).toHaveLength(0);
33
+ });
34
+
35
+ it("a failed live read is a refusal — never guessed as a new template", async () => {
36
+ const { fetchImpl } = fakeCp({ liveStatus: 500 });
37
+ const err: unknown = await planOfficialPublish({ cpUrl: CP, token: "t", slugs: ["content-os"], fetchImpl }).catch((e: unknown) => e);
38
+ expect(err).toBeInstanceOf(OfficialPublishError);
39
+ expect((err as OfficialPublishError).code).toBe("live-read-failed");
40
+ });
41
+
42
+ it("an unknown slug is refused before any network call", async () => {
43
+ const { calls, fetchImpl } = fakeCp();
44
+ await expect(
45
+ planOfficialPublish({ cpUrl: CP, token: "t", slugs: ["no-such-template"], fetchImpl }),
46
+ ).rejects.toMatchObject({ code: "unknown-slug" });
47
+ expect(calls).toHaveLength(0);
48
+ });
49
+ });
50
+
51
+ describe("plan → execute — the wire", () => {
52
+ it("plans read-only, then POSTs the official vendorId + sourcePackage", async () => {
53
+ const { calls, fetchImpl } = fakeCp({ postStatus: 200 });
54
+ const plan = await planOfficialPublish({ cpUrl: CP, token: "t", slugs: ["content-os"], fetchImpl });
55
+ // Read-only: the only non-GET a plan makes is the CP's read-only preview.
56
+ expect(calls.filter(isWrite)).toHaveLength(0);
57
+ expect(plan.items).toHaveLength(1);
58
+ expect(plan.items[0].status).toBe("new");
59
+
60
+ const results = await executeOfficialPublish(plan, { token: "t", fetchImpl });
61
+ const posts = calls.filter(isWrite);
62
+ expect(posts).toHaveLength(1);
63
+ expect(posts[0].body.vendorId).toBe(OFFICIAL.id);
64
+ expect(posts[0].body.definition.sourcePackage).toEqual(OFFICIAL_SOURCE_PACKAGE);
65
+ // The CP refuses an official publish that does not come through the catalog door.
66
+ expect(posts[0].headers["x-synap-publish-door"]).toBe("catalog");
67
+ expect(results[0]).toMatchObject({ slug: "content-os", outcome: "updated", stampStored: true });
68
+ });
69
+
70
+ // The process contract (statusProperty, subjectStatus, onFail, gate, criteria,
71
+ // activators, relationToSubject, humanOnlyStatuses) is authored in YAML and
72
+ // must arrive in the POSTed definition UNCHANGED — through the real dist
73
+ // `toPackageDefinition` and the real body builder. Compared to the YAML parse,
74
+ // so a converter that drops or rewrites any playbook field fails here.
75
+ it("POSTs every authored playbook field — the process contract — unchanged", async () => {
76
+ const yamlPlaybooks = (readYamlTemplates().find((t) => t.slug === "content-os")!.data as any)
77
+ .playbooks as any[];
78
+ const produce = yamlPlaybooks.find((p) => p.name === "Produce Content");
79
+ // Non-vacuity: the YAML really declares the contract fields compared below.
80
+ expect(produce.subjectProfile.statusProperty).toBe("post-status");
81
+ expect(produce.activators).toEqual([{ on: "enters_status", status: "Briefed", mode: "propose" }]);
82
+ expect(produce.stages.find((s: any) => s.key === "review").onFail).toEqual({ toStage: "draft" });
83
+ expect(
84
+ produce.stages.find((s: any) => s.key === "draft").expectedOutputs[0].relationToSubject,
85
+ ).toBe("made_for");
86
+
87
+ const { calls, fetchImpl } = fakeCp();
88
+ const plan = await planOfficialPublish({ cpUrl: CP, token: "t", slugs: ["content-os"], fetchImpl });
89
+ await executeOfficialPublish(plan, { token: "t", fetchImpl });
90
+ const posted = calls.find(isWrite)!.body.definition.playbooks;
91
+ expect(posted).toEqual(yamlPlaybooks);
92
+ const postedProduce = posted.find((p: any) => p.name === "Produce Content");
93
+ expect(postedProduce.stages.map((s: any) => s.subjectStatus)).toEqual([
94
+ "Briefed",
95
+ "Scripted",
96
+ "In Draft",
97
+ "Review",
98
+ "Scheduled",
99
+ "Published",
100
+ ]);
101
+ expect(postedProduce.humanOnlyStatuses).toEqual(["Idea", "Archived"]);
102
+ });
103
+
104
+ it("reports a stamp the CP did not store", async () => {
105
+ const { fetchImpl } = fakeCp({ storeStamp: false });
106
+ const plan = await planOfficialPublish({ cpUrl: CP, token: "t", slugs: ["content-os"], fetchImpl });
107
+ const [r] = await executeOfficialPublish(plan, { token: "t", fetchImpl });
108
+ expect(r.stampStored).toBe(false);
109
+ });
110
+ });
111
+
112
+ describe("diffDefinitions", () => {
113
+ it("names added/removed profiles, fields, views, links and dependencies", () => {
114
+ const live = {
115
+ profiles: [
116
+ { slug: "post", properties: [{ slug: "title" }, { slug: "old-field" }] },
117
+ { slug: "gone" },
118
+ ],
119
+ views: [{ slug: "v-old" }],
120
+ entityLinks: [{ sourceProfileSlug: "post", type: "x", targetProfileSlug: "gone" }],
121
+ dependencies: [{ slug: "foundation" }],
122
+ onboarding: { a: 1, b: 2 },
123
+ };
124
+ const next = {
125
+ profiles: [{ slug: "post", properties: [{ slug: "title" }, { slug: "new-field" }] }, { slug: "fresh" }],
126
+ views: [{ slug: "v-new" }],
127
+ entityLinks: [],
128
+ dependencies: [{ slug: "foundation" }, { slug: "brand-library" }],
129
+ onboarding: { b: 2, a: 1 }, // same content, different key order → NOT a change
130
+ };
131
+ const d = diffDefinitions(live, next);
132
+ expect(d.profiles).toEqual({ added: ["fresh"], removed: ["gone"] });
133
+ expect(d.fields).toEqual({ added: ["post.new-field"], removed: ["post.old-field"] });
134
+ expect(d.views).toEqual({ added: ["v-new"], removed: ["v-old"] });
135
+ expect(d.entityLinks.removed).toEqual(["post —x→ gone"]);
136
+ expect(d.dependencies).toEqual({ added: ["brand-library"], removed: [] });
137
+ expect(d.otherKeysChanged).toEqual([]);
138
+ });
139
+
140
+ it("matches a live view (CP-derived slug) to the catalog view that names it", () => {
141
+ const live = { views: [{ name: "Task Board", slug: "task-board", displayName: "Task Board" }] };
142
+ const next = { views: [{ name: "Task Board" }] };
143
+ expect(diffDefinitions(live, next).views).toEqual({ added: [], removed: [] });
144
+ });
145
+ });
146
+
147
+ describe("cpFetch — a dropped read is retried once, a write never", () => {
148
+ const flaky = (failures: number) => {
149
+ let calls = 0;
150
+ const impl = (async () => {
151
+ calls++;
152
+ if (calls <= failures) throw new TypeError("fetch failed");
153
+ return new Response("{}", { status: 200 });
154
+ }) as unknown as typeof fetch;
155
+ return { impl, calls: () => calls };
156
+ };
157
+
158
+ it("a GET that drops once succeeds on the retry", async () => {
159
+ const f = flaky(1);
160
+ const res = await cpFetch(f.impl, "https://cp.test/api/packages", {}, { retryDelayMs: 0 });
161
+ expect(res.status).toBe(200);
162
+ expect(f.calls()).toBe(2);
163
+ });
164
+
165
+ it("a GET that drops twice is cp-unreachable", async () => {
166
+ const f = flaky(2);
167
+ await expect(cpFetch(f.impl, "https://cp.test/x", {}, { retryDelayMs: 0 })).rejects.toMatchObject({ code: "cp-unreachable" });
168
+ expect(f.calls()).toBe(2);
169
+ });
170
+
171
+ it("a POST that drops is never re-sent (it may have landed)", async () => {
172
+ const f = flaky(1);
173
+ await expect(cpFetch(f.impl, "https://cp.test/api/packages", { method: "POST" }, { retryDelayMs: 0 })).rejects.toMatchObject({ code: "cp-unreachable" });
174
+ expect(f.calls()).toBe(1);
175
+ });
176
+ });