@synap-core/workspace-templates 0.11.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/README.md +483 -0
  2. package/automations/bookmark-enrichment.automation.json +321 -0
  3. package/automations/github-pr-events.automation.json +505 -0
  4. package/automations/mail-feed.automation.json +86 -0
  5. package/automations/ship-a-change-checks-green.automation.json +106 -0
  6. package/automations/ship-a-change-checks-red.automation.json +106 -0
  7. package/automations/ship-a-change-deployed.automation.json +56 -0
  8. package/automations/ship-a-change-pr-merged.automation.json +110 -0
  9. package/automations/ship-a-change-pr-opened.automation.json +104 -0
  10. package/automations/url-bookmark-capture.automation.json +132 -0
  11. package/capabilities/agency-skills.capability.json +157 -0
  12. package/capabilities/apify.capability.json +265 -0
  13. package/capabilities/apollo.capability.json +120 -0
  14. package/capabilities/arch-backend.capability.json +97 -0
  15. package/capabilities/arch-client-intelligence.capability.json +401 -0
  16. package/capabilities/backblaze-b2.capability.json +205 -0
  17. package/capabilities/cal-com.capability.json +266 -0
  18. package/capabilities/canva.capability.json +274 -0
  19. package/capabilities/claude-code-routines.capability.json +172 -0
  20. package/capabilities/claude-managed-agents.capability.json +215 -0
  21. package/capabilities/client-comms-report.capability.json +246 -0
  22. package/capabilities/content-idea-to-production.capability.json +29 -0
  23. package/capabilities/datagouv.capability.json +315 -0
  24. package/capabilities/discord-bot.capability.json +157 -0
  25. package/capabilities/dropbox.capability.json +281 -0
  26. package/capabilities/exa.capability.json +109 -0
  27. package/capabilities/fal-ai.capability.json +83 -0
  28. package/capabilities/fal-media-lab.capability.json +158 -0
  29. package/capabilities/fireflies.capability.json +191 -0
  30. package/capabilities/freellmapi.capability.json +114 -0
  31. package/capabilities/generic-apikey.capability.json +92 -0
  32. package/capabilities/github-merge.capability.json +99 -0
  33. package/capabilities/github-read.capability.json +325 -0
  34. package/capabilities/governance-tighten-recommender.capability.json +81 -0
  35. package/capabilities/jev.capability.json +226 -0
  36. package/capabilities/lead-conversion-enrollment.capability.json +246 -0
  37. package/capabilities/mail-feed.capability.json +277 -0
  38. package/capabilities/mailgun.capability.json +52 -0
  39. package/capabilities/nango-google.capability.json +639 -0
  40. package/capabilities/notion.capability.json +417 -0
  41. package/capabilities/omniroute.capability.json +137 -0
  42. package/capabilities/posthog-analytics.capability.json +258 -0
  43. package/capabilities/postiz.capability.json +467 -0
  44. package/capabilities/proactive-digest.capability.json +190 -0
  45. package/capabilities/proton.capability.json +288 -0
  46. package/capabilities/replicate-rvm.capability.json +97 -0
  47. package/capabilities/research-methods.capability.json +57 -0
  48. package/capabilities/shopify.capability.json +395 -0
  49. package/capabilities/slack.capability.json +369 -0
  50. package/capabilities/stripe.capability.json +420 -0
  51. package/capabilities/telegram-bridge.capability.json +56 -0
  52. package/capabilities/treg.capability.json +102 -0
  53. package/capabilities/unipile-linkedin.capability.json +471 -0
  54. package/capabilities/vercel.capability.json +424 -0
  55. package/dist/define-CPS0n8BV.d.ts +1331 -0
  56. package/dist/index.d.ts +189 -900
  57. package/dist/index.js +24654 -21639
  58. package/dist/index.js.map +1 -1
  59. package/dist/wire.d.ts +222 -0
  60. package/dist/wire.js +77 -0
  61. package/dist/wire.js.map +1 -0
  62. package/package.json +41 -27
  63. package/packs/bookmark-views-pack.json +33 -0
  64. package/packs/crm-pack.json +76 -0
  65. package/packs/customer-profile.json +28 -0
  66. package/packs/deep-research.playbook.json +30 -0
  67. package/packs/event-views-pack.json +33 -0
  68. package/packs/habit-profile.json +27 -0
  69. package/packs/hr-pack.json +54 -0
  70. package/packs/invoice-profile.json +28 -0
  71. package/packs/knowledge-base-pack.json +56 -0
  72. package/packs/meeting-profile.json +29 -0
  73. package/packs/product-profile.json +30 -0
  74. package/packs/project-management-pack.json +49 -0
  75. package/packs/recipe-profile.json +29 -0
  76. package/packs/task-views-pack.json +39 -0
  77. package/retired.yaml +136 -0
  78. package/skills/app-factory-build-slice/SKILL.md +70 -0
  79. package/skills/app-factory-build-slice.catalog.json +17 -0
  80. package/skills/app-factory-design-canvas/SKILL.md +96 -0
  81. package/skills/app-factory-design-canvas.catalog.json +17 -0
  82. package/skills/app-factory-design-review/SKILL.md +99 -0
  83. package/skills/app-factory-design-review.catalog.json +18 -0
  84. package/skills/app-factory-research/SKILL.md +123 -0
  85. package/skills/app-factory-research.catalog.json +17 -0
  86. package/skills/bookmark-enrichment/SKILL.md +40 -0
  87. package/skills/carousel-format/carousel-format.skill.json +18 -0
  88. package/skills/collage-broll/collage-broll.skill.json +20 -0
  89. package/skills/content-taste/content-taste.skill.json +18 -0
  90. package/skills/creative-director/creative-director.skill.json +18 -0
  91. package/skills/design-critique/SKILL.md +135 -0
  92. package/skills/design-critique.catalog.json +12 -0
  93. package/skills/designing-with-real-components/SKILL.md +140 -0
  94. package/skills/designing-with-real-components.catalog.json +12 -0
  95. package/skills/edit-loop/edit-loop.skill.json +25 -0
  96. package/skills/facecam-edit/facecam-edit.skill.json +24 -0
  97. package/skills/mapping-the-server/SKILL.md +54 -0
  98. package/skills/math-explainer-manimgx/math-explainer-manimgx.skill.json +20 -0
  99. package/skills/motion-quality-gates/motion-quality-gates.skill.json +20 -0
  100. package/skills/open-design-edit/open-design-edit.skill.json +24 -0
  101. package/skills/reading-client-progress/SKILL.md +65 -0
  102. package/skills/routing-to-channels/SKILL.md +54 -0
  103. package/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
  104. package/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
  105. package/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
  106. package/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
  107. package/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
  108. package/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
  109. package/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
  110. package/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
  111. package/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
  112. package/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
  113. package/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
  114. package/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
  115. package/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
  116. package/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
  117. package/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
  118. package/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
  119. package/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
  120. package/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
  121. package/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
  122. package/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
  123. package/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
  124. package/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
  125. package/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
  126. package/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
  127. package/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
  128. package/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
  129. package/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
  130. package/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
  131. package/skills/video-direction/video-direction.skill.json +20 -0
  132. package/skills/video-style-vocabulary/video-style-vocabulary.skill.json +20 -0
  133. package/skills/video-toolbox/video-toolbox.skill.json +20 -0
  134. package/src/__fixtures__/lifecycle-presets.flat-before.json +205 -0
  135. package/src/agency-os.yaml +122 -0
  136. package/src/agent-os-skill-slugs.test.ts +65 -0
  137. package/src/apify-social-reader.test.ts +317 -0
  138. package/src/base.template.test.ts +25 -19
  139. package/src/base.yaml +95 -39
  140. package/src/blockchain-ecosystem.yaml +1 -0
  141. package/src/brand-library.yaml +525 -14
  142. package/src/builder-dev-fold.template.test.ts +113 -0
  143. package/src/builder.yaml +798 -92
  144. package/src/business-model-catalog-description.test.ts +47 -0
  145. package/src/business-model-grp.template.test.ts +214 -0
  146. package/src/business-model.yaml +598 -0
  147. package/src/catalog-safety.test.ts +198 -0
  148. package/src/catalog.template-items.test.ts +75 -0
  149. package/src/catalog.ts +85 -2
  150. package/src/civic-france.yaml +102 -0
  151. package/src/concept-guard.test.ts +28 -8
  152. package/src/concept-guard.ts +2 -2
  153. package/src/content-creation.template.test.ts +52 -0
  154. package/src/content-creation.yaml +192 -0
  155. package/src/content-os.edit-loop.template.test.ts +210 -0
  156. package/src/content-os.publish-due-posts.template.test.ts +261 -0
  157. package/src/content-os.template.test.ts +322 -0
  158. package/src/content-os.video-takes.template.test.ts +338 -0
  159. package/src/content-os.work-a-take.template.test.ts +125 -0
  160. package/src/content-os.yaml +1830 -39
  161. package/src/content-studio-retired.template.test.ts +82 -0
  162. package/src/corpus-flow-validity.test.ts +4 -5
  163. package/src/crm-absorbs-outreach.template.test.ts +117 -0
  164. package/src/crm-build-plan.template.test.ts +128 -0
  165. package/src/crm-lists.template.test.ts +96 -0
  166. package/src/crm-messages-cadence.template.test.ts +164 -0
  167. package/src/crm-one-send-door.template.test.ts +65 -0
  168. package/src/crm.yaml +1456 -181
  169. package/src/decisions-wave.template.test.ts +186 -0
  170. package/src/define.ts +130 -17
  171. package/src/design-to-implementation.template.test.ts +94 -0
  172. package/src/dist-freshness.test.ts +97 -0
  173. package/src/domain-template-type.test.ts +75 -0
  174. package/src/ecosystem.yaml +27 -66
  175. package/src/enterprise-os.yaml +10 -134
  176. package/src/facecam-edit.audio-studio.test.ts +188 -0
  177. package/src/fal-generate-image.test.ts +200 -0
  178. package/src/fal-media-lab-pod-files.test.ts +180 -0
  179. package/src/finance.yaml +254 -1
  180. package/src/foundation.yaml +130 -28
  181. package/src/frame-host-modules.d.mts +6 -0
  182. package/src/frame-host-modules.mjs +58 -0
  183. package/src/generate.mjs +9 -3
  184. package/src/github-merge.test.ts +210 -0
  185. package/src/grants.yaml +57 -17
  186. package/src/hr.yaml +39 -6
  187. package/src/index.ts +36 -1
  188. package/src/intent.test.ts +161 -0
  189. package/src/internal-runbook.yaml +19 -1
  190. package/src/jev.test.ts +356 -0
  191. package/src/legal.yaml +33 -1
  192. package/src/life-os.yaml +65 -240
  193. package/src/lifecycle-coverage.test.ts +124 -0
  194. package/src/lifecycle-declarations.test.ts +252 -0
  195. package/src/lifecycle-presets.test.ts +468 -0
  196. package/src/lifecycle-presets.ts +494 -0
  197. package/src/lifecycle-stuck.test.ts +46 -0
  198. package/src/lifecycle-waits.parity.test.ts +319 -0
  199. package/src/marketing.yaml +108 -4
  200. package/src/meetings-knowledge.yaml +18 -0
  201. package/src/networking.yaml +23 -2
  202. package/src/operations.yaml +304 -4
  203. package/src/pipeline-views.template.test.ts +88 -0
  204. package/src/playbook-goal-placeholders.test.ts +57 -0
  205. package/src/postiz-contract.test.ts +134 -0
  206. package/src/presentation-declarations.test.ts +174 -0
  207. package/src/process-contract.validate.test.ts +352 -0
  208. package/src/project-management.yaml +39 -740
  209. package/src/publish-body.d.mts +29 -0
  210. package/src/publish-body.mjs +74 -0
  211. package/src/publish-body.test.ts +74 -0
  212. package/src/publish-official-lib.test.ts +176 -0
  213. package/src/replicate-rvm.test.ts +504 -0
  214. package/src/research-base.yaml +119 -20
  215. package/src/research-decision.yaml +60 -3
  216. package/src/reserved-profile-slugs.test.ts +83 -0
  217. package/src/retired-goal-kinds.test.ts +58 -0
  218. package/src/role-reference-and-step-domain.test.ts +192 -0
  219. package/src/seed-obligation.test.ts +176 -0
  220. package/src/send-message-maps.test.ts +227 -0
  221. package/src/shared-role-ssot.test.ts +42 -1
  222. package/src/ship-a-change.template.test.ts +177 -0
  223. package/src/skills.test.ts +170 -0
  224. package/src/social.yaml +85 -27
  225. package/src/space-packages.freshness.test.ts +97 -0
  226. package/src/space-packages.generated.ts +40 -0
  227. package/src/space-packages.test.ts +125 -0
  228. package/src/space-packages.ts +73 -0
  229. package/src/start.test.ts +188 -0
  230. package/src/task-intents.test.ts +293 -0
  231. package/src/templates.ts +21639 -21522
  232. package/src/types.ts +443 -60
  233. package/src/unipile-publish-post.test.ts +162 -0
  234. package/src/validate-flow.drift.test.ts +2 -3
  235. package/src/validate-flow.ts +32 -2
  236. package/src/validate-onboarding.test.ts +59 -0
  237. package/src/validate.test.ts +156 -3
  238. package/src/validate.ts +1384 -7
  239. package/src/wire-core.ts +231 -0
  240. package/src/wire.test.ts +159 -0
  241. package/src/wire.ts +107 -0
  242. package/src/__fixtures__/report-automation-flow.json +0 -393
  243. package/src/agent-fleet.yaml +0 -1645
  244. package/src/business-developer.template.test.ts +0 -351
  245. package/src/business-developer.yaml +0 -860
  246. package/src/content-studio.yaml +0 -361
  247. package/src/dev-dashboard.yaml +0 -990
  248. package/src/outreach-comms.yaml +0 -486
  249. package/src/the-arch.yaml +0 -201
@@ -0,0 +1,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
+ });
@@ -8,14 +8,14 @@
8
8
  * seeders be deleted (Wave 4) without silently changing what every workspace
9
9
  * gets.
10
10
  *
11
- * FIDELITY MECHANISM. The report prompts are the product — a single altered
12
- * character is a regression. Importing `REPORT_AUTOMATION_FLOW` from
13
- * `@synap/database` into this frontend package would be an upward layering
14
- * violation, so instead the byte-for-byte guardrail is a COMMITTED, GENERATED
15
- * fixture (`__fixtures__/report-automation-flow.json`) snapshotted from the
16
- * backend const at authoring time, never hand-typed. The chain the test proves:
17
- * compiled base flow === fixture (=== REPORT_AUTOMATION_FLOW at capture).
18
- * Same for the 23-ish relation defs via `__fixtures__/default-relation-defs.json`.
11
+ * FIDELITY MECHANISM. base.yaml is now the SINGLE source of the report flow —
12
+ * the backend `REPORT_AUTOMATION_FLOW` const it was once snapshotted against was
13
+ * retired, and with it the `__fixtures__/report-automation-flow.json` copy-pin
14
+ * (a copy pinned to a copy caught no upstream drift). The report flow is guarded
15
+ * here directly instead: the prompt-fragment smoke check below proves the product
16
+ * content survived, and `templates-freshness.test.ts` pins templates.ts == the
17
+ * YAML source. The 23-ish relation defs KEEP their byte-for-byte fixture
18
+ * guardrail (`__fixtures__/default-relation-defs.json`).
19
19
  */
20
20
 
21
21
  import { readFileSync } from "fs";
@@ -55,18 +55,13 @@ describe("Task A — report automation, byte-faithful", () => {
55
55
  expect(automation?.description).toMatch(/Gather this workspace's state/);
56
56
  });
57
57
 
58
- it("flow BYTE-MATCHES the REPORT_AUTOMATION_FLOW snapshot", () => {
59
- const fixture = readFixture("report-automation-flow.json");
60
- // deep-equal: the compiled template's flow is JSON.stringify(parsed YAML), so
61
- // a deep-equal against the JSON snapshot of the source const proves the
62
- // transcription is lossless (nodes, edges, and every prompt string).
63
- expect(automation?.flow).toEqual(fixture);
64
- });
65
-
66
58
  it("preserves the load-bearing hardened prompt fragments (v13)", () => {
67
- // A coarse smoke check that the specific product content survived — the
68
- // deep-equal above is authoritative, but these named fragments make a
69
- // regression legible if the fixture is ever regenerated wrongly.
59
+ // The primary guardrail that the report flow's product content survived in
60
+ // base.yaml. (A byte-for-byte fixture pin used to sit above this, snapshotting
61
+ // the retired backend `REPORT_AUTOMATION_FLOW` const; with that const gone
62
+ // base.yaml is the single source, so a copy-pinned-to-a-copy proved nothing
63
+ // and was dropped. `templates-freshness.test.ts` pins templates.ts == the
64
+ // YAML source.)
70
65
  const flowStr = JSON.stringify(automation?.flow);
71
66
  for (const marker of [
72
67
  "COUNTS — the exact number of records gathered",
@@ -77,6 +72,17 @@ describe("Task A — report automation, byte-faithful", () => {
77
72
  expect(flowStr).toContain(marker);
78
73
  }
79
74
  });
75
+
76
+ it("carries the v15 retry policy on all four AI rounds (regression guard)", () => {
77
+ // v15's fix for the 2026-08-03 finishReason=length incident: each of the four
78
+ // ai.generate rounds (analyze/relate/assemble/summarize) gets ONE retry with a
79
+ // 5s delay. This field dropped SILENTLY once — the 700→2000 fix ported maxTokens
80
+ // into base.yaml but not the retry policy, so it went missing on the reconcile-
81
+ // winning path. Assert it here (base.yaml is the SSOT now) so it can't drift again.
82
+ const flowStr = JSON.stringify(automation?.flow);
83
+ expect((flowStr.match(/"retryDelay":5000/g) ?? []).length).toBe(4);
84
+ expect((flowStr.match(/"maxRetries":1(?=[,}])/g) ?? []).length).toBe(4);
85
+ });
80
86
  });
81
87
 
82
88
  describe("Task B — relation defs, full coverage", () => {
package/src/base.yaml CHANGED
@@ -458,6 +458,8 @@ automations:
458
458
  maxTokens: "2000"
459
459
  errorHandling:
460
460
  continueOnError: true
461
+ maxRetries: 1
462
+ retryDelay: 5000
461
463
  - id: relate
462
464
  type: capability
463
465
  position:
@@ -540,6 +542,8 @@ automations:
540
542
  maxTokens: "2000"
541
543
  errorHandling:
542
544
  continueOnError: true
545
+ maxRetries: 1
546
+ retryDelay: 5000
543
547
  - id: assemble
544
548
  type: capability
545
549
  position:
@@ -581,15 +585,17 @@ automations:
581
585
  3. The outer container must always have STRICTLY MORE colons than anything nested
582
586
  inside it. Four outside, three inside. If you ever nest a four-colon block in a
583
587
  four-colon block the section closes early and the rest of the report leaks out.
584
- 4. ATTRIBUTE VALUES MUST BE QUOTED. An UNQUOTED value ends at the first } it
585
- contains, which silently truncates the attribute — so always quote. Use
586
- DOUBLE quotes for a plain value, and SINGLE quotes when the value is itself
587
- a JSON object, so the JSON's own double quotes survive:
588
- :::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
+ ```
589
595
  :::
590
- A single-quoted value may contain both " and } — those are only special to
591
- an unquoted value. Write the JSON compact, on one line, with no trailing
592
- 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.
593
599
  5. The ONLY section attributes that survive are: id, agent, round, skills,
594
600
  confidence, stepRunId, nodeId, status. Anything else is silently dropped.
595
601
  6. confidence is a 0-1 ratio written as a decimal string, e.g. confidence="0.8".
@@ -607,12 +613,13 @@ automations:
607
613
  8. NEVER EMIT :::synap-view, AND NEVER EMIT :::synap-cell WITH AN instanceId.
608
614
  Both need an id you have not been given and cannot obtain, and a made-up
609
615
  reference produces a permanent broken block in a document a human reads.
610
- 9. YOU MAY EMIT A CHART :::synap-cell — from this allowlist ONLY, configured
611
- through cellProps. These cells fetch their own data at render time, so they
612
- need no id, and they make the report a live surface instead of a text file:
613
- - :::synap-cell{cellKey="stat-card" cellProps='{"profileSlug":"<kind>","aggregation":"count","timePeriod":"week","label":"<Kind> created this week"}'}
614
- - :::synap-cell{cellKey="chart-pie" cellProps='{"profileSlug":"<kind>","groupBy":"<property>","label":"<the finding this shows>"}'}
615
- - :::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>"}
616
623
  Rules, all mandatory:
617
624
  (a) profileSlug must be one of the kinds gathered for THIS report: task,
618
625
  note, person, company. A chart over any other kind is a claim this
@@ -633,28 +640,29 @@ automations:
633
640
  nothing here. If no gathered kind has a distribution worth showing,
634
641
  emit no cells at all — that is a correct report, not a lesser one.
635
642
  (e) AT MOST ONE cell per section, and NEVER at the top of a section.
636
- Place it immediately AFTER the sentence making the claim it evidences,
637
- so the reader has the claim before the picture. A chart that opens a
638
- section makes the reader decode a graphic to find out what the section
639
- is about, which is the delay the ## heading and the claim sentence
640
- exist to remove.
641
- (f) The label must state the FINDING, not the dimension. Write
642
- label:"Half the open tasks are blocked", never label:"Distribution",
643
- "Status" or "Tasks by status". Always set label explicitly: with none,
644
- a pie chart titles itself "Distribution" and a bar chart "Breakdown" —
645
- words true of every such chart ever drawn, which tell the reader
646
- nothing they cannot already see in the shapes.
647
- (g) A CHART IS A LIVE VIEW, THE PROSE IS A SNAPSHOT. These cells run
648
- their own query when the report is OPENED, which may be long after
649
- this text was written — so a sentence quoting a count and a chart
650
- measuring the same thing WILL eventually disagree, and the reader
651
- cannot tell which is stale. Never write a specific number in a
652
- sentence that sits beside a chart of the same data: let the chart
653
- carry the number and let the sentence carry the claim
654
- ("most open tasks are stalled at review", not "12 of 19 are stalled").
655
- And if the sentence already gives the number the chart would show,
656
- DROP THE CHART — one of them is redundant, and the sentence is the one
657
- 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.
658
666
  Everything else this report needs is ::::synap-section containers plus
659
667
  [[entity:…]] chips inline.
660
668
 
@@ -671,6 +679,11 @@ automations:
671
679
  section whose heading merely names the round tells the reader nothing
672
680
  they cannot already see.
673
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
+
674
687
  STRUCTURE: one ::::synap-section per round, in order — analyze, then relate.
675
688
  Open with a single "# " title line ABOVE the first section, and open each
676
689
  section with its own ## claim heading per rule 10.
@@ -698,8 +711,14 @@ automations:
698
711
  majority are sitting in one status — the oldest is
699
712
  [[entity:3f2a91c4-7b10-4e55-9c02-8ad1f6e4b2d7|Migrate the billing worker]].
700
713
  Notes are thin.
701
- :::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
+ ```
702
719
  :::
720
+
721
+ Review is where open tasks accumulate.
703
722
  ::::
704
723
 
705
724
  ::::synap-section{agent="analyst" round="relate" status="failed" confidence="0.2"}
@@ -724,6 +743,9 @@ automations:
724
743
  RELATE ROUND MATERIAL:
725
744
  {{steps.relate.output}}
726
745
  maxTokens: "2000"
746
+ errorHandling:
747
+ maxRetries: 1
748
+ retryDelay: 5000
727
749
  - id: summarize
728
750
  type: capability
729
751
  position:
@@ -742,6 +764,9 @@ automations:
742
764
  Material to summarize (may be an error object if the round failed):
743
765
  {{steps.analyze.output}}
744
766
  maxTokens: "500"
767
+ errorHandling:
768
+ maxRetries: 1
769
+ retryDelay: 5000
745
770
  - id: guard-assembled
746
771
  type: guard
747
772
  position:
@@ -756,6 +781,17 @@ automations:
756
781
  - path: steps.assemble.output.error
757
782
  exists: false
758
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}}"
759
795
  - id: create-report
760
796
  type: output
761
797
  position:
@@ -777,11 +813,25 @@ automations:
777
813
  - Notes
778
814
  - People
779
815
  - Companies
780
- body: "{{steps.assemble.output}}"
816
+ body: "{{steps.freeze-charts.output.markdown}}"
781
817
  systemData:
782
818
  renderer:
783
819
  kind: cell
784
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
785
835
  edges:
786
836
  - id: e-trigger-now
787
837
  source: trigger
@@ -834,6 +884,12 @@ automations:
834
884
  - id: e-summarize-guard
835
885
  source: summarize
836
886
  target: guard-assembled
837
- - id: e-guard-create
887
+ - id: e-guard-freeze
838
888
  source: guard-assembled
889
+ target: freeze-charts
890
+ - id: e-freeze-create
891
+ source: freeze-charts
839
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]