@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,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The founder's decisions of 2026-10-12 (pod file 00c1b2fe-ef98-41fa-86cf-801dfab12cd1, decisions wave),
|
|
3
|
+
* pinned against the REAL YAML corpus (`readYamlTemplates`, the source the
|
|
4
|
+
* build and the publish both read) and the real validator.
|
|
5
|
+
*
|
|
6
|
+
* 1. Business Developer folds into CRM: "Qualify a lead" runs on the `lead`
|
|
7
|
+
* ROLE's leadStage; "Advance a commercial proposal" is gone; the Synap-fee
|
|
8
|
+
* and commission model went to the private arch-sales, which was retired
|
|
9
|
+
* the same day with the-arch (The Arch is no longer in use);
|
|
10
|
+
* business-developer
|
|
11
|
+
* is a tombstone pointing at crm, and nothing installs it any more.
|
|
12
|
+
* 4. "Deliver a deliverable" starts when a deliverable ENTERS in-progress.
|
|
13
|
+
* 5. Billing: per-engagement milestone invoices, and a monthly recurring
|
|
14
|
+
* process on revenue-stream whose schedule the pod can actually create.
|
|
15
|
+
* 6. A new brand voice guide is a draft.
|
|
16
|
+
*
|
|
17
|
+
* Does NOT cover: runtime behaviour on a pod (a role activator firing on
|
|
18
|
+
* attach, the facet status write) — that is the backend lane's (R1).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, expect, it } from "vitest";
|
|
22
|
+
import { readYamlTemplates } from "./generate.mjs";
|
|
23
|
+
import type { WorkspaceYaml } from "./types.js";
|
|
24
|
+
import { validateTemplate } from "./validate.js";
|
|
25
|
+
import { loadRetired } from "../scripts/catalog-lib.mjs";
|
|
26
|
+
|
|
27
|
+
const corpus = readYamlTemplates().map((t) => t.data as WorkspaceYaml);
|
|
28
|
+
const tpl = (slug: string, c: WorkspaceYaml[] = corpus) => {
|
|
29
|
+
const t = c.find((x) => x.meta.slug === slug);
|
|
30
|
+
if (!t) throw new Error(`template ${slug} not in the corpus`);
|
|
31
|
+
return t;
|
|
32
|
+
};
|
|
33
|
+
const profileOf = (t: WorkspaceYaml, slug: string) => t.profiles.find((p) => p.slug === slug);
|
|
34
|
+
const propOf = (t: WorkspaceYaml, profile: string, prop: string) =>
|
|
35
|
+
profileOf(t, profile)?.properties.find((p) => p.slug === prop);
|
|
36
|
+
const playbookOf = (t: WorkspaceYaml, name: string) => t.playbooks?.find((p) => p.name === name);
|
|
37
|
+
const errorsOf = (t: WorkspaceYaml, c: WorkspaceYaml[] = corpus) =>
|
|
38
|
+
validateTemplate(t, { corpus: c }).errors.map((e) => `${e.rule} ${e.path}`);
|
|
39
|
+
|
|
40
|
+
describe("decision 1 — Business Developer folds into CRM", () => {
|
|
41
|
+
it("non-vacuity: the corpus is the real one", () => {
|
|
42
|
+
expect(corpus.length).toBeGreaterThanOrEqual(25);
|
|
43
|
+
expect(corpus.some((t) => t.meta.slug === "crm")).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("business-developer is a tombstone replaced by crm, and no live template depends on it", async () => {
|
|
47
|
+
expect(corpus.some((t) => t.meta.slug === "business-developer")).toBe(false);
|
|
48
|
+
const { retired } = await loadRetired();
|
|
49
|
+
expect(retired.find((r) => r.slug === "business-developer")).toMatchObject({ kind: "workspace", replacedBy: "crm" });
|
|
50
|
+
const dependents = corpus.filter((t) => (t.dependencies ?? []).some((d) => d.slug === "business-developer"));
|
|
51
|
+
expect(dependents.map((t) => t.meta.slug)).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("'Qualify a lead' is a process on the lead ROLE's leadStage, started by a person", () => {
|
|
55
|
+
const crm = tpl("crm");
|
|
56
|
+
const pb = playbookOf(crm, "Qualify a lead");
|
|
57
|
+
expect(pb?.subjectProfile).toMatchObject({ profileSlug: "lead", statusProperty: "leadStage" });
|
|
58
|
+
// Manual only since 2026-10-27 (founder): a `created` worker ran once per
|
|
59
|
+
// lead and burned the budget; "Score a list" scores a whole list in one run.
|
|
60
|
+
expect(pb?.activators).toEqual([{ on: "manual", mode: "propose" }]);
|
|
61
|
+
// The subject is a ROLE, declared pod-wide by foundation.
|
|
62
|
+
expect(profileOf(tpl("foundation"), "lead")).toMatchObject({ profileKind: "role", scope: "shared" });
|
|
63
|
+
// Every leadStage value is a stage's status or the person's.
|
|
64
|
+
const owned = new Set([...(pb?.stages ?? []).map((s) => s.subjectStatus), ...(pb?.humanOnlyStatuses ?? [])]);
|
|
65
|
+
expect([...owned].filter(Boolean).sort()).toEqual([...(propOf(tpl("foundation"), "lead", "leadStage")?.enumValues ?? [])].sort());
|
|
66
|
+
// Only the last stage, reached when the person chose pursue, moves the lead
|
|
67
|
+
// to qualified. It opens no deal (CRM build plan A4, 2026-10-22): a deal is
|
|
68
|
+
// opened when a person records a booked meeting, which converts the lead.
|
|
69
|
+
expect(pb?.stages?.at(-1)).toMatchObject({ key: "pursue", subjectStatus: "qualified" });
|
|
70
|
+
expect(pb?.stages?.flatMap((s) => s.expectedOutputs ?? [])).toEqual([]);
|
|
71
|
+
expect(pb?.expectedOutputs).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("the validator reads a role subject's statuses from the role (a wrong one is refused)", () => {
|
|
75
|
+
expect(errorsOf(tpl("crm"))).toEqual([]);
|
|
76
|
+
const c = structuredClone(corpus);
|
|
77
|
+
const pb = playbookOf(tpl("crm", c), "Qualify a lead")!;
|
|
78
|
+
pb.stages![0]!.subjectStatus = "won"; // a deal status, not a leadStage one
|
|
79
|
+
expect(errorsOf(tpl("crm", c), c)).toContain('stage-subject-status-unknown playbooks."Qualify a lead".stages.research.subjectStatus');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("'Advance a commercial proposal' is gone everywhere; CRM's 'Win a deal' remains", () => {
|
|
83
|
+
const names = corpus.flatMap((t) => (t.playbooks ?? []).map((p) => p.name));
|
|
84
|
+
expect(names).not.toContain("Advance a commercial proposal");
|
|
85
|
+
expect(names).not.toContain("Qualify a CRM lead");
|
|
86
|
+
expect(playbookOf(tpl("crm"), "Win a deal")).toBeDefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("CRM carries the moved lead/client fields, the campaign kind and member_of", () => {
|
|
90
|
+
const crm = tpl("crm");
|
|
91
|
+
for (const p of ["icpScore", "qualificationNotes", "nextAction", "outreachChannels", "convertedAt", "disqualificationReason"])
|
|
92
|
+
expect(propOf(crm, "lead", p), `lead.${p}`).toBeDefined();
|
|
93
|
+
for (const p of ["becameClientAt", "handoffStatus", "handoffDestination", "handoffNotes"])
|
|
94
|
+
expect(propOf(crm, "client", p), `client.${p}`).toBeDefined();
|
|
95
|
+
expect(profileOf(crm, "campaign")?.semanticSlug).toBe("marketing.campaign");
|
|
96
|
+
expect(crm.entityLinks?.filter((l) => l.type === "member_of").map((l) => l.sourceProfileSlug)).toEqual(["person", "company"]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("the Synap-fee and commission model left the catalog with arch-sales and the-arch, both tombstones", async () => {
|
|
100
|
+
const FEE = ["synapFeeModel", "synapFixedFee", "synapSuccessFeeRate", "synapFeeAmount", "sourcedByUserId", "commissionEntitlementId", "commissionStatus"];
|
|
101
|
+
const { retired } = await loadRetired();
|
|
102
|
+
for (const slug of ["arch-sales", "the-arch"]) {
|
|
103
|
+
expect(corpus.some((t) => t.meta.slug === slug), `${slug} is live`).toBe(false);
|
|
104
|
+
const stone = retired.find((r) => r.slug === slug);
|
|
105
|
+
expect(stone, `${slug} tombstone`).toMatchObject({ kind: "workspace", since: "2026-10-12" });
|
|
106
|
+
expect(stone?.replacedBy, `${slug} has no replacement`).toBeUndefined();
|
|
107
|
+
const dependents = corpus.filter((t) => (t.dependencies ?? []).some((d) => d.slug === slug));
|
|
108
|
+
expect(dependents.map((t) => t.meta.slug), `dependents of ${slug}`).toEqual([]);
|
|
109
|
+
}
|
|
110
|
+
// No live template's deal carries the fee model any more (networking's
|
|
111
|
+
// `intro` keeps its own copy of the fee vocabulary: a separate decision).
|
|
112
|
+
for (const p of FEE) {
|
|
113
|
+
const holders = corpus.filter((t) => !!propOf(t, "deal", p)).map((t) => t.meta.slug);
|
|
114
|
+
expect(holders, `deal.${p}`).toEqual([]);
|
|
115
|
+
}
|
|
116
|
+
const holders = (pred: (t: WorkspaceYaml) => boolean) => corpus.filter(pred).map((t) => t.meta.slug);
|
|
117
|
+
expect(holders((t) => !!profileOf(t, "crm-commission-entitlement"))).toEqual([]);
|
|
118
|
+
expect(holders((t) => (t.automations ?? []).some((a) => a.name === "Win and hand off"))).toEqual([]);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("decision 4 — 'Deliver a deliverable' starts when the deliverable enters in-progress", () => {
|
|
123
|
+
const pb = playbookOf(tpl("operations"), "Deliver a deliverable")!;
|
|
124
|
+
|
|
125
|
+
it("its one activator is entering in-progress", () => {
|
|
126
|
+
expect(pb.activators).toEqual([{ on: "enters_status", status: "in-progress", mode: "propose" }]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("no stage writes not-started (a run started at in-progress never moves the deliverable back)", () => {
|
|
130
|
+
expect(pb.stages?.[0]?.subjectStatus).toBeUndefined();
|
|
131
|
+
expect(pb.stages?.map((s) => s.subjectStatus)).not.toContain("not-started");
|
|
132
|
+
// `cancelled` (0.20.0, plan aac47c4a W5) is a person's call too: no stage writes it.
|
|
133
|
+
expect(pb.humanOnlyStatuses).toEqual(["not-started", "cancelled"]);
|
|
134
|
+
expect(pb.stages?.map((s) => s.subjectStatus)).not.toContain("cancelled");
|
|
135
|
+
expect(errorsOf(tpl("operations"))).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("decision 5 — billing", () => {
|
|
140
|
+
it("an engagement chooses at-close (default) or per-deliverable billing", () => {
|
|
141
|
+
expect(propOf(tpl("operations"), "engagement", "engagement-billing-mode")).toMatchObject({
|
|
142
|
+
inputType: "select",
|
|
143
|
+
enumValues: ["at-close", "per-deliverable"],
|
|
144
|
+
constraints: { defaultValue: "at-close" },
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("an accepted deliverable may produce its milestone invoice (an optional slot)", () => {
|
|
149
|
+
const accepted = playbookOf(tpl("operations"), "Deliver a deliverable")!.stages!.find((s) => s.key === "accepted");
|
|
150
|
+
expect(accepted?.expectedOutputs?.map((o) => o.kind)).toEqual(["invoice"]);
|
|
151
|
+
// Owed by nobody (synap-backend 30a9ab85): an at-close run never shows it pending.
|
|
152
|
+
expect(accepted?.expectedOutputs?.[0]?.optional).toBe(true);
|
|
153
|
+
expect(accepted?.goal).toMatch(/per deliverable/);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("'Bill a recurring fee' runs monthly over active monthly revenue streams, with a filter the pod keeps", () => {
|
|
157
|
+
const finance = tpl("finance");
|
|
158
|
+
const pb = playbookOf(finance, "Bill a recurring fee")!;
|
|
159
|
+
expect(pb.subjectProfile?.profileSlug).toBe("revenue-stream");
|
|
160
|
+
expect(pb.schedule).toMatchObject({ cron: "0 8 1 * *", enabled: true });
|
|
161
|
+
expect(pb.metadata).toMatchObject({ governance: { forceProposeWrites: true } });
|
|
162
|
+
// The pod's cron door keeps ONLY a string filter (readSubjectProfile), and
|
|
163
|
+
// refuses an unfiltered AI fan-out, so an object here = no schedule at all.
|
|
164
|
+
const filter = pb.subjectProfile?.filter;
|
|
165
|
+
expect(typeof filter).toBe("string");
|
|
166
|
+
const parsed = JSON.parse(filter as string) as Record<string, string>;
|
|
167
|
+
expect(Object.keys(parsed).length).toBeGreaterThan(0);
|
|
168
|
+
// Reachability: every filtered key is a property of revenue-stream and every value one of its options.
|
|
169
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
170
|
+
const prop = propOf(finance, "revenue-stream", key.replace(/^properties\./, ""));
|
|
171
|
+
expect(prop?.enumValues, key).toContain(value);
|
|
172
|
+
}
|
|
173
|
+
expect(pb.expectedOutputs?.map((o) => o.kind)).toEqual(["invoice"]);
|
|
174
|
+
expect(errorsOf(finance)).toEqual([]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("the drafted invoice is taken over by 'Bill a client' (its created activator)", () => {
|
|
178
|
+
expect(playbookOf(tpl("finance"), "Bill a client")?.activators).toContainEqual({ on: "created", mode: "propose" });
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("settled item — a new brand voice guide is a draft", () => {
|
|
183
|
+
it("voice-status defaults to draft", () => {
|
|
184
|
+
expect(propOf(tpl("brand-library"), "brand-voice-guide", "voice-status")?.constraints).toEqual({ defaultValue: "draft" });
|
|
185
|
+
});
|
|
186
|
+
});
|
package/src/define.ts
CHANGED
|
@@ -16,6 +16,8 @@ import type {
|
|
|
16
16
|
WorkspacePrimarySurface,
|
|
17
17
|
WorkspaceYaml, } from './types.js';
|
|
18
18
|
import { getWorkspaceTemplate } from './templates.js';
|
|
19
|
+
import { resolveLifecyclePresets } from './lifecycle-presets.js';
|
|
20
|
+
import { spacePackageCells, type SpacePackageCellWire } from './space-packages.js';
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* A `TemplateAutomation` as the apply door expects it: `flowDefinition`, not
|
|
@@ -33,7 +35,8 @@ export interface PackageAutomationWire {
|
|
|
33
35
|
status?: TemplateAutomation['status'];
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
/** The ONE automation projection (authored `flow` → wire `flowDefinition`), shared by workspaces and standalone automation packages. */
|
|
39
|
+
export function toAutomationWire(
|
|
37
40
|
automations: TemplateAutomation[] | undefined
|
|
38
41
|
): PackageAutomationWire[] | undefined {
|
|
39
42
|
return automations?.map((a) => ({
|
|
@@ -211,8 +214,9 @@ export function normalizeTemplateLayout(
|
|
|
211
214
|
* `kind` is INFERRED from field presence when absent. `TemplateBentoBlock.kind`
|
|
212
215
|
* is a required field in TypeScript, but the generated `templates.ts` casts the
|
|
213
216
|
* parsed YAML `as unknown as Record<string, WorkspaceYaml>` — so the compiler
|
|
214
|
-
* never checks the authored data against the type, and
|
|
215
|
-
* authored 8 widget blocks with no `kind:`
|
|
217
|
+
* never checks the authored data against the type, and the (since retired,
|
|
218
|
+
* R7 2026-09-27) `agent-fleet.yaml` authored 8 widget blocks with no `kind:`
|
|
219
|
+
* at all (bare `widgetType`). Routing
|
|
216
220
|
* strictly on `kind` would silently DROP those — the exact failure mode this
|
|
217
221
|
* function exists to end. Inferring by field mirrors what the pod itself does
|
|
218
222
|
* (it splits by field and never reads `kind`), so the seam is total: a block
|
|
@@ -330,6 +334,20 @@ export interface WorkspaceDefinitionInput {
|
|
|
330
334
|
workspaceType?: NonNullable<WorkspaceYaml['workspace']>['workspaceType'];
|
|
331
335
|
workspaceVisibility?: string;
|
|
332
336
|
workspaceCapabilities?: string[];
|
|
337
|
+
/**
|
|
338
|
+
* THE DECLARED INTENT SINK (Phase 3). `taskIntents` is authored at the TOP
|
|
339
|
+
* level of the template, not inside `workspace`, and is forwarded verbatim to
|
|
340
|
+
* `workspaces.createFromDefinition`, which persists it to
|
|
341
|
+
* `settings.taskIntents` — where the intent resolver joins it against what is
|
|
342
|
+
* actually installed. No shape transform: the authored form IS the wire form.
|
|
343
|
+
*/
|
|
344
|
+
taskIntents?: WorkspaceYaml['taskIntents'];
|
|
345
|
+
/**
|
|
346
|
+
* What the space PRODUCES (`meta.intent`), forwarded verbatim; the pod
|
|
347
|
+
* projects it into `settings.intent`. Same top-level field on the package
|
|
348
|
+
* door.
|
|
349
|
+
*/
|
|
350
|
+
intent?: WorkspaceYaml['meta']['intent'];
|
|
333
351
|
sourceRoles?: NonNullable<WorkspaceYaml['workspace']>['sourceRoles'];
|
|
334
352
|
defaultSources?: NonNullable<WorkspaceYaml['workspace']>['defaultSources'];
|
|
335
353
|
/** Per-workspace onboarding context — written to settings.onboarding. */
|
|
@@ -378,6 +396,30 @@ function toWorkspacePlaybooks(
|
|
|
378
396
|
}));
|
|
379
397
|
}
|
|
380
398
|
|
|
399
|
+
/**
|
|
400
|
+
* A template by slug (the bundled corpus, already compiled) or as authored
|
|
401
|
+
* YAML, with its lifecycle presets compiled (`adopts:` → flat options,
|
|
402
|
+
* generated `enumValues`; lifecycle-presets.ts), so both install doors carry
|
|
403
|
+
* the flat declaration the pod stores. An overlay that does not resolve throws,
|
|
404
|
+
* like an unknown slug: `validateTemplate` names it first on every catalog path.
|
|
405
|
+
*/
|
|
406
|
+
function compiledTemplate(slugOrYaml: string | WorkspaceYaml): WorkspaceYaml {
|
|
407
|
+
const tpl: WorkspaceYaml | undefined =
|
|
408
|
+
typeof slugOrYaml === 'string' ? getWorkspaceTemplate(slugOrYaml) : slugOrYaml;
|
|
409
|
+
if (!tpl) {
|
|
410
|
+
throw new Error(`Workspace template not found: ${String(slugOrYaml)}`);
|
|
411
|
+
}
|
|
412
|
+
const { template, problems } = resolveLifecyclePresets(tpl);
|
|
413
|
+
if (problems.length > 0) {
|
|
414
|
+
throw new Error(
|
|
415
|
+
`Template "${String(tpl.meta?.slug)}": lifecycle presets do not resolve — ${problems
|
|
416
|
+
.map((p) => `${p.path}: ${p.message}`)
|
|
417
|
+
.join('; ')}`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
return template;
|
|
421
|
+
}
|
|
422
|
+
|
|
381
423
|
/**
|
|
382
424
|
* Resolve a template (by slug or YAML object) into the createFromDefinition
|
|
383
425
|
* payload. Pass `nameOverride` to rename the workspace at creation time.
|
|
@@ -386,12 +428,7 @@ export function toWorkspaceDefinition(
|
|
|
386
428
|
slugOrYaml: string | WorkspaceYaml,
|
|
387
429
|
nameOverride?: string
|
|
388
430
|
): ToDefinitionResult {
|
|
389
|
-
const tpl
|
|
390
|
-
typeof slugOrYaml === 'string' ? getWorkspaceTemplate(slugOrYaml) : slugOrYaml;
|
|
391
|
-
|
|
392
|
-
if (!tpl) {
|
|
393
|
-
throw new Error(`Workspace template not found: ${String(slugOrYaml)}`);
|
|
394
|
-
}
|
|
431
|
+
const tpl = compiledTemplate(slugOrYaml);
|
|
395
432
|
|
|
396
433
|
const ws = tpl.workspace;
|
|
397
434
|
const name = nameOverride ?? ws.name;
|
|
@@ -432,6 +469,11 @@ export function toWorkspaceDefinition(
|
|
|
432
469
|
workspaceType: ws.workspaceType,
|
|
433
470
|
workspaceVisibility: ws.visibility,
|
|
434
471
|
workspaceCapabilities: ws.capabilities,
|
|
472
|
+
// Forwarded VERBATIM from the template's TOP level (not `workspace`) — the
|
|
473
|
+
// pod persists it to `settings.taskIntents` and reports any gap against what
|
|
474
|
+
// the installed capabilities actually provide. Authored form IS wire form.
|
|
475
|
+
taskIntents: tpl.taskIntents,
|
|
476
|
+
intent: tpl.meta.intent,
|
|
435
477
|
sourceRoles: ws.sourceRoles,
|
|
436
478
|
defaultSources: ws.defaultSources,
|
|
437
479
|
// Workspace creation persists its own compact playbook parameter shape.
|
|
@@ -500,7 +542,11 @@ type WorkspaceDefinitionForwarded =
|
|
|
500
542
|
| 'actionPlacements' // → settings.actionPlacements via applyPackagePostWorkspace
|
|
501
543
|
| 'automations' // → flowAutomations → shared applyPackagePostWorkspace door
|
|
502
544
|
| 'onboarding'
|
|
503
|
-
| 'dependencies'
|
|
545
|
+
| 'dependencies'
|
|
546
|
+
// Phase 3 gave the declaration a sink: `createWorkspaceFromDefinition`
|
|
547
|
+
// accepts `taskIntents` and persists it verbatim to `settings.taskIntents`,
|
|
548
|
+
// where the intent resolver joins it against what is actually installed.
|
|
549
|
+
| 'taskIntents';
|
|
504
550
|
|
|
505
551
|
/**
|
|
506
552
|
* Keys deliberately NOT forwarded by `toWorkspaceDefinition`. Each needs a
|
|
@@ -508,8 +554,21 @@ type WorkspaceDefinitionForwarded =
|
|
|
508
554
|
* else is a real gap and should say so.
|
|
509
555
|
*/
|
|
510
556
|
type WorkspaceDefinitionDropped =
|
|
557
|
+
// A POST-workspace layer (rule rows via the pod's rule door), carried on the
|
|
558
|
+
// PACKAGE definition (`toPackageDefinition`), not the workspace door's input.
|
|
559
|
+
// The workspace door has no rules field; the rule reconcile converges them.
|
|
560
|
+
| 'rules'
|
|
561
|
+
// Same layer as `rules`: the skill is LINKED to the space by
|
|
562
|
+
// `applyPackagePostWorkspace` (`skills.workspaceId` + `scope:"workspace"`),
|
|
563
|
+
// which owns the row. The workspace door has no skills field, so forwarding
|
|
564
|
+
// it here would be a no-op — it is carried on the PACKAGE definition instead.
|
|
565
|
+
| 'skills'
|
|
511
566
|
// Normalized into `layoutConfig.primarySurface`.
|
|
512
567
|
| 'app'
|
|
568
|
+
// Same layer as `skills`: built into frame cells that `applyPackagePostWorkspace`
|
|
569
|
+
// installs in the space. The workspace door has no cells field; carried on
|
|
570
|
+
// the PACKAGE definition as `cells[]` instead.
|
|
571
|
+
| 'spacePackage'
|
|
513
572
|
// The door DOES accept `extends`, but no template declares it today (0/21).
|
|
514
573
|
// Forwarding is a no-op until one does — wire it then.
|
|
515
574
|
| 'extends';
|
|
@@ -543,6 +602,12 @@ void _workspaceDefinitionLedger;
|
|
|
543
602
|
export interface PackageDefinitionOutput {
|
|
544
603
|
_meta: {
|
|
545
604
|
slug: string;
|
|
605
|
+
/** The PACKAGE's own name (meta.name). For an overlay this differs from
|
|
606
|
+
* `workspaceName`, which names the workspace it composes onto. */
|
|
607
|
+
name?: string;
|
|
608
|
+
/** The PACKAGE's own description (meta.description) — what the catalog
|
|
609
|
+
* lists. Top-level `description` stays the workspace's (install copy). */
|
|
610
|
+
description?: string;
|
|
546
611
|
icon?: string;
|
|
547
612
|
color?: string;
|
|
548
613
|
domain?: string;
|
|
@@ -556,6 +621,25 @@ export interface PackageDefinitionOutput {
|
|
|
556
621
|
workspaceSubtype?: string;
|
|
557
622
|
workspaceVisibility?: string;
|
|
558
623
|
workspaceCapabilities?: string[];
|
|
624
|
+
/**
|
|
625
|
+
* The DECLARED intents, carried to the marketplace/CLI install path too.
|
|
626
|
+
* Without it here, a workspace installed from the marketplace would LOSE its
|
|
627
|
+
* declaration while a bundled one kept it — the same field behaving
|
|
628
|
+
* differently per install route, which is precisely the fork this package
|
|
629
|
+
* exists to prevent.
|
|
630
|
+
*/
|
|
631
|
+
taskIntents?: WorkspaceYaml['taskIntents'];
|
|
632
|
+
/**
|
|
633
|
+
* What the space PRODUCES (`meta.intent`), TOP-LEVEL like `taskIntents` —
|
|
634
|
+
* never in `_meta`, which the CP rebuilds from its row columns on serve
|
|
635
|
+
* (`normalizePublishedDefinition`), so a key there never reaches a pod.
|
|
636
|
+
*/
|
|
637
|
+
intent?: WorkspaceYaml['meta']['intent'];
|
|
638
|
+
/**
|
|
639
|
+
* The onboarding start (`meta.start`), TOP-LEVEL for the same reason as
|
|
640
|
+
* `intent`. Package door only: the pod and the CP read starts off the package.
|
|
641
|
+
*/
|
|
642
|
+
start?: WorkspaceYaml['meta']['start'];
|
|
559
643
|
sourceRoles?: WorkspaceYaml['workspace']['sourceRoles'];
|
|
560
644
|
defaultSources?: WorkspaceYaml['workspace']['defaultSources'];
|
|
561
645
|
profiles: WorkspaceYaml['profiles'];
|
|
@@ -596,6 +680,24 @@ export interface PackageDefinitionOutput {
|
|
|
596
680
|
*/
|
|
597
681
|
actionPlacements?: WorkspaceYaml['actionPlacements'];
|
|
598
682
|
onboarding?: WorkspaceYaml['onboarding'];
|
|
683
|
+
/**
|
|
684
|
+
* Rules the template installs in its space — applied by the pod's
|
|
685
|
+
* `applyPackagePostWorkspace` through the ONE rule door and converged by the
|
|
686
|
+
* template-rule reconcile (three-way stamp on `metadata.rule.seed`).
|
|
687
|
+
*/
|
|
688
|
+
rules?: WorkspaceYaml['rules'];
|
|
689
|
+
/**
|
|
690
|
+
* Skills the template declares for its space — linked to the new space by the
|
|
691
|
+
* pod's `applyPackagePostWorkspace` (approved skills only), which is why they
|
|
692
|
+
* travel on the PACKAGE definition and not the workspace door's input.
|
|
693
|
+
*/
|
|
694
|
+
skills?: WorkspaceYaml['skills'];
|
|
695
|
+
/**
|
|
696
|
+
* The space package's built modules (`spacePackage` → `space-packages.ts`),
|
|
697
|
+
* installed by the pod's `applyPackagePostWorkspace` as sandboxed frame cells
|
|
698
|
+
* `cell:<_meta.slug>:<key>` — what the template's `app: {kind: stored}` opens.
|
|
699
|
+
*/
|
|
700
|
+
cells?: SpacePackageCellWire[];
|
|
599
701
|
/** Template-composition dependencies — resolved by the apply endpoint. */
|
|
600
702
|
dependencies?: WorkspaceYaml['dependencies'];
|
|
601
703
|
}
|
|
@@ -609,20 +711,18 @@ export interface PackageDefinitionOutput {
|
|
|
609
711
|
export function toPackageDefinition(
|
|
610
712
|
slugOrYaml: string | WorkspaceYaml
|
|
611
713
|
): PackageDefinitionOutput {
|
|
612
|
-
const tpl
|
|
613
|
-
typeof slugOrYaml === 'string' ? getWorkspaceTemplate(slugOrYaml) : slugOrYaml;
|
|
614
|
-
|
|
615
|
-
if (!tpl) {
|
|
616
|
-
throw new Error(`Workspace template not found: ${String(slugOrYaml)}`);
|
|
617
|
-
}
|
|
714
|
+
const tpl = compiledTemplate(slugOrYaml);
|
|
618
715
|
|
|
619
716
|
const ws = tpl.workspace;
|
|
620
717
|
// Same routing as the tRPC door — the union is split by `kind`, never dumped
|
|
621
718
|
// whole into one field. See `splitBentoBlocks`.
|
|
622
719
|
const bento = splitBentoBlocks(tpl.bento?.blocks);
|
|
720
|
+
const cells = spacePackageCells(tpl);
|
|
623
721
|
return {
|
|
624
722
|
_meta: {
|
|
625
723
|
slug: tpl.meta.slug,
|
|
724
|
+
name: tpl.meta.name,
|
|
725
|
+
description: tpl.meta.description,
|
|
626
726
|
icon: tpl.meta.icon,
|
|
627
727
|
color: tpl.meta.color,
|
|
628
728
|
domain: tpl.meta.domain,
|
|
@@ -635,6 +735,11 @@ export function toPackageDefinition(
|
|
|
635
735
|
workspaceSubtype: ws.subtype,
|
|
636
736
|
workspaceVisibility: ws.visibility,
|
|
637
737
|
workspaceCapabilities: ws.capabilities,
|
|
738
|
+
// Same declaration, same wire field, on the marketplace path — see the
|
|
739
|
+
// `taskIntents` note on the PackageDefinition type above.
|
|
740
|
+
taskIntents: tpl.taskIntents,
|
|
741
|
+
...(tpl.meta.intent ? { intent: tpl.meta.intent } : {}),
|
|
742
|
+
...(tpl.meta.start ? { start: tpl.meta.start } : {}),
|
|
638
743
|
sourceRoles: ws.sourceRoles,
|
|
639
744
|
defaultSources: ws.defaultSources,
|
|
640
745
|
profiles: tpl.profiles,
|
|
@@ -655,6 +760,9 @@ export function toPackageDefinition(
|
|
|
655
760
|
automations: toAutomationWire(tpl.automations),
|
|
656
761
|
actionPlacements: tpl.actionPlacements,
|
|
657
762
|
onboarding: tpl.onboarding,
|
|
763
|
+
...(tpl.rules?.length ? { rules: tpl.rules } : {}),
|
|
764
|
+
...(tpl.skills?.length ? { skills: tpl.skills } : {}),
|
|
765
|
+
...(cells ? { cells } : {}),
|
|
658
766
|
dependencies: tpl.dependencies,
|
|
659
767
|
};
|
|
660
768
|
}
|
|
@@ -686,6 +794,9 @@ type PackageDefinitionForwarded =
|
|
|
686
794
|
| 'automations' // → mapped to the apply door's flowDefinition wire shape
|
|
687
795
|
| 'actionPlacements' // → carried verbatim (Hub inbound schema declares it — parity closed)
|
|
688
796
|
| 'onboarding'
|
|
797
|
+
| 'rules' // → carried verbatim (applied post-workspace via the rule door)
|
|
798
|
+
| 'skills' // → carried verbatim (linked to the space post-workspace)
|
|
799
|
+
| 'spacePackage' // → cells (the built frame modules, installed post-workspace)
|
|
689
800
|
| 'dependencies';
|
|
690
801
|
|
|
691
802
|
/** Keys deliberately NOT forwarded by `toPackageDefinition`. */
|
|
@@ -696,7 +807,9 @@ type PackageDefinitionDropped =
|
|
|
696
807
|
// (package-definition.ts:262), but `PackageDefinitionOutput` omits it. No
|
|
697
808
|
// template declares it today (0/21), so this is latent, not live — wire it
|
|
698
809
|
// when a template first uses one.
|
|
699
|
-
| 'extends'
|
|
810
|
+
| 'extends'
|
|
811
|
+
// Phase 1 declaration with no sink yet — see `WorkspaceDefinitionDropped`.
|
|
812
|
+
| 'taskIntents';
|
|
700
813
|
|
|
701
814
|
type UnclassifiedPackageDefinitionKey = Exclude<
|
|
702
815
|
keyof WorkspaceYaml,
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The design loop on `ui-design` (Builder, "Design to implementation"):
|
|
3
|
+
* Generate writes 3 sibling variants, Critique judges them with the
|
|
4
|
+
* design-critique rubric before a person reviews.
|
|
5
|
+
*
|
|
6
|
+
* What this guards:
|
|
7
|
+
* 1. REACHABILITY: every property an `evidence` criterion of the process reads
|
|
8
|
+
* is a property `ui-design` declares (derived from the stages, not listed)
|
|
9
|
+
* — an evidence key with no property is a criterion that can never pass;
|
|
10
|
+
* 2. Critique sits between Generate and Review, writes no status (like
|
|
11
|
+
* Verify), is a check gate and loops to Generate;
|
|
12
|
+
* 3. CORPUS-WIDE: no process that PRODUCES things of its own subject kind
|
|
13
|
+
* starts `on: created` — each produced thing would propose another run of
|
|
14
|
+
* the same process (the variants of a design are ui-designs). The
|
|
15
|
+
* activator grammar has no filter to tell a request from its product.
|
|
16
|
+
* 4. the skills the goal tells the agent to load exist in the catalog.
|
|
17
|
+
*
|
|
18
|
+
* Not covered: the wording of the rubric, and whether the pod honours a
|
|
19
|
+
* `manual` worker — only the catalog side.
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { describe, expect, it } from "vitest";
|
|
24
|
+
import { WORKSPACE_TEMPLATES, getWorkspaceTemplate } from "./templates.js";
|
|
25
|
+
|
|
26
|
+
const builder = getWorkspaceTemplate("builder-workspace")!;
|
|
27
|
+
const pb = builder.playbooks!.find(
|
|
28
|
+
(p) => p.name === "Design to implementation"
|
|
29
|
+
)!;
|
|
30
|
+
const kind = builder.profiles.find((p) => p.slug === "ui-design")!;
|
|
31
|
+
type Stage = {
|
|
32
|
+
key: string;
|
|
33
|
+
subjectStatus?: string;
|
|
34
|
+
gate?: { kind: string };
|
|
35
|
+
onFail?: { toStage: string };
|
|
36
|
+
criteria?: { check?: { kind: string; evidenceKey?: string } }[];
|
|
37
|
+
expectedOutputs?: { kind: string }[];
|
|
38
|
+
};
|
|
39
|
+
const stages = (pb as unknown as { stages: Stage[] }).stages;
|
|
40
|
+
|
|
41
|
+
describe("Design to implementation — the design loop", () => {
|
|
42
|
+
it("every evidence key the process reads is a property ui-design declares", () => {
|
|
43
|
+
const keys = stages.flatMap((s) =>
|
|
44
|
+
(s.criteria ?? []).map((c) => c.check?.evidenceKey).filter(Boolean)
|
|
45
|
+
);
|
|
46
|
+
expect(keys.length).toBeGreaterThanOrEqual(4); // non-vacuity: design-json, critique, implementation-pr, implemented-story-id
|
|
47
|
+
const declared = new Set(kind.properties.map((p) => p.slug));
|
|
48
|
+
expect(keys.filter((k) => !declared.has(k!))).toEqual([]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("Critique sits between Generate and Review: no status, a check gate, rework to Generate", () => {
|
|
52
|
+
expect(stages.map((s) => s.key).slice(0, 3)).toEqual([
|
|
53
|
+
"generate",
|
|
54
|
+
"critique",
|
|
55
|
+
"review",
|
|
56
|
+
]);
|
|
57
|
+
const critique = stages.find((s) => s.key === "critique")!;
|
|
58
|
+
expect(critique.subjectStatus).toBeUndefined();
|
|
59
|
+
expect(critique.gate).toEqual({ kind: "check" });
|
|
60
|
+
expect(critique.onFail).toEqual({ toStage: "generate" });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("no process that produces its own subject kind starts on `created` (corpus-wide)", () => {
|
|
64
|
+
const selfProducing: string[] = [];
|
|
65
|
+
const offenders: string[] = [];
|
|
66
|
+
for (const t of Object.values(WORKSPACE_TEMPLATES))
|
|
67
|
+
for (const p of t.playbooks ?? []) {
|
|
68
|
+
const subject = p.subjectProfile?.profileSlug;
|
|
69
|
+
const ss = (p as unknown as { stages?: Stage[] }).stages ?? [];
|
|
70
|
+
const outs = [
|
|
71
|
+
...ss.flatMap((s) => s.expectedOutputs ?? []),
|
|
72
|
+
...((p as unknown as { expectedOutputs?: { kind: string }[] })
|
|
73
|
+
.expectedOutputs ?? []),
|
|
74
|
+
];
|
|
75
|
+
if (!subject || !outs.some((o) => o.kind === subject)) continue;
|
|
76
|
+
selfProducing.push(`${t.meta.slug}:${p.name}`);
|
|
77
|
+
if ((p.activators ?? []).some((a) => a.on === "created"))
|
|
78
|
+
offenders.push(`${t.meta.slug}:${p.name}`);
|
|
79
|
+
}
|
|
80
|
+
expect(selfProducing).toContain(
|
|
81
|
+
"builder-workspace:Design to implementation"
|
|
82
|
+
); // non-vacuity
|
|
83
|
+
expect(offenders).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("the skills the goal loads exist in the catalog", () => {
|
|
87
|
+
for (const slug of ["designing-with-real-components", "design-critique"]) {
|
|
88
|
+
expect(pb.goalTemplate).toContain(slug);
|
|
89
|
+
expect(
|
|
90
|
+
existsSync(join(__dirname, "..", "skills", slug, "SKILL.md"))
|
|
91
|
+
).toBe(true);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A template that DECLARES entity kinds must be installable as a workspace
|
|
3
|
+
* that can HOLD them.
|
|
4
|
+
*
|
|
5
|
+
* The pod refuses to file entities into a workspace whose type is
|
|
6
|
+
* `operational` or `agent` (`isDomainHomeWorkspace`, synap-backend
|
|
7
|
+
* `packages/database/src/services/workspace-resolution-service.ts`; enforced at
|
|
8
|
+
* `routers/entities/create.ts` with DOMAIN_INTO_NON_DOMAIN_HOME_MESSAGE) and
|
|
9
|
+
* leaves it out of auto-placement and of the orient domain map. That rule is
|
|
10
|
+
* deliberate — for real admin/system surfaces. Finance, People (hr), Legal and
|
|
11
|
+
* Internal Runbook are DOMAINS that ship profiles (account, invoice, contract,
|
|
12
|
+
* …) yet were typed `operational`, so live 2026-09-25 no entity could be filed
|
|
13
|
+
* into Finance at all.
|
|
14
|
+
*
|
|
15
|
+
* Reads the YAML SOURCES (not the generated `templates.ts`), and derives the
|
|
16
|
+
* set by globbing every `src/*.yaml` — a new template joins by existing.
|
|
17
|
+
*/
|
|
18
|
+
import { readdirSync, readFileSync } from "fs";
|
|
19
|
+
import { dirname, join } from "path";
|
|
20
|
+
import { fileURLToPath } from "url";
|
|
21
|
+
import { load } from "js-yaml";
|
|
22
|
+
import { describe, expect, it } from "vitest";
|
|
23
|
+
|
|
24
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const NON_DOMAIN_TYPES = new Set(["operational", "agent"]);
|
|
26
|
+
|
|
27
|
+
// Declares kinds AND is a non-domain type — an OPEN founder decision, not a
|
|
28
|
+
// silent pass. A ratchet: delete a line once the template is retyped (or its
|
|
29
|
+
// profiles move out); the test fails on a stale entry.
|
|
30
|
+
// Empty since R7 (2026-09-27): both former entries — dev-dashboard and
|
|
31
|
+
// agent-fleet — were retired as templates (option A); their useful views were
|
|
32
|
+
// folded into builder-workspace.
|
|
33
|
+
const KNOWN_CONTRADICTIONS = new Set<string>([]);
|
|
34
|
+
|
|
35
|
+
type Tpl = {
|
|
36
|
+
meta?: { slug?: string };
|
|
37
|
+
workspace?: { workspaceType?: string };
|
|
38
|
+
profiles?: unknown[];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const templates = readdirSync(HERE)
|
|
42
|
+
.filter((f) => f.endsWith(".yaml"))
|
|
43
|
+
.map((f) => load(readFileSync(join(HERE, f), "utf8")) as Tpl)
|
|
44
|
+
.filter((t) => t && typeof t === "object" && t.meta?.slug);
|
|
45
|
+
|
|
46
|
+
describe("a template that declares kinds can hold them", () => {
|
|
47
|
+
it("scans a plausible corpus (non-vacuity)", () => {
|
|
48
|
+
expect(templates.length).toBeGreaterThan(20);
|
|
49
|
+
const slugs = templates.map((t) => t.meta!.slug);
|
|
50
|
+
expect(slugs).toEqual(expect.arrayContaining(["finance", "crm"]));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("no kind-declaring template is typed operational/agent (except the ratchet)", () => {
|
|
54
|
+
const offenders = templates
|
|
55
|
+
.filter(
|
|
56
|
+
(t) =>
|
|
57
|
+
(t.profiles?.length ?? 0) > 0 &&
|
|
58
|
+
NON_DOMAIN_TYPES.has(t.workspace?.workspaceType ?? "")
|
|
59
|
+
)
|
|
60
|
+
.map((t) => t.meta!.slug!)
|
|
61
|
+
.sort();
|
|
62
|
+
expect(offenders.filter((s) => !KNOWN_CONTRADICTIONS.has(s))).toEqual([]);
|
|
63
|
+
expect(
|
|
64
|
+
[...KNOWN_CONTRADICTIONS].filter((s) => !offenders.includes(s)),
|
|
65
|
+
"resolved — remove from KNOWN_CONTRADICTIONS"
|
|
66
|
+
).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("the four domains named in the 2026-09-25 bug are domain homes", () => {
|
|
70
|
+
const bySlug = new Map(templates.map((t) => [t.meta!.slug, t]));
|
|
71
|
+
for (const slug of ["finance", "hr", "legal", "internal-runbook"]) {
|
|
72
|
+
expect(bySlug.get(slug)?.workspace?.workspaceType, slug).toBe("personal");
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|