@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,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content OS pin strategy — Produce Content is pinned; the weekly cron that
|
|
3
|
+
* runs it ships paused; Plan Next Content's workers (weekly and by hand) run
|
|
4
|
+
* once over every post and propose. Not a YAML snapshot.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from "fs";
|
|
7
|
+
import { dirname, join } from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import { load } from "js-yaml";
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
import { getWorkspaceTemplate } from "./templates.js";
|
|
12
|
+
import type { TemplatePlaybook, WorkspaceYaml } from "./types.js";
|
|
13
|
+
|
|
14
|
+
const template = (() => {
|
|
15
|
+
const resolved = getWorkspaceTemplate("content-os");
|
|
16
|
+
if (!resolved) throw new Error("content-os template is not registered");
|
|
17
|
+
return resolved;
|
|
18
|
+
})();
|
|
19
|
+
|
|
20
|
+
type Strategy = NonNullable<TemplatePlaybook["inputStrategy"]>;
|
|
21
|
+
|
|
22
|
+
function strategyKind(strategy: Strategy): string {
|
|
23
|
+
return typeof strategy === "string" ? strategy : strategy.kind;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("Content OS pin strategy", () => {
|
|
27
|
+
it("keeps the legacy string form and the pinned object", () => {
|
|
28
|
+
expect(strategyKind("query")).toBe("query");
|
|
29
|
+
expect(strategyKind({ kind: "pinned" })).toBe("pinned");
|
|
30
|
+
// @ts-expect-error pinned entity ids are not part of the strategy
|
|
31
|
+
const rejected: Strategy = { kind: "pinned", pinnedEntityIds: ["post-1"] };
|
|
32
|
+
expect(rejected).toBeDefined();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("pins Produce Content; Plan Next Content runs weekly and by hand, once over every post, proposing", () => {
|
|
36
|
+
const produce = template.playbooks?.find((p) => p.name === "Produce Content");
|
|
37
|
+
const plan = template.playbooks?.find((p) => p.name === "Plan Next Content");
|
|
38
|
+
|
|
39
|
+
expect(produce?.inputStrategy).toEqual({
|
|
40
|
+
kind: "pinned",
|
|
41
|
+
fallback: {
|
|
42
|
+
filter: { "post-status": "Idea" },
|
|
43
|
+
orderBy: "createdAt",
|
|
44
|
+
orderDir: "asc",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
expect(produce).not.toHaveProperty("schedule");
|
|
48
|
+
expect(produce?.inputStrategy).not.toHaveProperty("pinnedEntityIds");
|
|
49
|
+
|
|
50
|
+
// A subject-bound playbook's `schedule:` fans out one run per post: the
|
|
51
|
+
// plan's clock is a per_moment activator instead, never that field.
|
|
52
|
+
expect(plan).not.toHaveProperty("schedule");
|
|
53
|
+
expect(plan).not.toHaveProperty("inputStrategy");
|
|
54
|
+
expect(plan?.subjectProfile).toEqual({ profileSlug: "post" });
|
|
55
|
+
expect(plan?.stages ?? []).toEqual([]);
|
|
56
|
+
expect(plan?.activators).toEqual([
|
|
57
|
+
{ on: "schedule", cron: "0 9 * * 1", mode: "propose" },
|
|
58
|
+
{ on: "manual", size: "per_moment", mode: "propose" },
|
|
59
|
+
]);
|
|
60
|
+
// The plan reads its numbers from the pod, and files dates as proposals.
|
|
61
|
+
expect(plan?.goalTemplate).toContain("platform-posting-freq");
|
|
62
|
+
expect(plan?.goalTemplate).toContain("never apply a date yourself");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("ships one paused playbook_run of Produce Content, with no subject binding", () => {
|
|
66
|
+
// Two automations: this weekly run, and Publish due posts (content-os.publish-due-posts.template.test.ts).
|
|
67
|
+
expect(template.automations?.map((a) => a.key)).toEqual(["content-os-weekly-produce", "content-os-publish-due-posts"]);
|
|
68
|
+
const automation = template.automations?.find((a) => a.key === "content-os-weekly-produce");
|
|
69
|
+
expect(automation).toMatchObject({
|
|
70
|
+
key: "content-os-weekly-produce",
|
|
71
|
+
triggerType: "cron",
|
|
72
|
+
status: "paused",
|
|
73
|
+
triggerConfig: { expression: "0 9 * * 1" },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const nodes = (automation?.flow.nodes ?? []) as Array<{
|
|
77
|
+
id?: string;
|
|
78
|
+
type?: string;
|
|
79
|
+
data?: Record<string, unknown>;
|
|
80
|
+
}>;
|
|
81
|
+
const runs = nodes.filter((n) => n.type === "playbook_run");
|
|
82
|
+
expect(runs).toHaveLength(1);
|
|
83
|
+
expect(runs[0]?.data).toMatchObject({ playbookName: "Produce Content" });
|
|
84
|
+
expect(runs[0]?.data).not.toHaveProperty("paramsMapping");
|
|
85
|
+
expect(runs[0]?.data).not.toHaveProperty("subject");
|
|
86
|
+
expect(runs[0]?.data).not.toHaveProperty("playbookId");
|
|
87
|
+
|
|
88
|
+
expect(automation?.flow.edges).toEqual([
|
|
89
|
+
{ id: "e-trigger-produce", source: "trigger", target: "produce" },
|
|
90
|
+
]);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Content OS is THE Content space (one space per domain), so it — not the
|
|
96
|
+
* superseded content-studio — must declare the video/visual kinds the browser
|
|
97
|
+
* Content app reads (`CONTENT_KINDS`). Without them "New video" fails on a pod
|
|
98
|
+
* with only Content OS: `Profile not found: content-composition`.
|
|
99
|
+
*/
|
|
100
|
+
describe("Content OS owns the video / visual pipeline", () => {
|
|
101
|
+
const profile = (slug: string) => template.profiles.find((p) => p.slug === slug);
|
|
102
|
+
|
|
103
|
+
it("declares content-composition, content-asset and render-job (workspace scope)", () => {
|
|
104
|
+
for (const slug of ["content-composition", "content-asset", "render-job"]) {
|
|
105
|
+
const p = profile(slug);
|
|
106
|
+
expect(p, `content-os must declare ${slug}`).toBeDefined();
|
|
107
|
+
expect(p?.scope ?? "workspace", `${slug} scope`).not.toBe("shared");
|
|
108
|
+
}
|
|
109
|
+
// The fields the Content app and the composition detail read.
|
|
110
|
+
expect(profile("render-job")?.properties.map((d) => d.slug)).toEqual(
|
|
111
|
+
expect.arrayContaining(["job-status", "render-progress", "composition-ref"])
|
|
112
|
+
);
|
|
113
|
+
expect(profile("content-asset")?.properties.map((d) => d.slug)).toContain("asset-kind");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("re-declares brand-template as shared (the grant) and requires brand-library (the SSOT)", () => {
|
|
117
|
+
expect(profile("brand-template")?.scope).toBe("shared");
|
|
118
|
+
expect(
|
|
119
|
+
(template.dependencies ?? []).some(
|
|
120
|
+
(d) => d.slug === "brand-library" && (d.relation ?? "require") === "require"
|
|
121
|
+
)
|
|
122
|
+
).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("ships the pipeline views and links", () => {
|
|
126
|
+
const viewScopes = (template.views ?? []).map((v) => v.scopeProfileSlug);
|
|
127
|
+
expect(viewScopes).toEqual(
|
|
128
|
+
expect.arrayContaining(["content-composition", "content-asset", "render-job"])
|
|
129
|
+
);
|
|
130
|
+
const links = (template.entityLinks ?? []).map(
|
|
131
|
+
(l) => `${l.sourceProfileSlug}-${l.type}->${l.targetProfileSlug}`
|
|
132
|
+
);
|
|
133
|
+
expect(links).toEqual(
|
|
134
|
+
expect.arrayContaining([
|
|
135
|
+
"render-job-renders->content-composition",
|
|
136
|
+
"content-asset-used_in->content-composition",
|
|
137
|
+
"content-asset-used_in->render-job",
|
|
138
|
+
"content-composition-made_for->post",
|
|
139
|
+
])
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("keeps its own home: the content-home cell", () => {
|
|
144
|
+
expect(template.app).toMatchObject({ kind: "stored", cellKey: "content-home" });
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// A stage's `subjectStatus` is written when the run ENTERS it, and the pod holds
|
|
149
|
+
// it only behind a HUMAN gate (a check gate judges the stage being left, so it
|
|
150
|
+
// passes on entry). A status that claims an outside act — the post is live, the
|
|
151
|
+
// issue went out — or that starts ANOTHER process must therefore sit behind a
|
|
152
|
+
// person's approval, or the record says it happened before anyone confirmed it
|
|
153
|
+
// (review M6: Repurpose fired on a post that never shipped).
|
|
154
|
+
describe("Content OS never writes an outside act before a person confirms it", () => {
|
|
155
|
+
const playbooks = (template.playbooks ?? []) as TemplatePlaybook[];
|
|
156
|
+
type Stage = NonNullable<TemplatePlaybook["stages"]>[number];
|
|
157
|
+
const bound: Array<{ playbook: string; stage: Stage }> = playbooks.flatMap((pb) =>
|
|
158
|
+
(pb.stages ?? [])
|
|
159
|
+
.filter((s) => typeof s.subjectStatus === "string")
|
|
160
|
+
.map((stage) => ({ playbook: pb.name, stage })),
|
|
161
|
+
);
|
|
162
|
+
// Values another playbook's activator listens for, per subject kind.
|
|
163
|
+
const activatedElsewhere = (owner: string, kind: string | undefined, status: string) =>
|
|
164
|
+
playbooks.some(
|
|
165
|
+
(pb) =>
|
|
166
|
+
pb.name !== owner &&
|
|
167
|
+
pb.subjectProfile?.profileSlug === kind &&
|
|
168
|
+
(pb.activators ?? []).some((a) => a.on === "enters_status" && a.status === status),
|
|
169
|
+
);
|
|
170
|
+
const kindOf = (name: string) => playbooks.find((p) => p.name === name)?.subjectProfile?.profileSlug;
|
|
171
|
+
|
|
172
|
+
const mustBeHuman = bound.filter(
|
|
173
|
+
({ playbook, stage }) =>
|
|
174
|
+
stage.category === "completed" ||
|
|
175
|
+
activatedElsewhere(playbook, kindOf(playbook), stage.subjectStatus as string),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
it("finds the stages it guards (non-vacuity)", () => {
|
|
179
|
+
expect(mustBeHuman.map(({ playbook, stage }) => `${playbook}/${stage.key}`).sort()).toEqual([
|
|
180
|
+
"Newsletter issue/sent",
|
|
181
|
+
"Produce Content/publish",
|
|
182
|
+
]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("gates each of them on a person", () => {
|
|
186
|
+
for (const { playbook, stage } of mustBeHuman) {
|
|
187
|
+
expect({ at: `${playbook}/${stage.key}`, gate: stage.gate }).toEqual({
|
|
188
|
+
at: `${playbook}/${stage.key}`,
|
|
189
|
+
gate: { kind: "human" },
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("keeps a recorded-evidence criterion on each (URL / date)", () => {
|
|
195
|
+
for (const { playbook, stage } of mustBeHuman) {
|
|
196
|
+
const evidence = (stage.criteria ?? [])
|
|
197
|
+
.map((c) => (c as { check?: { kind?: string; evidenceKey?: string } }).check)
|
|
198
|
+
.filter((c) => c?.kind === "evidence")
|
|
199
|
+
.map((c) => c!.evidenceKey);
|
|
200
|
+
expect({ at: `${playbook}/${stage.key}`, has: evidence.length > 0 }).toEqual({
|
|
201
|
+
at: `${playbook}/${stage.key}`,
|
|
202
|
+
has: true,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// The script step (D4, 2026-10-12): a post is WRITTEN (script / outline) and a
|
|
209
|
+
// person approves that before anything is generated. Read from the YAML SOURCE,
|
|
210
|
+
// not the generated templates.ts, so the assertion sees this file's edit
|
|
211
|
+
// without a rebuild.
|
|
212
|
+
describe("Content OS scripts a post before generating it", () => {
|
|
213
|
+
const source = load(
|
|
214
|
+
readFileSync(join(dirname(fileURLToPath(import.meta.url)), "content-os.yaml"), "utf8"),
|
|
215
|
+
) as WorkspaceYaml;
|
|
216
|
+
const post = source.profiles.find((p) => p.slug === "post");
|
|
217
|
+
const prop = (slug: string) => post?.properties.find((d) => d.slug === slug);
|
|
218
|
+
const produce = (source.playbooks ?? []).find((p) => p.name === "Produce Content") as
|
|
219
|
+
| TemplatePlaybook
|
|
220
|
+
| undefined;
|
|
221
|
+
const stage = (key: string) => produce?.stages?.find((s) => s.key === key);
|
|
222
|
+
|
|
223
|
+
it("puts Scripted between Briefed and In Draft, in the enum and the lifecycle", () => {
|
|
224
|
+
const order = ["Idea", "Briefed", "Scripted", "In Draft", "Review", "Scheduled", "Published", "Archived"];
|
|
225
|
+
expect(prop("post-status")?.enumValues).toEqual(order);
|
|
226
|
+
expect(post?.lifecycle?.options.map((o) => o.key)).toEqual(order);
|
|
227
|
+
expect(post?.lifecycle?.options.find((o) => o.key === "Scripted")?.category).toBe("active");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("declares post-goal with the shared goal enum", () => {
|
|
231
|
+
expect(prop("post-goal")).toMatchObject({ inputType: "select" });
|
|
232
|
+
expect(prop("post-goal")?.enumValues).toEqual([
|
|
233
|
+
"sell",
|
|
234
|
+
"hot-take",
|
|
235
|
+
"tutorial",
|
|
236
|
+
"product-demo",
|
|
237
|
+
"announcement",
|
|
238
|
+
"proof",
|
|
239
|
+
"behind-the-scenes",
|
|
240
|
+
"community",
|
|
241
|
+
]);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("runs a script stage at Scripted, right after the brief, producing a Script document", () => {
|
|
245
|
+
expect(produce?.stages?.map((s) => s.key)).toEqual([
|
|
246
|
+
"brief",
|
|
247
|
+
"script",
|
|
248
|
+
"draft",
|
|
249
|
+
"review",
|
|
250
|
+
"schedule",
|
|
251
|
+
"publish",
|
|
252
|
+
]);
|
|
253
|
+
expect(stage("script")?.subjectStatus).toBe("Scripted");
|
|
254
|
+
expect(stage("script")?.expectedOutputs).toEqual([
|
|
255
|
+
expect.objectContaining({ kind: "document", label: "Script" }),
|
|
256
|
+
]);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("holds the draft behind a person's approval of the script, reworking the script on reject", () => {
|
|
260
|
+
expect(stage("draft")?.gate).toEqual({ kind: "human" });
|
|
261
|
+
expect(stage("draft")?.onFail).toEqual({ toStage: "script" });
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Decision B (review 22fec5ee, 3 Nov 2026): an idea is its own kind, linked
|
|
266
|
+
// many-to-many to posts by `feeds`, and "Package ideas" is the door that
|
|
267
|
+
// proposes posts from the bank without writing them.
|
|
268
|
+
describe("Content OS ideas: their own kind, feeding posts", () => {
|
|
269
|
+
const idea = (template.profiles ?? []).find((p) => p.slug === "idea");
|
|
270
|
+
|
|
271
|
+
it("declares the idea kind with one lifecycle and no post fields", () => {
|
|
272
|
+
expect(idea?.lifecycle?.property).toBe("idea-status");
|
|
273
|
+
expect(idea?.lifecycle?.options.map((o) => `${o.key}:${o.category}`)).toEqual([
|
|
274
|
+
"New:open",
|
|
275
|
+
"Shaped:active",
|
|
276
|
+
"Used:done",
|
|
277
|
+
"Dropped:lost",
|
|
278
|
+
]);
|
|
279
|
+
const fields = (idea?.properties ?? []).map((p) => p.slug);
|
|
280
|
+
expect(fields).toEqual(expect.arrayContaining(["idea-status", "content", "idea-pillar", "idea-stage", "idea-tags"]));
|
|
281
|
+
// An idea has no format and no date (that is what makes it not a post).
|
|
282
|
+
expect(fields.some((f) => f.startsWith("post-") || f === "publish-date")).toBe(false);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("links an idea to the posts it feeds, many-to-many", () => {
|
|
286
|
+
const links = (template.entityLinks ?? []).map((l) => `${l.sourceProfileSlug}-${l.type}->${l.targetProfileSlug}`);
|
|
287
|
+
expect(links).toEqual(expect.arrayContaining(["idea-feeds->post", "idea-belongs_to_pillar->pillar"]));
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("packages ideas through a bank-wide playbook that proposes posts, never writes them", () => {
|
|
291
|
+
const pkg = template.playbooks?.find((p) => p.name === "Package ideas");
|
|
292
|
+
expect(pkg?.subjectProfile).toBeUndefined();
|
|
293
|
+
expect(pkg?.expectedOutputs).toEqual([expect.objectContaining({ kind: "post", relationToSubject: "none" })]);
|
|
294
|
+
expect(pkg?.goalTemplate).toMatch(/idea feeds post/);
|
|
295
|
+
expect(pkg?.goalTemplate).toMatch(/ONE synap_capture graph/);
|
|
296
|
+
expect(pkg?.goalTemplate).toMatch(/Never a script/);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// A post sent for another account (L9: one post per extra account, `post-copy-of`)
|
|
301
|
+
// enters Published like its original. Repurpose must still propose once per
|
|
302
|
+
// ORIGINAL: its activator leaves out a post that carries post-copy-of
|
|
303
|
+
// (backend#766 `unless`), and that property is one the post kind declares.
|
|
304
|
+
describe("Content OS Repurpose proposes once per original, never for a copy", () => {
|
|
305
|
+
const playbooks = (template.playbooks ?? []) as TemplatePlaybook[];
|
|
306
|
+
const repurpose = playbooks.find((p) => p.name === "Repurpose");
|
|
307
|
+
|
|
308
|
+
it("finds Repurpose and its Published activator (non-vacuity)", () => {
|
|
309
|
+
expect(repurpose?.subjectProfile?.profileSlug).toBe("post");
|
|
310
|
+
expect(repurpose?.activators?.filter((a) => a.on === "enters_status" && a.status === "Published")).toHaveLength(1);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("leaves out a post that carries post-copy-of", () => {
|
|
314
|
+
const published = repurpose?.activators?.find((a) => a.on === "enters_status" && a.status === "Published");
|
|
315
|
+
expect(published?.unless).toEqual({ property: "post-copy-of", present: true });
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("names a property the post kind declares", () => {
|
|
319
|
+
const post = template.profiles.find((p) => p.slug === "post");
|
|
320
|
+
expect(post?.properties.map((d) => d.slug)).toContain("post-copy-of");
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// "Write the caption" (founder, 5 Nov): Content Studio's "Write it for me" runs it on ONE post. It drafts, never
|
|
325
|
+
// decides: it writes the script's "## Caption draft" section, never the "## Caption" on its own; the person's yes to ONE
|
|
326
|
+
// owed confirm ask (or their Save in the app) makes it the caption. It binds no stage and moves no status.
|
|
327
|
+
describe("Content OS Write the caption drafts one post's caption, never the person's", () => {
|
|
328
|
+
const playbooks = (template.playbooks ?? []) as TemplatePlaybook[];
|
|
329
|
+
const caption = playbooks.find((p) => p.name === "Write the caption");
|
|
330
|
+
|
|
331
|
+
it("is a post's process, run by hand on one post (non-vacuity)", () => {
|
|
332
|
+
expect(caption?.subjectProfile?.profileSlug).toBe("post");
|
|
333
|
+
expect(caption?.params).toEqual([expect.objectContaining({ name: "post", type: "entity", required: true })]);
|
|
334
|
+
expect(caption?.goalTemplate).toContain("{post}");
|
|
335
|
+
expect(caption?.status).toBe("active");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("writes the draft section only, never the saved caption, and never moves the status", () => {
|
|
339
|
+
expect(caption?.goalTemplate).toMatch(/"## Caption draft" section/);
|
|
340
|
+
expect(caption?.goalTemplate).toMatch(/Never write or change the "## Caption" section on\s+your own/);
|
|
341
|
+
// The person's approval is ONE owed confirm ask; only their yes makes the draft the caption.
|
|
342
|
+
expect(caption?.goalTemplate).toMatch(/ONE confirm ask on this session/);
|
|
343
|
+
expect(caption?.goalTemplate).toMatch(/On yes, put the draft in\s+the "## Caption" section/);
|
|
344
|
+
expect(caption?.goalTemplate).toMatch(/Never move the post's status/);
|
|
345
|
+
expect(caption?.stages ?? []).toEqual([]);
|
|
346
|
+
expect(caption?.activators ?? []).toEqual([]);
|
|
347
|
+
expect(caption?.expectedOutputs).toEqual([expect.objectContaining({ kind: "document", label: "Caption draft", relationToSubject: "none" })]);
|
|
348
|
+
});
|
|
349
|
+
});
|