@synap-core/workspace-templates 0.12.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +509 -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 +1496 -0
- package/capabilities/telegram-bridge.capability.json +56 -0
- package/capabilities/treg.capability.json +102 -0
- package/capabilities/unipile-linkedin.capability.json +494 -0
- package/capabilities/vercel.capability.json +424 -0
- package/dist/define-DqoItRiR.d.ts +1370 -0
- package/dist/index.d.ts +174 -900
- package/dist/index.js +26447 -24075
- 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 +27 -4
- 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 +105 -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 +138 -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.yaml +85 -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 +779 -91
- 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 +296 -49
- package/src/catalog-safety.test.ts +198 -0
- package/src/catalog.template-items.test.ts +75 -0
- package/src/catalog.ts +66 -2
- 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 +349 -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 +1873 -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 +246 -0
- package/src/crm-one-send-door.template.test.ts +65 -0
- package/src/crm.yaml +1563 -181
- package/src/decisions-wave.template.test.ts +186 -0
- package/src/define.ts +146 -17
- package/src/design-to-implementation.template.test.ts +94 -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 +78 -38
- 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 +46 -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 +51 -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 +81 -0
- package/src/retired-goal-kinds.test.ts +52 -0
- package/src/role-reference-and-step-domain.test.ts +192 -0
- package/src/seed-obligation.test.ts +1 -6
- package/src/send-message-maps.test.ts +227 -0
- package/src/shared-role-ssot.test.ts +40 -2
- 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/stripe-capability.test.ts +192 -0
- package/src/task-intents.test.ts +293 -0
- package/src/template-documents.test.ts +158 -0
- package/src/templates.ts +20907 -21464
- package/src/types.ts +475 -61
- package/src/unipile-linkedin-find-profile.test.ts +183 -0
- package/src/unipile-publish-post.test.ts +162 -0
- package/src/validate-flow.drift.test.ts +2 -3
- package/src/validate-flow.ts +22 -2
- package/src/validate-onboarding.test.ts +59 -0
- package/src/validate.test.ts +71 -4
- package/src/validate.ts +1416 -7
- package/src/wire-core.ts +231 -0
- package/src/wire.test.ts +159 -0
- package/src/wire.ts +107 -0
- package/views/bookmark-views-pack.view.json +39 -0
- package/views/event-views-pack.view.json +45 -0
- package/views/task-views-pack.view.json +53 -0
- 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
|
+
});
|
package/src/base.yaml
CHANGED
|
@@ -585,15 +585,17 @@ automations:
|
|
|
585
585
|
3. The outer container must always have STRICTLY MORE colons than anything nested
|
|
586
586
|
inside it. Four outside, three inside. If you ever nest a four-colon block in a
|
|
587
587
|
four-colon block the section closes early and the rest of the report leaks out.
|
|
588
|
-
4.
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
588
|
+
4. ATTRIBUTES ARE REFERENCES ONLY, ALWAYS DOUBLE-QUOTED: cellKey="…", id="…".
|
|
589
|
+
A cell's settings NEVER go in an attribute. They go in a ```json block,
|
|
590
|
+
the first thing inside the embed, compact on one line, a JSON object:
|
|
591
|
+
:::synap-cell{cellKey="chart-pie"}
|
|
592
|
+
```json
|
|
593
|
+
{"profileSlug":"task","groupBy":"status","label":"Open tasks by status"}
|
|
594
|
+
```
|
|
593
595
|
:::
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
596
|
+
Inside that block ordinary JSON rules apply (an apostrophe is fine).
|
|
597
|
+
Malformed JSON is shown to the reader as a broken cell. Never write the
|
|
598
|
+
old cellProps='…' attribute form.
|
|
597
599
|
5. The ONLY section attributes that survive are: id, agent, round, skills,
|
|
598
600
|
confidence, stepRunId, nodeId, status. Anything else is silently dropped.
|
|
599
601
|
6. confidence is a 0-1 ratio written as a decimal string, e.g. confidence="0.8".
|
|
@@ -611,12 +613,13 @@ automations:
|
|
|
611
613
|
8. NEVER EMIT :::synap-view, AND NEVER EMIT :::synap-cell WITH AN instanceId.
|
|
612
614
|
Both need an id you have not been given and cannot obtain, and a made-up
|
|
613
615
|
reference produces a permanent broken block in a document a human reads.
|
|
614
|
-
9. YOU MAY EMIT A CHART :::synap-cell — from this allowlist ONLY,
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
-
|
|
619
|
-
-
|
|
616
|
+
9. YOU MAY EMIT A CHART :::synap-cell — from this allowlist ONLY, each with its
|
|
617
|
+
settings in the ```json block of rule 4. These cells need no id: their data
|
|
618
|
+
is read from the settings in the json block, never written by you. The three
|
|
619
|
+
cells and their json blocks:
|
|
620
|
+
- cellKey="stat-card" with {"profileSlug":"<kind>","aggregation":"count","timePeriod":"week","label":"<Kind> created this week"}
|
|
621
|
+
- cellKey="chart-pie" with {"profileSlug":"<kind>","groupBy":"<property>","label":"<Kind> by <property>"}
|
|
622
|
+
- cellKey="chart-bar" with {"profileSlug":"<kind>","mode":"category","groupBy":"<property>","label":"<Kind> by <property>"}
|
|
620
623
|
Rules, all mandatory:
|
|
621
624
|
(a) profileSlug must be one of the kinds gathered for THIS report: task,
|
|
622
625
|
note, person, company. A chart over any other kind is a claim this
|
|
@@ -637,28 +640,29 @@ automations:
|
|
|
637
640
|
nothing here. If no gathered kind has a distribution worth showing,
|
|
638
641
|
emit no cells at all — that is a correct report, not a lesser one.
|
|
639
642
|
(e) AT MOST ONE cell per section, and NEVER at the top of a section.
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
643
|
+
The ## claim heading and at least one sentence of context come first.
|
|
644
|
+
Then the cell. Then, IMMEDIATELY AFTER the cell's closing ::: line,
|
|
645
|
+
one prose sentence stating the FINDING the chart shows
|
|
646
|
+
("Review is where open tasks accumulate."). The finding lives in
|
|
647
|
+
prose, where it can be read, quoted and edited — never inside the
|
|
648
|
+
cell.
|
|
649
|
+
(f) The json block carries STRUCTURE ONLY: profileSlug, aggregation,
|
|
650
|
+
timePeriod, mode, groupBy, field, and a SHORT label naming what is
|
|
651
|
+
measured ("Open tasks by status", "Notes created this week"). The
|
|
652
|
+
label is a name, not a sentence: no verb, no finding. Always set the
|
|
653
|
+
label: with none, a pie chart titles itself "Distribution" and a bar
|
|
654
|
+
chart "Breakdown".
|
|
655
|
+
(g) YOU WRITE THE CHART'S QUESTION, NEVER ITS NUMBERS. Never put a
|
|
656
|
+
"data" or "capturedAt" key in a json block. After you finish, the
|
|
657
|
+
pipeline runs each chart's own query and stores the result in the
|
|
658
|
+
embed as a dated snapshot, so the chart shows the workspace as it
|
|
659
|
+
was when this report was generated. You never see those numbers,
|
|
660
|
+
so never write a specific number in a sentence that sits beside a
|
|
661
|
+
chart of the same data: let the chart carry the number and let the
|
|
662
|
+
sentence carry the claim ("most open tasks are stalled at review",
|
|
663
|
+
not "12 of 19 are stalled"). And if the sentence already gives the
|
|
664
|
+
number the chart would show, DROP THE CHART — one of them is
|
|
665
|
+
redundant.
|
|
662
666
|
Everything else this report needs is ::::synap-section containers plus
|
|
663
667
|
[[entity:…]] chips inline.
|
|
664
668
|
|
|
@@ -675,6 +679,11 @@ automations:
|
|
|
675
679
|
section whose heading merely names the round tells the reader nothing
|
|
676
680
|
they cannot already see.
|
|
677
681
|
|
|
682
|
+
LAYOUT: put ONE blank line between blocks (paragraphs, headings, embeds) and
|
|
683
|
+
none directly after a section's opening line or before its closing ::::, as
|
|
684
|
+
in the worked example — that is the layout the document editor writes back,
|
|
685
|
+
so the report opens in it for editing.
|
|
686
|
+
|
|
678
687
|
STRUCTURE: one ::::synap-section per round, in order — analyze, then relate.
|
|
679
688
|
Open with a single "# " title line ABOVE the first section, and open each
|
|
680
689
|
section with its own ## claim heading per rule 10.
|
|
@@ -702,8 +711,14 @@ automations:
|
|
|
702
711
|
majority are sitting in one status — the oldest is
|
|
703
712
|
[[entity:3f2a91c4-7b10-4e55-9c02-8ad1f6e4b2d7|Migrate the billing worker]].
|
|
704
713
|
Notes are thin.
|
|
705
|
-
|
|
714
|
+
|
|
715
|
+
:::synap-cell{cellKey="chart-pie"}
|
|
716
|
+
```json
|
|
717
|
+
{"profileSlug":"task","groupBy":"status","label":"Open tasks by status"}
|
|
718
|
+
```
|
|
706
719
|
:::
|
|
720
|
+
|
|
721
|
+
Review is where open tasks accumulate.
|
|
707
722
|
::::
|
|
708
723
|
|
|
709
724
|
::::synap-section{agent="analyst" round="relate" status="failed" confidence="0.2"}
|
|
@@ -766,6 +781,17 @@ automations:
|
|
|
766
781
|
- path: steps.assemble.output.error
|
|
767
782
|
exists: false
|
|
768
783
|
message: The assembler round failed — refusing to write a report without a body.
|
|
784
|
+
- id: freeze-charts
|
|
785
|
+
type: capability
|
|
786
|
+
position:
|
|
787
|
+
x: 1740
|
|
788
|
+
"y": 320
|
|
789
|
+
data:
|
|
790
|
+
label: Freeze charts
|
|
791
|
+
verbId: document.freeze_charts
|
|
792
|
+
verbKind: read
|
|
793
|
+
inputMapping:
|
|
794
|
+
markdown: "{{steps.assemble.output}}"
|
|
769
795
|
- id: create-report
|
|
770
796
|
type: output
|
|
771
797
|
position:
|
|
@@ -787,11 +813,25 @@ automations:
|
|
|
787
813
|
- Notes
|
|
788
814
|
- People
|
|
789
815
|
- Companies
|
|
790
|
-
body: "{{steps.
|
|
816
|
+
body: "{{steps.freeze-charts.output.markdown}}"
|
|
791
817
|
systemData:
|
|
792
818
|
renderer:
|
|
793
819
|
kind: cell
|
|
794
820
|
cellKey: "{{trigger.payload.renderer}}"
|
|
821
|
+
- id: stamp-chart-diagnostics
|
|
822
|
+
type: capability
|
|
823
|
+
position:
|
|
824
|
+
x: 2100
|
|
825
|
+
"y": 320
|
|
826
|
+
data:
|
|
827
|
+
label: Record charts left live
|
|
828
|
+
verbId: document.stamp_diagnostics
|
|
829
|
+
verbKind: write
|
|
830
|
+
inputMapping:
|
|
831
|
+
documentId: "{{steps.create-report.output.documentId}}"
|
|
832
|
+
items: "{{steps.freeze-charts.output.diagnostics}}"
|
|
833
|
+
errorHandling:
|
|
834
|
+
continueOnError: true
|
|
795
835
|
edges:
|
|
796
836
|
- id: e-trigger-now
|
|
797
837
|
source: trigger
|
|
@@ -844,6 +884,12 @@ automations:
|
|
|
844
884
|
- id: e-summarize-guard
|
|
845
885
|
source: summarize
|
|
846
886
|
target: guard-assembled
|
|
847
|
-
- id: e-guard-
|
|
887
|
+
- id: e-guard-freeze
|
|
848
888
|
source: guard-assembled
|
|
889
|
+
target: freeze-charts
|
|
890
|
+
- id: e-freeze-create
|
|
891
|
+
source: freeze-charts
|
|
849
892
|
target: create-report
|
|
893
|
+
- id: e-create-stamp
|
|
894
|
+
source: create-report
|
|
895
|
+
target: stamp-chart-diagnostics
|
|
@@ -433,6 +433,7 @@ onboarding:
|
|
|
433
433
|
who's providing liquidity, who's auditing, who's building, and who's amplifying.
|
|
434
434
|
collect:
|
|
435
435
|
- profileSlug: market-maker
|
|
436
|
+
min: 1
|
|
436
437
|
what: The market-makers / liquidity providers you work with, tagged onto the real company.
|
|
437
438
|
cardinality: few
|
|
438
439
|
keyFields: [status, exchanges]
|