@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,317 @@
1
+ /**
2
+ * Apify's `capture_social_post` is a PURE URL READER ("Any link → data",
3
+ * 2026-10-15): same verb id, intent `read_webpage`, `metadata.readOnly`,
4
+ * declared `urls`, and it returns the UrlRead shape (`@synap-core/types/url-read`).
5
+ * It writes nothing: the Library item is the one home of a kept post.
6
+ *
7
+ * SEAM: the verb's REAL `code` body, run the way the pod's executor wraps it
8
+ * (`async (args, context) => { <code> }` with `callProvider` / `propose` in
9
+ * scope), against Apify dataset payloads shaped like the actors' documented
10
+ * output, and its answer passed through the REAL parse door `normalizeUrlRead`.
11
+ * The URL table runs the declared `urls` through the REAL `matchUrlPattern`, so
12
+ * the declaration and the code cannot drift apart.
13
+ *
14
+ * NOT covered: the live actors' output (the payloads are hand-written from the
15
+ * actors' documented fields), the isolate's polyfilled `URL` (Node's is used),
16
+ * and how the pod routes or stores a read (backend's url-readers tests).
17
+ */
18
+ import { readFileSync } from "node:fs";
19
+ import { dirname, join } from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+ import { describe, expect, it } from "vitest";
22
+ import { matchUrlPattern, normalizeUrlRead, type ReaderStamp } from "@synap-core/types/url-read";
23
+
24
+ const HERE = dirname(fileURLToPath(import.meta.url));
25
+ const manifest = JSON.parse(readFileSync(join(HERE, "../capabilities/apify.capability.json"), "utf8"));
26
+ type Skill = { name: string; intent?: string; code?: string; urls?: string[]; metadata?: { readOnly?: boolean } };
27
+ const skills = manifest.spec.skills as Skill[];
28
+ const reader = skills.find((s) => s.name === "capture_social_post")!;
29
+
30
+ const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor as new (
31
+ ...args: string[]
32
+ ) => (...a: unknown[]) => Promise<unknown>;
33
+ const body = new AsyncFunction("args", "context", "callProvider", "propose", reader.code!);
34
+
35
+ type Call = { tool: string; method: string; path: string; input: unknown };
36
+ async function run(url: string, answer: unknown | (() => never), extra: Record<string, unknown> = {}) {
37
+ const calls: Call[] = [];
38
+ const writes: unknown[] = [];
39
+ const callProvider = async (tool: string, method: string, path: string, input: unknown) => {
40
+ calls.push({ tool, method, path, input });
41
+ if (typeof answer === "function") return (answer as () => never)();
42
+ return { status: 201, headers: {}, body: answer };
43
+ };
44
+ const propose = { entity: async (x: unknown) => (writes.push(x), { status: "proposed" }) };
45
+ const raw = (await body({ url, ...extra }, {}, callProvider, propose)) as Record<string, any>;
46
+ return { raw, calls, writes };
47
+ }
48
+ const VIA: ReaderStamp = { id: "apify.capture_social_post", name: "Apify", billed: true, readAt: "2026-10-15T00:00:00.000Z" };
49
+
50
+ const IG_CAROUSEL = {
51
+ url: "https://www.instagram.com/p/CAROUSEL1/",
52
+ type: "Sidecar",
53
+ caption: "Three looks for autumn\nswipe for the third",
54
+ ownerUsername: "studio.x",
55
+ ownerFullName: "Studio X",
56
+ timestamp: "2026-10-01T09:30:00.000Z",
57
+ displayUrl: "https://cdn.example/ig/slide1.jpg",
58
+ dimensionsWidth: 1080,
59
+ dimensionsHeight: 1350,
60
+ likesCount: 812,
61
+ commentsCount: 41,
62
+ images: ["https://cdn.example/ig/slide1.jpg", "https://cdn.example/ig/slide2.jpg", "https://cdn.example/ig/slide3.jpg"],
63
+ childPosts: [
64
+ { type: "Image", displayUrl: "https://cdn.example/ig/slide1.jpg", dimensionsWidth: 1080, dimensionsHeight: 1350 },
65
+ { type: "Image", displayUrl: "https://cdn.example/ig/slide2.jpg" },
66
+ { type: "Video", displayUrl: "https://cdn.example/ig/slide3.jpg", videoUrl: "https://cdn.example/ig/slide3.mp4", videoDuration: 7.5 },
67
+ ],
68
+ };
69
+ const IG_REEL = {
70
+ url: "https://www.instagram.com/reel/REEL1/",
71
+ type: "Video",
72
+ caption: "Behind the scenes",
73
+ ownerUsername: "studio.x",
74
+ timestamp: "2026-10-02T10:00:00.000Z",
75
+ displayUrl: "https://cdn.example/ig/reel-cover.jpg",
76
+ videoUrl: "https://cdn.example/ig/reel.mp4",
77
+ videoDuration: 31.2,
78
+ likesCount: 90,
79
+ commentsCount: 3,
80
+ videoViewCount: 4200,
81
+ };
82
+ const TIKTOK = {
83
+ webVideoUrl: "https://www.tiktok.com/@maker/video/7400000000000000001",
84
+ text: "How we pack an order",
85
+ createTimeISO: "2026-09-30T18:00:00.000Z",
86
+ authorMeta: { name: "maker", profileUrl: "https://www.tiktok.com/@maker" },
87
+ videoMeta: { coverUrl: "https://cdn.example/tt/cover.jpg", downloadAddr: "https://cdn.example/tt/dl.mp4", width: 720, height: 1280, duration: 22 },
88
+ mediaUrls: ["https://api.apify.com/v2/key-value-stores/s/records/video-1.mp4"],
89
+ diggCount: 1500,
90
+ commentCount: 12,
91
+ playCount: 99000,
92
+ };
93
+
94
+ describe("capture_social_post — declared as a pure reader", () => {
95
+ it("is a read_webpage, read-only verb that takes a url and declares the links it reads", () => {
96
+ expect(reader.intent).toBe("read_webpage");
97
+ expect(reader.metadata?.readOnly).toBe(true);
98
+ expect((reader as any).parameters).toEqual({ url: "string", withVideo: "boolean?" });
99
+ expect(reader.urls).toEqual([
100
+ "instagram.com/p/*",
101
+ "instagram.com/reel/*",
102
+ "instagram.com/reels/*",
103
+ "instagram.com/tv/*",
104
+ "tiktok.com/@*/video/*",
105
+ "vm.tiktok.com/*",
106
+ "vt.tiktok.com/*",
107
+ ]);
108
+ });
109
+
110
+ it("the Apify verbs that only read are read-only; the generic actor run is not", () => {
111
+ const ro = Object.fromEntries(skills.map((s) => [s.name, s.metadata?.readOnly === true]));
112
+ expect(ro).toMatchObject({
113
+ apify_search_actors: true,
114
+ apify_get_actor_schema: true,
115
+ apify_list_actor_runs: true,
116
+ apify_get_dataset_items: true,
117
+ capture_social_post: true,
118
+ apify_run_actor: false,
119
+ });
120
+ });
121
+
122
+ it("the code proposes nothing and hands no media-urls over (the Library item is the one home)", () => {
123
+ expect(reader.code).not.toMatch(/propose\.entity|media-urls|bookmark/);
124
+ });
125
+ });
126
+
127
+ describe("capture_social_post — its answer through the real parse door", () => {
128
+ it("the vendor receives the URL as PARSED, never the raw string (a tab inside is dropped by WHATWG)", async () => {
129
+ const { calls } = await run("https://www.instagram.com/p/CAR\tOUSEL1/", [IG_CAROUSEL]);
130
+ expect(calls.map((c) => c.input)).toEqual([
131
+ { directUrls: ["https://www.instagram.com/p/CAROUSEL1/"], resultsType: "posts" },
132
+ ]);
133
+ const tt = await run("https://www.tiktok.com/@ada/video/1\n23", []);
134
+ expect(tt.calls.map((c) => c.input)).toEqual([
135
+ { postURLs: ["https://www.tiktok.com/@ada/video/123"], shouldDownloadVideos: false },
136
+ ]);
137
+ });
138
+
139
+ it("withVideo: declared as an optional boolean; TikTok downloads the video ONLY when it is true; Instagram is unaffected", async () => {
140
+ expect((reader as Skill & { parameters?: Record<string, string> }).parameters).toEqual({
141
+ url: "string",
142
+ withVideo: "boolean?",
143
+ });
144
+ const TT = "https://www.tiktok.com/@ada/video/123";
145
+ for (const [extra, expected] of [
146
+ [{}, false],
147
+ [{ withVideo: false }, false],
148
+ [{ withVideo: "yes" }, false],
149
+ [{ withVideo: true }, true],
150
+ ] as const) {
151
+ const { calls } = await run(TT, [], extra);
152
+ expect(calls.map((c) => c.input)).toEqual([{ postURLs: [TT], shouldDownloadVideos: expected }]);
153
+ }
154
+ const ig = await run("https://www.instagram.com/p/CAROUSEL1/", [IG_CAROUSEL], { withVideo: true });
155
+ expect(ig.calls.map((c) => c.input)).toEqual([
156
+ { directUrls: ["https://www.instagram.com/p/CAROUSEL1/"], resultsType: "posts" },
157
+ ]);
158
+ });
159
+
160
+ it("an Instagram carousel: every slide as an image, a video slide's file too, in order, with its counts", async () => {
161
+ const { raw, calls, writes } = await run("https://www.instagram.com/p/CAROUSEL1/", [IG_CAROUSEL]);
162
+ expect(writes).toEqual([]);
163
+ expect(calls).toEqual([
164
+ {
165
+ tool: "apify_api",
166
+ method: "POST",
167
+ path: "/actors/apify~instagram-scraper/run-sync-get-dataset-items",
168
+ input: { directUrls: ["https://www.instagram.com/p/CAROUSEL1/"], resultsType: "posts" },
169
+ },
170
+ ]);
171
+ const read = normalizeUrlRead(raw, "https://www.instagram.com/p/CAROUSEL1/", VIA);
172
+ expect(read).toMatchObject({
173
+ status: "ok",
174
+ canonicalUrl: "https://www.instagram.com/p/CAROUSEL1/",
175
+ title: "Three looks for autumn",
176
+ author: "studio.x",
177
+ authorUrl: "https://www.instagram.com/studio.x/",
178
+ text: IG_CAROUSEL.caption,
179
+ publishedAt: "2026-10-01T09:30:00.000Z",
180
+ fields: { platform: "instagram", likes: 812, comments: 41 },
181
+ });
182
+ expect(read.media?.map((m) => [m.order, m.kind, m.role, m.url])).toEqual([
183
+ [0, "image", "slide", "https://cdn.example/ig/slide1.jpg"],
184
+ [1, "image", "slide", "https://cdn.example/ig/slide2.jpg"],
185
+ [2, "image", "slide", "https://cdn.example/ig/slide3.jpg"],
186
+ [3, "video", "slide", "https://cdn.example/ig/slide3.mp4"],
187
+ ]);
188
+ expect(read.media?.[3].durationSec).toBe(7.5);
189
+ });
190
+
191
+ it("an Instagram reel: the cover still, then the video file, with views", async () => {
192
+ const { raw } = await run("https://instagram.com/reel/REEL1/?igsh=abc", [IG_REEL]);
193
+ const read = normalizeUrlRead(raw, "https://instagram.com/reel/REEL1/?igsh=abc", VIA);
194
+ expect(read.status).toBe("ok");
195
+ expect(read.media?.map((m) => [m.kind, m.role ?? null, m.url])).toEqual([
196
+ ["image", "cover", "https://cdn.example/ig/reel-cover.jpg"],
197
+ ["video", null, "https://cdn.example/ig/reel.mp4"],
198
+ ]);
199
+ expect(read.media?.[1].durationSec).toBe(31.2);
200
+ expect(read.fields).toEqual({ platform: "instagram", likes: 90, comments: 3, views: 4200 });
201
+ });
202
+
203
+ it("an Instagram carousel known only by `images`: each one a slide", async () => {
204
+ const { childPosts: _drop, ...noChildren } = IG_CAROUSEL;
205
+ const { raw } = await run("https://www.instagram.com/p/CAROUSEL1/", [noChildren]);
206
+ const read = normalizeUrlRead(raw, "https://www.instagram.com/p/CAROUSEL1/", VIA);
207
+ expect(read.media?.map((m) => m.url)).toEqual(IG_CAROUSEL.images);
208
+ expect(read.media?.every((m) => m.kind === "image" && m.role === "slide")).toBe(true);
209
+ });
210
+
211
+ it("a TikTok video: the cover, then the stored video file (the post page is never a media file)", async () => {
212
+ const { raw, calls } = await run("https://www.tiktok.com/@maker/video/7400000000000000001", [TIKTOK]);
213
+ expect(calls[0].path).toBe("/actors/clockworks~tiktok-scraper/run-sync-get-dataset-items");
214
+ const read = normalizeUrlRead(raw, "https://www.tiktok.com/@maker/video/7400000000000000001", VIA);
215
+ expect(read).toMatchObject({
216
+ status: "ok",
217
+ canonicalUrl: TIKTOK.webVideoUrl,
218
+ author: "maker",
219
+ authorUrl: "https://www.tiktok.com/@maker",
220
+ text: "How we pack an order",
221
+ publishedAt: "2026-09-30T18:00:00.000Z",
222
+ fields: { platform: "tiktok", likes: 1500, comments: 12, views: 99000 },
223
+ });
224
+ expect(read.media?.map((m) => [m.kind, m.role ?? null, m.url])).toEqual([
225
+ ["image", "cover", "https://cdn.example/tt/cover.jpg"],
226
+ ["video", null, TIKTOK.mediaUrls[0]],
227
+ ]);
228
+ expect(read.media?.some((m) => m.url === TIKTOK.webVideoUrl)).toBe(false);
229
+ });
230
+
231
+ it("a TikTok the actor did not store: its download address is the video", async () => {
232
+ const { mediaUrls: _drop, ...noStored } = TIKTOK;
233
+ const { raw } = await run("https://vm.tiktok.com/ZMabc123/", [noStored]);
234
+ const read = normalizeUrlRead(raw, "https://vm.tiktok.com/ZMabc123/", VIA);
235
+ expect(read.media?.find((m) => m.kind === "video")?.url).toBe("https://cdn.example/tt/dl.mp4");
236
+ });
237
+
238
+ it("not there / not public is BLOCKED with the reason, never an empty ok", async () => {
239
+ const cases: Array<[unknown, string]> = [
240
+ [[], "not_found"],
241
+ [[{ url: "https://www.instagram.com/p/GONE/", error: "not_found", errorDescription: "Page not found" }], "not_found"],
242
+ [[{ inputUrl: "https://www.instagram.com/p/GONE/", error: "no_items", errorDescription: "Empty or private data for provided input" }], "not_found"],
243
+ [[{ url: "https://www.instagram.com/p/LOCK/", error: "restricted_page", errorDescription: "This account is private" }], "private"],
244
+ ];
245
+ for (const [answer, reason] of cases) {
246
+ const { raw } = await run("https://www.instagram.com/p/X1/", answer);
247
+ const read = normalizeUrlRead(raw, "https://www.instagram.com/p/X1/", VIA);
248
+ expect(read.status, JSON.stringify(answer)).toBe("blocked");
249
+ expect(read.blocked?.reason, JSON.stringify(answer)).toBe(reason);
250
+ }
251
+ });
252
+
253
+ it("an Apify run failure PROPAGATES (the executor keeps its failure class); it is never folded into a read", async () => {
254
+ // HTTP / auth / quota / run failure: callProvider throws, and so does the verb.
255
+ await expect(
256
+ run("https://www.instagram.com/p/X1/", () => {
257
+ throw new Error("callProvider failed: Apify 402 Payment Required");
258
+ }),
259
+ ).rejects.toThrow(/402 Payment Required/);
260
+ // An actor error that says nothing about the post is a run failure too.
261
+ await expect(run("https://www.instagram.com/p/X1/", [{ error: "rate_limited", errorDescription: "Try later" }])).rejects.toThrow(
262
+ /rate_limited: Try later/,
263
+ );
264
+ });
265
+
266
+ it("parity: the code reads EXACTLY the links its `urls` declare (the real matcher), and calls Apify for nothing else", async () => {
267
+ const table = [
268
+ "https://www.instagram.com/p/ABC123/",
269
+ "https://instagram.com/p/ABC123",
270
+ "http://instagram.com/reel/R1/",
271
+ "https://www.instagram.com/reels/R1/",
272
+ "https://www.instagram.com/tv/T1/",
273
+ "https://www.tiktok.com/@maker/video/7400000000000000001",
274
+ "https://tiktok.com/@maker/video/1?lang=en",
275
+ "https://vm.tiktok.com/ZMabc123/",
276
+ "https://vt.tiktok.com/ZSxyz/",
277
+ // not declared: a profile, a story, another host, a lookalike, a bare TikTok profile
278
+ "https://www.instagram.com/studio.x/",
279
+ "https://www.instagram.com/stories/studio.x/1/",
280
+ "https://m.instagram.com/p/ABC123/",
281
+ "https://notinstagram.com/p/ABC123/",
282
+ "https://www.tiktok.com/@maker",
283
+ "https://www.tiktok.com/@maker/photo/1",
284
+ "https://example.com/p/ABC123/",
285
+ "ftp://instagram.com/p/ABC123/",
286
+ ];
287
+ const declaredHits = table.filter((u) => reader.urls!.some((p) => matchUrlPattern(p, u)));
288
+ // non-vacuity: the table holds both answers
289
+ expect(declaredHits.length).toBe(9);
290
+ for (const u of table) {
291
+ const { raw, calls } = await run(u, [IG_REEL]);
292
+ const declared = declaredHits.includes(u);
293
+ expect(raw.status === "blocked" && raw.blocked?.reason === "unsupported", u).toBe(!declared);
294
+ expect(calls.length, u).toBe(declared ? 1 : 0);
295
+ }
296
+ });
297
+ });
298
+
299
+ describe("apify_run_actor — a billed run always hands its items back", () => {
300
+ const runner = skills.find((s) => s.name === "apify_run_actor")!;
301
+ const runBody = new AsyncFunction("args", "context", "callProvider", "propose", runner.code!);
302
+
303
+ it("writes nothing (a bookmark without a url failed the verb AFTER the credit was spent)", async () => {
304
+ const writes: unknown[] = [];
305
+ // Any write-back fails the way the approval did: the run must not depend on one.
306
+ const propose = { entity: async (x: unknown) => (writes.push(x), Promise.reject(new Error("missing_field: url"))) };
307
+ const items = [{ id: 1 }, { id: 2 }];
308
+ const callProvider = async () => ({ status: 201, headers: {}, body: items });
309
+ const out = (await runBody({ actorId: "owner/actor", input: {} }, {}, callProvider, propose)) as Record<string, any>;
310
+ expect(writes).toEqual([]);
311
+ expect(out).toMatchObject({ success: true, actorId: "owner/actor", itemCount: 2, items });
312
+ });
313
+
314
+ it("the code has no propose.entity at all", () => {
315
+ expect(runner.code).not.toMatch(/propose\.entity|bookmark/);
316
+ });
317
+ });
package/src/base.yaml CHANGED
@@ -585,15 +585,17 @@ automations:
585
585
  3. The outer container must always have STRICTLY MORE colons than anything nested
586
586
  inside it. Four outside, three inside. If you ever nest a four-colon block in a
587
587
  four-colon block the section closes early and the rest of the report leaks out.
588
- 4. ATTRIBUTE VALUES MUST BE QUOTED. An UNQUOTED value ends at the first } it
589
- contains, which silently truncates the attribute — so always quote. Use
590
- DOUBLE quotes for a plain value, and SINGLE quotes when the value is itself
591
- a JSON object, so the JSON's own double quotes survive:
592
- :::synap-cell{cellKey="chart-pie" cellProps='{"profileSlug":"task","groupBy":"status"}'}
588
+ 4. ATTRIBUTES ARE REFERENCES ONLY, ALWAYS DOUBLE-QUOTED: cellKey="…", id="…".
589
+ A cell's settings NEVER go in an attribute. They go in a ```json block,
590
+ the first thing inside the embed, compact on one line, a JSON object:
591
+ :::synap-cell{cellKey="chart-pie"}
592
+ ```json
593
+ {"profileSlug":"task","groupBy":"status","label":"Open tasks by status"}
594
+ ```
593
595
  :::
594
- A single-quoted value may contain both " and } — those are only special to
595
- an unquoted value. Write the JSON compact, on one line, with no trailing
596
- comma; malformed JSON renders the cell with empty config.
596
+ Inside that block ordinary JSON rules apply (an apostrophe is fine).
597
+ Malformed JSON is shown to the reader as a broken cell. Never write the
598
+ old cellProps='…' attribute form.
597
599
  5. The ONLY section attributes that survive are: id, agent, round, skills,
598
600
  confidence, stepRunId, nodeId, status. Anything else is silently dropped.
599
601
  6. confidence is a 0-1 ratio written as a decimal string, e.g. confidence="0.8".
@@ -611,12 +613,13 @@ automations:
611
613
  8. NEVER EMIT :::synap-view, AND NEVER EMIT :::synap-cell WITH AN instanceId.
612
614
  Both need an id you have not been given and cannot obtain, and a made-up
613
615
  reference produces a permanent broken block in a document a human reads.
614
- 9. YOU MAY EMIT A CHART :::synap-cell — from this allowlist ONLY, configured
615
- through cellProps. These cells fetch their own data at render time, so they
616
- need no id, and they make the report a live surface instead of a text file:
617
- - :::synap-cell{cellKey="stat-card" cellProps='{"profileSlug":"<kind>","aggregation":"count","timePeriod":"week","label":"<Kind> created this week"}'}
618
- - :::synap-cell{cellKey="chart-pie" cellProps='{"profileSlug":"<kind>","groupBy":"<property>","label":"<the finding this shows>"}'}
619
- - :::synap-cell{cellKey="chart-bar" cellProps='{"profileSlug":"<kind>","mode":"category","groupBy":"<property>","label":"<the finding this shows>"}'}
616
+ 9. YOU MAY EMIT A CHART :::synap-cell — from this allowlist ONLY, each with its
617
+ settings in the ```json block of rule 4. These cells need no id: their data
618
+ is read from the settings in the json block, never written by you. The three
619
+ cells and their json blocks:
620
+ - cellKey="stat-card" with {"profileSlug":"<kind>","aggregation":"count","timePeriod":"week","label":"<Kind> created this week"}
621
+ - cellKey="chart-pie" with {"profileSlug":"<kind>","groupBy":"<property>","label":"<Kind> by <property>"}
622
+ - cellKey="chart-bar" with {"profileSlug":"<kind>","mode":"category","groupBy":"<property>","label":"<Kind> by <property>"}
620
623
  Rules, all mandatory:
621
624
  (a) profileSlug must be one of the kinds gathered for THIS report: task,
622
625
  note, person, company. A chart over any other kind is a claim this
@@ -637,28 +640,29 @@ automations:
637
640
  nothing here. If no gathered kind has a distribution worth showing,
638
641
  emit no cells at all — that is a correct report, not a lesser one.
639
642
  (e) AT MOST ONE cell per section, and NEVER at the top of a section.
640
- Place it immediately AFTER the sentence making the claim it evidences,
641
- so the reader has the claim before the picture. A chart that opens a
642
- section makes the reader decode a graphic to find out what the section
643
- is about, which is the delay the ## heading and the claim sentence
644
- exist to remove.
645
- (f) The label must state the FINDING, not the dimension. Write
646
- label:"Half the open tasks are blocked", never label:"Distribution",
647
- "Status" or "Tasks by status". Always set label explicitly: with none,
648
- a pie chart titles itself "Distribution" and a bar chart "Breakdown" —
649
- words true of every such chart ever drawn, which tell the reader
650
- nothing they cannot already see in the shapes.
651
- (g) A CHART IS A LIVE VIEW, THE PROSE IS A SNAPSHOT. These cells run
652
- their own query when the report is OPENED, which may be long after
653
- this text was written — so a sentence quoting a count and a chart
654
- measuring the same thing WILL eventually disagree, and the reader
655
- cannot tell which is stale. Never write a specific number in a
656
- sentence that sits beside a chart of the same data: let the chart
657
- carry the number and let the sentence carry the claim
658
- ("most open tasks are stalled at review", not "12 of 19 are stalled").
659
- And if the sentence already gives the number the chart would show,
660
- DROP THE CHART — one of them is redundant, and the sentence is the one
661
- that cannot go stale against itself.
643
+ The ## claim heading and at least one sentence of context come first.
644
+ Then the cell. Then, IMMEDIATELY AFTER the cell's closing ::: line,
645
+ one prose sentence stating the FINDING the chart shows
646
+ ("Review is where open tasks accumulate."). The finding lives in
647
+ prose, where it can be read, quoted and edited — never inside the
648
+ cell.
649
+ (f) The json block carries STRUCTURE ONLY: profileSlug, aggregation,
650
+ timePeriod, mode, groupBy, field, and a SHORT label naming what is
651
+ measured ("Open tasks by status", "Notes created this week"). The
652
+ label is a name, not a sentence: no verb, no finding. Always set the
653
+ label: with none, a pie chart titles itself "Distribution" and a bar
654
+ chart "Breakdown".
655
+ (g) YOU WRITE THE CHART'S QUESTION, NEVER ITS NUMBERS. Never put a
656
+ "data" or "capturedAt" key in a json block. After you finish, the
657
+ pipeline runs each chart's own query and stores the result in the
658
+ embed as a dated snapshot, so the chart shows the workspace as it
659
+ was when this report was generated. You never see those numbers,
660
+ so never write a specific number in a sentence that sits beside a
661
+ chart of the same data: let the chart carry the number and let the
662
+ sentence carry the claim ("most open tasks are stalled at review",
663
+ not "12 of 19 are stalled"). And if the sentence already gives the
664
+ number the chart would show, DROP THE CHART — one of them is
665
+ redundant.
662
666
  Everything else this report needs is ::::synap-section containers plus
663
667
  [[entity:…]] chips inline.
664
668
 
@@ -675,6 +679,11 @@ automations:
675
679
  section whose heading merely names the round tells the reader nothing
676
680
  they cannot already see.
677
681
 
682
+ LAYOUT: put ONE blank line between blocks (paragraphs, headings, embeds) and
683
+ none directly after a section's opening line or before its closing ::::, as
684
+ in the worked example — that is the layout the document editor writes back,
685
+ so the report opens in it for editing.
686
+
678
687
  STRUCTURE: one ::::synap-section per round, in order — analyze, then relate.
679
688
  Open with a single "# " title line ABOVE the first section, and open each
680
689
  section with its own ## claim heading per rule 10.
@@ -702,8 +711,14 @@ automations:
702
711
  majority are sitting in one status — the oldest is
703
712
  [[entity:3f2a91c4-7b10-4e55-9c02-8ad1f6e4b2d7|Migrate the billing worker]].
704
713
  Notes are thin.
705
- :::synap-cell{cellKey="chart-pie" cellProps='{"profileSlug":"task","groupBy":"status","label":"Review is where open tasks accumulate"}'}
714
+
715
+ :::synap-cell{cellKey="chart-pie"}
716
+ ```json
717
+ {"profileSlug":"task","groupBy":"status","label":"Open tasks by status"}
718
+ ```
706
719
  :::
720
+
721
+ Review is where open tasks accumulate.
707
722
  ::::
708
723
 
709
724
  ::::synap-section{agent="analyst" round="relate" status="failed" confidence="0.2"}
@@ -766,6 +781,17 @@ automations:
766
781
  - path: steps.assemble.output.error
767
782
  exists: false
768
783
  message: The assembler round failed — refusing to write a report without a body.
784
+ - id: freeze-charts
785
+ type: capability
786
+ position:
787
+ x: 1740
788
+ "y": 320
789
+ data:
790
+ label: Freeze charts
791
+ verbId: document.freeze_charts
792
+ verbKind: read
793
+ inputMapping:
794
+ markdown: "{{steps.assemble.output}}"
769
795
  - id: create-report
770
796
  type: output
771
797
  position:
@@ -787,11 +813,25 @@ automations:
787
813
  - Notes
788
814
  - People
789
815
  - Companies
790
- body: "{{steps.assemble.output}}"
816
+ body: "{{steps.freeze-charts.output.markdown}}"
791
817
  systemData:
792
818
  renderer:
793
819
  kind: cell
794
820
  cellKey: "{{trigger.payload.renderer}}"
821
+ - id: stamp-chart-diagnostics
822
+ type: capability
823
+ position:
824
+ x: 2100
825
+ "y": 320
826
+ data:
827
+ label: Record charts left live
828
+ verbId: document.stamp_diagnostics
829
+ verbKind: write
830
+ inputMapping:
831
+ documentId: "{{steps.create-report.output.documentId}}"
832
+ items: "{{steps.freeze-charts.output.diagnostics}}"
833
+ errorHandling:
834
+ continueOnError: true
795
835
  edges:
796
836
  - id: e-trigger-now
797
837
  source: trigger
@@ -844,6 +884,12 @@ automations:
844
884
  - id: e-summarize-guard
845
885
  source: summarize
846
886
  target: guard-assembled
847
- - id: e-guard-create
887
+ - id: e-guard-freeze
848
888
  source: guard-assembled
889
+ target: freeze-charts
890
+ - id: e-freeze-create
891
+ source: freeze-charts
849
892
  target: create-report
893
+ - id: e-create-stamp
894
+ source: create-report
895
+ target: stamp-chart-diagnostics
@@ -433,6 +433,7 @@ onboarding:
433
433
  who's providing liquidity, who's auditing, who's building, and who's amplifying.
434
434
  collect:
435
435
  - profileSlug: market-maker
436
+ min: 1
436
437
  what: The market-makers / liquidity providers you work with, tagged onto the real company.
437
438
  cardinality: few
438
439
  keyFields: [status, exchanges]