@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.
- package/README.md +483 -0
- package/automations/bookmark-enrichment.automation.json +321 -0
- package/automations/github-pr-events.automation.json +505 -0
- package/automations/mail-feed.automation.json +86 -0
- package/automations/ship-a-change-checks-green.automation.json +106 -0
- package/automations/ship-a-change-checks-red.automation.json +106 -0
- package/automations/ship-a-change-deployed.automation.json +56 -0
- package/automations/ship-a-change-pr-merged.automation.json +110 -0
- package/automations/ship-a-change-pr-opened.automation.json +104 -0
- package/automations/url-bookmark-capture.automation.json +132 -0
- package/capabilities/agency-skills.capability.json +157 -0
- package/capabilities/apify.capability.json +265 -0
- package/capabilities/apollo.capability.json +120 -0
- package/capabilities/arch-backend.capability.json +97 -0
- package/capabilities/arch-client-intelligence.capability.json +401 -0
- package/capabilities/backblaze-b2.capability.json +205 -0
- package/capabilities/cal-com.capability.json +266 -0
- package/capabilities/canva.capability.json +274 -0
- package/capabilities/claude-code-routines.capability.json +172 -0
- package/capabilities/claude-managed-agents.capability.json +215 -0
- package/capabilities/client-comms-report.capability.json +246 -0
- package/capabilities/content-idea-to-production.capability.json +29 -0
- package/capabilities/datagouv.capability.json +315 -0
- package/capabilities/discord-bot.capability.json +157 -0
- package/capabilities/dropbox.capability.json +281 -0
- package/capabilities/exa.capability.json +109 -0
- package/capabilities/fal-ai.capability.json +83 -0
- package/capabilities/fal-media-lab.capability.json +158 -0
- package/capabilities/fireflies.capability.json +191 -0
- package/capabilities/freellmapi.capability.json +114 -0
- package/capabilities/generic-apikey.capability.json +92 -0
- package/capabilities/github-merge.capability.json +99 -0
- package/capabilities/github-read.capability.json +325 -0
- package/capabilities/governance-tighten-recommender.capability.json +81 -0
- package/capabilities/jev.capability.json +226 -0
- package/capabilities/lead-conversion-enrollment.capability.json +246 -0
- package/capabilities/mail-feed.capability.json +277 -0
- package/capabilities/mailgun.capability.json +52 -0
- package/capabilities/nango-google.capability.json +639 -0
- package/capabilities/notion.capability.json +417 -0
- package/capabilities/omniroute.capability.json +137 -0
- package/capabilities/posthog-analytics.capability.json +258 -0
- package/capabilities/postiz.capability.json +467 -0
- package/capabilities/proactive-digest.capability.json +190 -0
- package/capabilities/proton.capability.json +288 -0
- package/capabilities/replicate-rvm.capability.json +97 -0
- package/capabilities/research-methods.capability.json +57 -0
- package/capabilities/shopify.capability.json +395 -0
- package/capabilities/slack.capability.json +369 -0
- package/capabilities/stripe.capability.json +420 -0
- package/capabilities/telegram-bridge.capability.json +56 -0
- package/capabilities/treg.capability.json +102 -0
- package/capabilities/unipile-linkedin.capability.json +471 -0
- package/capabilities/vercel.capability.json +424 -0
- package/dist/define-CPS0n8BV.d.ts +1331 -0
- package/dist/index.d.ts +189 -900
- package/dist/index.js +24654 -21639
- package/dist/index.js.map +1 -1
- package/dist/wire.d.ts +222 -0
- package/dist/wire.js +77 -0
- package/dist/wire.js.map +1 -0
- package/package.json +41 -27
- package/packs/bookmark-views-pack.json +33 -0
- package/packs/crm-pack.json +76 -0
- package/packs/customer-profile.json +28 -0
- package/packs/deep-research.playbook.json +30 -0
- package/packs/event-views-pack.json +33 -0
- package/packs/habit-profile.json +27 -0
- package/packs/hr-pack.json +54 -0
- package/packs/invoice-profile.json +28 -0
- package/packs/knowledge-base-pack.json +56 -0
- package/packs/meeting-profile.json +29 -0
- package/packs/product-profile.json +30 -0
- package/packs/project-management-pack.json +49 -0
- package/packs/recipe-profile.json +29 -0
- package/packs/task-views-pack.json +39 -0
- package/retired.yaml +136 -0
- package/skills/app-factory-build-slice/SKILL.md +70 -0
- package/skills/app-factory-build-slice.catalog.json +17 -0
- package/skills/app-factory-design-canvas/SKILL.md +96 -0
- package/skills/app-factory-design-canvas.catalog.json +17 -0
- package/skills/app-factory-design-review/SKILL.md +99 -0
- package/skills/app-factory-design-review.catalog.json +18 -0
- package/skills/app-factory-research/SKILL.md +123 -0
- package/skills/app-factory-research.catalog.json +17 -0
- package/skills/bookmark-enrichment/SKILL.md +40 -0
- package/skills/carousel-format/carousel-format.skill.json +18 -0
- package/skills/collage-broll/collage-broll.skill.json +20 -0
- package/skills/content-taste/content-taste.skill.json +18 -0
- package/skills/creative-director/creative-director.skill.json +18 -0
- package/skills/design-critique/SKILL.md +135 -0
- package/skills/design-critique.catalog.json +12 -0
- package/skills/designing-with-real-components/SKILL.md +140 -0
- package/skills/designing-with-real-components.catalog.json +12 -0
- package/skills/edit-loop/edit-loop.skill.json +25 -0
- package/skills/facecam-edit/facecam-edit.skill.json +24 -0
- package/skills/mapping-the-server/SKILL.md +54 -0
- package/skills/math-explainer-manimgx/math-explainer-manimgx.skill.json +20 -0
- package/skills/motion-quality-gates/motion-quality-gates.skill.json +20 -0
- package/skills/open-design-edit/open-design-edit.skill.json +24 -0
- package/skills/reading-client-progress/SKILL.md +65 -0
- package/skills/routing-to-channels/SKILL.md +54 -0
- package/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
- package/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
- package/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
- package/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
- package/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
- package/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
- package/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
- package/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
- package/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
- package/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
- package/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
- package/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
- package/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
- package/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
- package/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
- package/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
- package/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
- package/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
- package/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
- package/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
- package/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
- package/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
- package/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
- package/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
- package/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
- package/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
- package/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
- package/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
- package/skills/video-direction/video-direction.skill.json +20 -0
- package/skills/video-style-vocabulary/video-style-vocabulary.skill.json +20 -0
- package/skills/video-toolbox/video-toolbox.skill.json +20 -0
- package/src/__fixtures__/lifecycle-presets.flat-before.json +205 -0
- package/src/agency-os.yaml +122 -0
- package/src/agent-os-skill-slugs.test.ts +65 -0
- package/src/apify-social-reader.test.ts +317 -0
- package/src/base.template.test.ts +25 -19
- package/src/base.yaml +95 -39
- package/src/blockchain-ecosystem.yaml +1 -0
- package/src/brand-library.yaml +525 -14
- package/src/builder-dev-fold.template.test.ts +113 -0
- package/src/builder.yaml +798 -92
- package/src/business-model-catalog-description.test.ts +47 -0
- package/src/business-model-grp.template.test.ts +214 -0
- package/src/business-model.yaml +598 -0
- package/src/catalog-safety.test.ts +198 -0
- package/src/catalog.template-items.test.ts +75 -0
- package/src/catalog.ts +85 -2
- package/src/civic-france.yaml +102 -0
- package/src/concept-guard.test.ts +28 -8
- package/src/concept-guard.ts +2 -2
- package/src/content-creation.template.test.ts +52 -0
- package/src/content-creation.yaml +192 -0
- package/src/content-os.edit-loop.template.test.ts +210 -0
- package/src/content-os.publish-due-posts.template.test.ts +261 -0
- package/src/content-os.template.test.ts +322 -0
- package/src/content-os.video-takes.template.test.ts +338 -0
- package/src/content-os.work-a-take.template.test.ts +125 -0
- package/src/content-os.yaml +1830 -39
- package/src/content-studio-retired.template.test.ts +82 -0
- package/src/corpus-flow-validity.test.ts +4 -5
- package/src/crm-absorbs-outreach.template.test.ts +117 -0
- package/src/crm-build-plan.template.test.ts +128 -0
- package/src/crm-lists.template.test.ts +96 -0
- package/src/crm-messages-cadence.template.test.ts +164 -0
- package/src/crm-one-send-door.template.test.ts +65 -0
- package/src/crm.yaml +1456 -181
- package/src/decisions-wave.template.test.ts +186 -0
- package/src/define.ts +130 -17
- package/src/design-to-implementation.template.test.ts +94 -0
- package/src/dist-freshness.test.ts +97 -0
- package/src/domain-template-type.test.ts +75 -0
- package/src/ecosystem.yaml +27 -66
- package/src/enterprise-os.yaml +10 -134
- package/src/facecam-edit.audio-studio.test.ts +188 -0
- package/src/fal-generate-image.test.ts +200 -0
- package/src/fal-media-lab-pod-files.test.ts +180 -0
- package/src/finance.yaml +254 -1
- package/src/foundation.yaml +130 -28
- package/src/frame-host-modules.d.mts +6 -0
- package/src/frame-host-modules.mjs +58 -0
- package/src/generate.mjs +9 -3
- package/src/github-merge.test.ts +210 -0
- package/src/grants.yaml +57 -17
- package/src/hr.yaml +39 -6
- package/src/index.ts +36 -1
- package/src/intent.test.ts +161 -0
- package/src/internal-runbook.yaml +19 -1
- package/src/jev.test.ts +356 -0
- package/src/legal.yaml +33 -1
- package/src/life-os.yaml +65 -240
- package/src/lifecycle-coverage.test.ts +124 -0
- package/src/lifecycle-declarations.test.ts +252 -0
- package/src/lifecycle-presets.test.ts +468 -0
- package/src/lifecycle-presets.ts +494 -0
- package/src/lifecycle-stuck.test.ts +46 -0
- package/src/lifecycle-waits.parity.test.ts +319 -0
- package/src/marketing.yaml +108 -4
- package/src/meetings-knowledge.yaml +18 -0
- package/src/networking.yaml +23 -2
- package/src/operations.yaml +304 -4
- package/src/pipeline-views.template.test.ts +88 -0
- package/src/playbook-goal-placeholders.test.ts +57 -0
- package/src/postiz-contract.test.ts +134 -0
- package/src/presentation-declarations.test.ts +174 -0
- package/src/process-contract.validate.test.ts +352 -0
- package/src/project-management.yaml +39 -740
- package/src/publish-body.d.mts +29 -0
- package/src/publish-body.mjs +74 -0
- package/src/publish-body.test.ts +74 -0
- package/src/publish-official-lib.test.ts +176 -0
- package/src/replicate-rvm.test.ts +504 -0
- package/src/research-base.yaml +119 -20
- package/src/research-decision.yaml +60 -3
- package/src/reserved-profile-slugs.test.ts +83 -0
- package/src/retired-goal-kinds.test.ts +58 -0
- package/src/role-reference-and-step-domain.test.ts +192 -0
- package/src/seed-obligation.test.ts +176 -0
- package/src/send-message-maps.test.ts +227 -0
- package/src/shared-role-ssot.test.ts +42 -1
- package/src/ship-a-change.template.test.ts +177 -0
- package/src/skills.test.ts +170 -0
- package/src/social.yaml +85 -27
- package/src/space-packages.freshness.test.ts +97 -0
- package/src/space-packages.generated.ts +40 -0
- package/src/space-packages.test.ts +125 -0
- package/src/space-packages.ts +73 -0
- package/src/start.test.ts +188 -0
- package/src/task-intents.test.ts +293 -0
- package/src/templates.ts +21639 -21522
- package/src/types.ts +443 -60
- package/src/unipile-publish-post.test.ts +162 -0
- package/src/validate-flow.drift.test.ts +2 -3
- package/src/validate-flow.ts +32 -2
- package/src/validate-onboarding.test.ts +59 -0
- package/src/validate.test.ts +156 -3
- package/src/validate.ts +1384 -7
- package/src/wire-core.ts +231 -0
- package/src/wire.test.ts +159 -0
- package/src/wire.ts +107 -0
- package/src/__fixtures__/report-automation-flow.json +0 -393
- package/src/agent-fleet.yaml +0 -1645
- package/src/business-developer.template.test.ts +0 -351
- package/src/business-developer.yaml +0 -860
- package/src/content-studio.yaml +0 -361
- package/src/dev-dashboard.yaml +0 -990
- package/src/outreach-comms.yaml +0 -486
- 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.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
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.
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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,
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
-
|
|
615
|
-
-
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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]
|