@synap-core/workspace-templates 0.12.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +174 -900
- package/dist/index.js +26148 -23931
- 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/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.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 +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/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 +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 +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/task-intents.test.ts +293 -0
- package/src/templates.ts +20851 -21496
- 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 +22 -2
- package/src/validate-onboarding.test.ts +59 -0
- package/src/validate.test.ts +71 -4
- package/src/validate.ts +1335 -7
- package/src/wire-core.ts +231 -0
- package/src/wire.test.ts +159 -0
- package/src/wire.ts +107 -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,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentation DECLARATIONS (entity rendering, decision 8) — a kind declares
|
|
3
|
+
* how it renders on its catalog profile; the pod stores it as
|
|
4
|
+
* `uiHints.presentation`.
|
|
5
|
+
*
|
|
6
|
+
* Pinned:
|
|
7
|
+
* - the whole corpus validates with zero `presentation-*` errors;
|
|
8
|
+
* - the declared kinds carry the family the plan assigns (non-vacuity: the
|
|
9
|
+
* corpus really declares them, so a deleted block fails here);
|
|
10
|
+
* - every declaration parses under the ONE parser (`@synap-core/types/presentation`);
|
|
11
|
+
* - each rule fires on a broken clone (self-checks): parser issue, unknown
|
|
12
|
+
* property, entityRef on a non-entity_id property, divergence across
|
|
13
|
+
* templates. Absent on another declaration is NOT a divergence.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, expect, it } from "vitest";
|
|
16
|
+
import { parseProfilePresentation } from "@synap-core/types/presentation";
|
|
17
|
+
import { validateTemplate } from "./validate.js";
|
|
18
|
+
import type { WorkspaceYaml } from "./types.js";
|
|
19
|
+
import { readYamlTemplates } from "./generate.mjs";
|
|
20
|
+
|
|
21
|
+
const corpus = readYamlTemplates().map((t) => t.data as WorkspaceYaml);
|
|
22
|
+
const tpl = (slug: string) => corpus.find((t) => t.meta.slug === slug)!;
|
|
23
|
+
const presentationErrors = (t: WorkspaceYaml, c: WorkspaceYaml[] = corpus) =>
|
|
24
|
+
validateTemplate(t, { corpus: c }).errors.filter((e) =>
|
|
25
|
+
e.rule.startsWith("presentation-")
|
|
26
|
+
);
|
|
27
|
+
const clone = <T>(v: T): T => JSON.parse(JSON.stringify(v)) as T;
|
|
28
|
+
|
|
29
|
+
describe("presentation declarations — the corpus", () => {
|
|
30
|
+
it("validates with zero presentation-* errors", () => {
|
|
31
|
+
for (const t of corpus)
|
|
32
|
+
expect(presentationErrors(t), t.meta.slug).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("the plan's kinds declare their family", () => {
|
|
36
|
+
const expected: Array<[string, string, string]> = [
|
|
37
|
+
["brand-library", "brand-asset", "media"],
|
|
38
|
+
["brand-library", "brand-reference", "link"],
|
|
39
|
+
["brand-library", "content-guideline", "document"],
|
|
40
|
+
["content-os", "render-job", "media"],
|
|
41
|
+
["content-os", "post", "work"],
|
|
42
|
+
["social", "post", "work"],
|
|
43
|
+
["builder-workspace", "ui-design", "media"],
|
|
44
|
+
["life-os", "bookmark", "link"],
|
|
45
|
+
["life-os", "book", "record"],
|
|
46
|
+
];
|
|
47
|
+
for (const [t, profile, family] of expected) {
|
|
48
|
+
const p = tpl(t)?.profiles.find((x) => x.slug === profile);
|
|
49
|
+
expect(p?.presentation?.family, `${t}/${profile}`).toBe(family);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("every declaration parses under the ONE parser", () => {
|
|
54
|
+
let declared = 0;
|
|
55
|
+
for (const t of corpus)
|
|
56
|
+
for (const p of t.profiles ?? []) {
|
|
57
|
+
if (p.presentation === undefined) continue;
|
|
58
|
+
declared++;
|
|
59
|
+
expect(
|
|
60
|
+
parseProfilePresentation(p.presentation).ok,
|
|
61
|
+
`${t.meta.slug}/${p.slug}`
|
|
62
|
+
).toBe(true);
|
|
63
|
+
}
|
|
64
|
+
expect(declared).toBeGreaterThanOrEqual(20);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("presentation declarations — each rule fires (self-checks)", () => {
|
|
69
|
+
const brand = () => clone(tpl("brand-library"));
|
|
70
|
+
const asset = (t: WorkspaceYaml) =>
|
|
71
|
+
t.profiles.find((p) => p.slug === "brand-asset")!;
|
|
72
|
+
|
|
73
|
+
it("presentation-invalid: the parser's issues", () => {
|
|
74
|
+
const t = brand();
|
|
75
|
+
(asset(t) as { presentation: unknown }).presentation = {
|
|
76
|
+
family: "gallery",
|
|
77
|
+
};
|
|
78
|
+
const errs = presentationErrors(t);
|
|
79
|
+
expect(errs.map((e) => e.rule)).toContain("presentation-invalid");
|
|
80
|
+
expect(
|
|
81
|
+
errs.find((e) => e.rule === "presentation-invalid")!.message
|
|
82
|
+
).toMatch(/family/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("presentation-property-unknown: a media property the profile lacks", () => {
|
|
86
|
+
const t = brand();
|
|
87
|
+
asset(t).presentation = {
|
|
88
|
+
family: "media",
|
|
89
|
+
media: { source: { property: "no-such-field" } },
|
|
90
|
+
};
|
|
91
|
+
expect(presentationErrors(t).map((e) => e.rule)).toContain(
|
|
92
|
+
"presentation-property-unknown"
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("presentation-property-unknown: a card field the profile lacks", () => {
|
|
97
|
+
const t = brand();
|
|
98
|
+
asset(t).presentation = {
|
|
99
|
+
family: "media",
|
|
100
|
+
fields: { card: ["asset-kind", "nope"] },
|
|
101
|
+
};
|
|
102
|
+
const errs = presentationErrors(t, [t]);
|
|
103
|
+
expect(errs).toHaveLength(1);
|
|
104
|
+
expect(errs[0]).toMatchObject({
|
|
105
|
+
rule: "presentation-property-unknown",
|
|
106
|
+
path: "profiles.brand-asset.presentation.fields.card[1]",
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("presentation-property-unknown: a FALLBACK property the profile lacks, at its own path", () => {
|
|
111
|
+
const t = brand();
|
|
112
|
+
asset(t).presentation = {
|
|
113
|
+
family: "media",
|
|
114
|
+
media: {
|
|
115
|
+
source: { property: "asset-document-id" },
|
|
116
|
+
fallbacks: [{ property: "asset-url" }, { property: "gone" }],
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
const errs = presentationErrors(t, [t]);
|
|
120
|
+
expect(errs).toHaveLength(1);
|
|
121
|
+
expect(errs[0]).toMatchObject({
|
|
122
|
+
rule: "presentation-property-unknown",
|
|
123
|
+
path: "profiles.brand-asset.presentation.media.fallbacks[1].property",
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("brand-asset declares every home of its picture: the upload, then the external URL", () => {
|
|
128
|
+
expect(asset(tpl("brand-library")).presentation?.media).toEqual({
|
|
129
|
+
source: { property: "asset-document-id" },
|
|
130
|
+
fallbacks: [{ property: "asset-url" }],
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("presentation-entityref-type: an entityRef that is not an entity_id", () => {
|
|
135
|
+
const t = brand();
|
|
136
|
+
asset(t).presentation = {
|
|
137
|
+
family: "media",
|
|
138
|
+
media: { source: { entityRef: "asset-kind" } },
|
|
139
|
+
};
|
|
140
|
+
const rules = presentationErrors(t).map((e) => e.rule);
|
|
141
|
+
expect(rules).toContain("presentation-entityref-type");
|
|
142
|
+
// The property exists — its TYPE is wrong; never reported as unknown.
|
|
143
|
+
expect(rules).not.toContain("presentation-property-unknown");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("documentId: true names no property and is accepted", () => {
|
|
147
|
+
const t = brand();
|
|
148
|
+
asset(t).presentation = {
|
|
149
|
+
family: "media",
|
|
150
|
+
media: { source: { documentId: true } },
|
|
151
|
+
};
|
|
152
|
+
expect(presentationErrors(t, [t])).toEqual([]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("presentation-diverges: two templates declare the same kind differently", () => {
|
|
156
|
+
const os = clone(tpl("content-os"));
|
|
157
|
+
os.profiles.find((p) => p.slug === "post")!.presentation = {
|
|
158
|
+
family: "document",
|
|
159
|
+
};
|
|
160
|
+
const rest = corpus.filter((t) => t.meta.slug !== "content-os");
|
|
161
|
+
expect(presentationErrors(os, [...rest, os]).map((e) => e.rule)).toContain(
|
|
162
|
+
"presentation-diverges"
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("absent on another declaration is not a divergence", () => {
|
|
167
|
+
const social = clone(tpl("social"));
|
|
168
|
+
delete social.profiles.find((p) => p.slug === "post")!.presentation;
|
|
169
|
+
const rest = corpus.filter((t) => t.meta.slug !== "social");
|
|
170
|
+
expect(presentationErrors(tpl("content-os"), [...rest, social])).toEqual(
|
|
171
|
+
[]
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The process contract a playbook declares is checked at authoring time
|
|
3
|
+
* (`checkProcessContract` in validate.ts), because every one of these mistakes
|
|
4
|
+
* is STORED by the pod and then silently never fires:
|
|
5
|
+
* - a `statusProperty` that is not a select of the subject kind → the stage's
|
|
6
|
+
* status write lands on a field nobody reads;
|
|
7
|
+
* - a `subjectStatus` / activator `status` / `humanOnlyStatuses` value that is
|
|
8
|
+
* not one of the enum values → a write the select refuses, or a rule on a
|
|
9
|
+
* value no entity ever enters;
|
|
10
|
+
* - an `onFail.toStage` naming no stage → a rejected gate with nowhere to go;
|
|
11
|
+
* - a `relationToSubject` naming no relation → an edge type nothing renders.
|
|
12
|
+
*
|
|
13
|
+
* Each rule has a failing fixture below, built by mutating ONE field of a clean
|
|
14
|
+
* fixture, and the clean fixture is asserted clean — so a fixture that fails for
|
|
15
|
+
* an unrelated reason cannot pass for the wrong one.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, expect, it } from "vitest";
|
|
19
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
20
|
+
import { dirname, resolve } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
import { MAX_SESSION_CRITERIA, validateTemplate } from "./validate.js";
|
|
23
|
+
import type { WorkspaceYaml } from "./types.js";
|
|
24
|
+
import { readYamlTemplates } from "./generate.mjs";
|
|
25
|
+
|
|
26
|
+
const PROCESS_RULES = new Set([
|
|
27
|
+
"playbook-status-property-no-subject",
|
|
28
|
+
"playbook-status-property-unknown",
|
|
29
|
+
"playbook-status-property-not-select",
|
|
30
|
+
"stage-subject-status-unknown",
|
|
31
|
+
"stage-subject-status-unknown-no-property",
|
|
32
|
+
"stage-onfail-unknown",
|
|
33
|
+
"activator-on-invalid",
|
|
34
|
+
"activator-mode-invalid",
|
|
35
|
+
"activator-no-subject",
|
|
36
|
+
"activator-status-required",
|
|
37
|
+
"activator-status-unknown",
|
|
38
|
+
"activator-status-unknown-no-property",
|
|
39
|
+
"activator-status-unexpected",
|
|
40
|
+
"human-only-status-unknown",
|
|
41
|
+
"human-only-status-unknown-no-property",
|
|
42
|
+
"output-relation-no-subject",
|
|
43
|
+
"output-relation-unknown",
|
|
44
|
+
"output-optional-not-boolean",
|
|
45
|
+
"playbook-criteria-over-session-cap",
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* `owner` OWNS the shared kind `widget` (with its lifecycle select). `proc`
|
|
50
|
+
* requires it and only re-declares `widget` with no properties — so resolving
|
|
51
|
+
* `widget-stage` proves the status property is found ACROSS the corpus.
|
|
52
|
+
*/
|
|
53
|
+
const owner = {
|
|
54
|
+
meta: { slug: "owner", name: "Owner", description: "d", icon: "x", color: "gray" },
|
|
55
|
+
workspace: { name: "Owner", description: "d" },
|
|
56
|
+
profiles: [
|
|
57
|
+
{
|
|
58
|
+
slug: "widget",
|
|
59
|
+
displayName: "Widget",
|
|
60
|
+
scope: "shared",
|
|
61
|
+
properties: [
|
|
62
|
+
{ slug: "widget-stage", label: "Stage", valueType: "string", inputType: "select", enumValues: ["New", "Built", "Shipped", "Archived"] },
|
|
63
|
+
{ slug: "widget-notes", label: "Notes", valueType: "string", inputType: "text" },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
entityLinks: [{ sourceProfileSlug: "widget", targetProfileSlug: "widget", type: "spawned_from", label: "Spawned from" }],
|
|
68
|
+
} as unknown as WorkspaceYaml;
|
|
69
|
+
|
|
70
|
+
function proc(mutate: (pb: Record<string, any>) => void = () => {}): WorkspaceYaml {
|
|
71
|
+
const pb: Record<string, any> = {
|
|
72
|
+
name: "Build Widget",
|
|
73
|
+
goalTemplate: "Build {widget}",
|
|
74
|
+
params: [{ name: "widget", type: "entity", required: true }],
|
|
75
|
+
subjectProfile: { profileSlug: "widget", statusProperty: "widget-stage" },
|
|
76
|
+
humanOnlyStatuses: ["New", "Archived"],
|
|
77
|
+
activators: [{ on: "enters_status", status: "Built", mode: "propose" }],
|
|
78
|
+
expectedOutputs: [{ kind: "widget", label: "Spin-off", relationToSubject: "spawned_from" }],
|
|
79
|
+
stages: [
|
|
80
|
+
{ key: "build", name: "Build", category: "started", subjectStatus: "Built" },
|
|
81
|
+
{
|
|
82
|
+
key: "ship",
|
|
83
|
+
name: "Ship",
|
|
84
|
+
category: "completed",
|
|
85
|
+
subjectStatus: "Shipped",
|
|
86
|
+
gate: { kind: "human" },
|
|
87
|
+
onFail: { toStage: "build" },
|
|
88
|
+
expectedOutputs: [{ kind: "widget", label: "Release", relationToSubject: "spawned_from" }],
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
mutate(pb);
|
|
93
|
+
return {
|
|
94
|
+
meta: { slug: "proc", name: "Proc", description: "d", icon: "x", color: "gray" },
|
|
95
|
+
workspace: { name: "Proc", description: "d" },
|
|
96
|
+
dependencies: [{ slug: "owner", kind: "workspace", relation: "require" }],
|
|
97
|
+
profiles: [{ slug: "widget", displayName: "Widget", scope: "shared", properties: [] }],
|
|
98
|
+
playbooks: [pb],
|
|
99
|
+
} as unknown as WorkspaceYaml;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function processErrors(tpl: WorkspaceYaml) {
|
|
103
|
+
const { errors } = validateTemplate(tpl, { corpus: [owner, tpl] });
|
|
104
|
+
return errors.filter((e) => PROCESS_RULES.has(e.rule));
|
|
105
|
+
}
|
|
106
|
+
const rulesOf = (tpl: WorkspaceYaml) => processErrors(tpl).map((e) => e.rule);
|
|
107
|
+
|
|
108
|
+
describe("process contract — the clean fixture is clean", () => {
|
|
109
|
+
it("a kind owned by a DEPENDENCY resolves its status property and relation", () => {
|
|
110
|
+
expect(processErrors(proc())).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("process contract — each rule fires on its own fixture", () => {
|
|
115
|
+
it("statusProperty that the kind does not carry", () => {
|
|
116
|
+
expect(rulesOf(proc((pb) => (pb.subjectProfile.statusProperty = "widget-phase")))).toEqual([
|
|
117
|
+
"playbook-status-property-unknown",
|
|
118
|
+
]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("statusProperty that is not a select", () => {
|
|
122
|
+
expect(
|
|
123
|
+
rulesOf(
|
|
124
|
+
proc((pb) => {
|
|
125
|
+
pb.subjectProfile.statusProperty = "widget-notes";
|
|
126
|
+
// Without a resolvable vocabulary the statuses are not re-reported.
|
|
127
|
+
})
|
|
128
|
+
)
|
|
129
|
+
).toEqual(["playbook-status-property-not-select"]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("statusProperty with no subject kind", () => {
|
|
133
|
+
const rules = rulesOf(proc((pb) => (pb.subjectProfile = { statusProperty: "widget-stage" })));
|
|
134
|
+
expect(rules).toContain("playbook-status-property-no-subject");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("a stage subjectStatus outside the enum", () => {
|
|
138
|
+
expect(rulesOf(proc((pb) => (pb.stages[0].subjectStatus = "Done")))).toEqual([
|
|
139
|
+
"stage-subject-status-unknown",
|
|
140
|
+
]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("a stage subjectStatus with no statusProperty to belong to", () => {
|
|
144
|
+
const rules = rulesOf(
|
|
145
|
+
proc((pb) => {
|
|
146
|
+
delete pb.subjectProfile.statusProperty;
|
|
147
|
+
delete pb.humanOnlyStatuses;
|
|
148
|
+
pb.activators = [{ on: "created", mode: "propose" }];
|
|
149
|
+
})
|
|
150
|
+
);
|
|
151
|
+
expect(rules).toEqual([
|
|
152
|
+
"stage-subject-status-unknown-no-property",
|
|
153
|
+
"stage-subject-status-unknown-no-property",
|
|
154
|
+
]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("onFail.toStage naming no stage of the playbook", () => {
|
|
158
|
+
expect(rulesOf(proc((pb) => (pb.stages[1].onFail = { toStage: "qa" })))).toEqual([
|
|
159
|
+
"stage-onfail-unknown",
|
|
160
|
+
]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("onFail.toStage may name a stage by its name", () => {
|
|
164
|
+
expect(rulesOf(proc((pb) => (pb.stages[1].onFail = { toStage: "Build" })))).toEqual([]);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("an activator status outside the enum", () => {
|
|
168
|
+
expect(
|
|
169
|
+
rulesOf(proc((pb) => (pb.activators = [{ on: "enters_status", status: "Done", mode: "run" }])))
|
|
170
|
+
).toEqual(["activator-status-unknown"]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("an enters_status activator with no status", () => {
|
|
174
|
+
expect(rulesOf(proc((pb) => (pb.activators = [{ on: "enters_status", mode: "propose" }])))).toEqual([
|
|
175
|
+
"activator-status-required",
|
|
176
|
+
]);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("an activator with an unknown trigger or mode", () => {
|
|
180
|
+
expect(rulesOf(proc((pb) => (pb.activators = [{ on: "updated", mode: "always" }])))).toEqual([
|
|
181
|
+
"activator-on-invalid",
|
|
182
|
+
"activator-mode-invalid",
|
|
183
|
+
]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* arch F8: the catalog speaks the pod's worker grammar. Rows rule out: a
|
|
188
|
+
* validator still limited to created/enters_status (count, schedule,
|
|
189
|
+
* manual refused); a count whose status is not checked against the enum; a
|
|
190
|
+
* manual or created trigger taking a status.
|
|
191
|
+
*/
|
|
192
|
+
it("count, schedule and manual workers validate, with their statuses checked", () => {
|
|
193
|
+
expect(
|
|
194
|
+
rulesOf(
|
|
195
|
+
proc(
|
|
196
|
+
(pb) =>
|
|
197
|
+
(pb.activators = [
|
|
198
|
+
{ on: "count", status: "Built", min: 5, size: "per_moment", mode: "propose", maxPendingApprovals: 10 },
|
|
199
|
+
{ on: "schedule", cron: "0 9 * * 1", status: "Built", mode: "run" },
|
|
200
|
+
{ on: "manual", mode: "propose" },
|
|
201
|
+
])
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
).toEqual([]);
|
|
205
|
+
expect(
|
|
206
|
+
rulesOf(proc((pb) => (pb.activators = [{ on: "count", status: "Done", min: 5, mode: "propose" }])))
|
|
207
|
+
).toEqual(["activator-status-unknown"]);
|
|
208
|
+
expect(rulesOf(proc((pb) => (pb.activators = [{ on: "count", min: 5, mode: "propose" }])))).toEqual([
|
|
209
|
+
"activator-status-required",
|
|
210
|
+
]);
|
|
211
|
+
expect(rulesOf(proc((pb) => (pb.activators = [{ on: "manual", status: "Built", mode: "propose" }])))).toEqual([
|
|
212
|
+
"activator-status-unexpected",
|
|
213
|
+
]);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("an activator on a playbook with no subject kind", () => {
|
|
217
|
+
const rules = rulesOf(
|
|
218
|
+
proc((pb) => {
|
|
219
|
+
delete pb.subjectProfile;
|
|
220
|
+
delete pb.stages;
|
|
221
|
+
delete pb.humanOnlyStatuses;
|
|
222
|
+
delete pb.expectedOutputs;
|
|
223
|
+
pb.activators = [{ on: "created", mode: "propose" }];
|
|
224
|
+
})
|
|
225
|
+
);
|
|
226
|
+
expect(rules).toEqual(["activator-no-subject"]);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("a humanOnlyStatuses value outside the enum", () => {
|
|
230
|
+
expect(rulesOf(proc((pb) => (pb.humanOnlyStatuses = ["New", "Retired"])))).toEqual([
|
|
231
|
+
"human-only-status-unknown",
|
|
232
|
+
]);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("a playbook-level relationToSubject the corpus does not define", () => {
|
|
236
|
+
expect(rulesOf(proc((pb) => (pb.expectedOutputs[0].relationToSubject = "spawned_by")))).toEqual([
|
|
237
|
+
"output-relation-unknown",
|
|
238
|
+
]);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("a stage-level relationToSubject the corpus does not define", () => {
|
|
242
|
+
expect(
|
|
243
|
+
rulesOf(proc((pb) => (pb.stages[1].expectedOutputs[0].relationToSubject = "spawned_by")))
|
|
244
|
+
).toEqual(["output-relation-unknown"]);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("an output `optional` that is not a boolean, at playbook and stage level", () => {
|
|
248
|
+
// The pod reads `optional` strictly (only a real `true`): a quoted value
|
|
249
|
+
// would leave the slot owed while the YAML reads as if it were not.
|
|
250
|
+
expect(rulesOf(proc((pb) => (pb.expectedOutputs[0].optional = "true")))).toEqual([
|
|
251
|
+
"output-optional-not-boolean",
|
|
252
|
+
]);
|
|
253
|
+
expect(rulesOf(proc((pb) => (pb.stages[1].expectedOutputs[0].optional = "false")))).toEqual([
|
|
254
|
+
"output-optional-not-boolean",
|
|
255
|
+
]);
|
|
256
|
+
// A real boolean is clean at both levels.
|
|
257
|
+
expect(
|
|
258
|
+
rulesOf(
|
|
259
|
+
proc((pb) => {
|
|
260
|
+
pb.expectedOutputs[0].optional = true;
|
|
261
|
+
pb.stages[1].expectedOutputs[0].optional = false;
|
|
262
|
+
})
|
|
263
|
+
)
|
|
264
|
+
).toEqual([]);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("a DEFAULT relation (base.yaml relationDefs) resolves through the corpus", () => {
|
|
268
|
+
const base = readYamlTemplates().find((t) => t.slug === "base")!.data as WorkspaceYaml;
|
|
269
|
+
const tpl = proc((pb) => (pb.expectedOutputs[0].relationToSubject = "relates_to"));
|
|
270
|
+
const { errors } = validateTemplate(tpl, { corpus: [owner, base, tpl] });
|
|
271
|
+
expect(errors.filter((e) => PROCESS_RULES.has(e.rule))).toEqual([]);
|
|
272
|
+
// …and the same value is refused when base is not in the corpus.
|
|
273
|
+
expect(rulesOf(tpl)).toEqual(["output-relation-unknown"]);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("relationToSubject on a subject-less playbook must be `none`", () => {
|
|
277
|
+
const strip = (pb: Record<string, any>) => {
|
|
278
|
+
delete pb.subjectProfile;
|
|
279
|
+
delete pb.stages;
|
|
280
|
+
delete pb.humanOnlyStatuses;
|
|
281
|
+
delete pb.activators;
|
|
282
|
+
};
|
|
283
|
+
expect(rulesOf(proc(strip))).toEqual(["output-relation-no-subject"]);
|
|
284
|
+
expect(
|
|
285
|
+
rulesOf(
|
|
286
|
+
proc((pb) => {
|
|
287
|
+
strip(pb);
|
|
288
|
+
pb.expectedOutputs[0].relationToSubject = "none";
|
|
289
|
+
})
|
|
290
|
+
)
|
|
291
|
+
).toEqual([]);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
describe("process contract — a session keeps at most MAX_SESSION_CRITERIA criteria", () => {
|
|
296
|
+
const crit = (key: string) => ({ key, statement: `${key} holds.`, check: { kind: "judge" } });
|
|
297
|
+
const withCriteria = (playbookLevel: number, perStage: number) =>
|
|
298
|
+
proc((pb) => {
|
|
299
|
+
pb.criteria = Array.from({ length: playbookLevel }, (_, i) => crit(`pb-${i}`));
|
|
300
|
+
pb.stages.forEach((s: Record<string, any>) => {
|
|
301
|
+
s.criteria = Array.from({ length: perStage }, (_, i) => crit(`${s.key}-${i}`));
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("playbook-level plus stage criteria at the cap pass", () => {
|
|
306
|
+
// 2 playbook-level + 2 stages × 5 = 12
|
|
307
|
+
expect(rulesOf(withCriteria(2, 5))).toEqual([]);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("one over the cap is refused (the pod drops the extra one silently)", () => {
|
|
311
|
+
// 3 + 2 × 5 = 13
|
|
312
|
+
expect(rulesOf(withCriteria(3, 5))).toEqual(["playbook-criteria-over-session-cap"]);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("a key repeated across stages counts once, as the pod keeps it once", () => {
|
|
316
|
+
const tpl = proc((pb) => {
|
|
317
|
+
pb.criteria = Array.from({ length: 12 }, (_, i) => crit(`k-${i}`));
|
|
318
|
+
pb.stages[0].criteria = [crit("k-0")];
|
|
319
|
+
});
|
|
320
|
+
expect(rulesOf(tpl)).toEqual([]);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const BACKEND_PLAYBOOKS = resolve(
|
|
325
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
326
|
+
"../../../../synap-backend/packages/playbooks/src/index.ts"
|
|
327
|
+
);
|
|
328
|
+
describe.skipIf(!existsSync(BACKEND_PLAYBOOKS))("MAX_SESSION_CRITERIA mirrors the pod's cap", () => {
|
|
329
|
+
if (!existsSync(BACKEND_PLAYBOOKS))
|
|
330
|
+
console.warn(`[process-contract] SKIP: no synap-backend checkout at ${BACKEND_PLAYBOOKS}`);
|
|
331
|
+
it("equals @synap/playbooks MAX_SESSION_CRITERIA", () => {
|
|
332
|
+
const m = readFileSync(BACKEND_PLAYBOOKS, "utf8").match(/export const MAX_SESSION_CRITERIA = (\d+);/);
|
|
333
|
+
expect(m, "the pod's MAX_SESSION_CRITERIA declaration was not found").not.toBeNull();
|
|
334
|
+
expect(MAX_SESSION_CRITERIA).toBe(Number(m![1]));
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe("process contract — the shipped corpus", () => {
|
|
339
|
+
it("non-vacuity: Content OS declares the contract, and the whole corpus passes it", () => {
|
|
340
|
+
const corpus = readYamlTemplates().map((t) => t.data as WorkspaceYaml);
|
|
341
|
+
const contentOs = corpus.find((t) => t.meta.slug === "content-os")!;
|
|
342
|
+
const produce = contentOs.playbooks!.find((p) => p.name === "Produce Content")!;
|
|
343
|
+
expect(produce.subjectProfile?.statusProperty).toBe("post-status");
|
|
344
|
+
expect(produce.stages!.filter((s) => s.subjectStatus).length).toBe(6);
|
|
345
|
+
const errs = corpus.flatMap((t) =>
|
|
346
|
+
validateTemplate(t, { corpus })
|
|
347
|
+
.errors.filter((e) => PROCESS_RULES.has(e.rule))
|
|
348
|
+
.map((e) => `${t.meta.slug}: ${e.message}`)
|
|
349
|
+
);
|
|
350
|
+
expect(errs).toEqual([]);
|
|
351
|
+
});
|
|
352
|
+
});
|