@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,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
|
|
@@ -268,6 +272,8 @@ export interface WorkspaceDefinitionInput {
|
|
|
268
272
|
proposalId?: string;
|
|
269
273
|
profiles: WorkspaceYaml['profiles'];
|
|
270
274
|
views: NonNullable<WorkspaceYaml['views']>;
|
|
275
|
+
/** Template documents — created by the pod with the views (`utils/template-documents.ts`). */
|
|
276
|
+
documents?: WorkspaceYaml['documents'];
|
|
271
277
|
entityLinks: NonNullable<WorkspaceYaml['entityLinks']>;
|
|
272
278
|
seedEntities: NonNullable<WorkspaceYaml['seedEntities']>;
|
|
273
279
|
suggestedRelations?: WorkspaceYaml['suggestedRelations'];
|
|
@@ -330,6 +336,20 @@ export interface WorkspaceDefinitionInput {
|
|
|
330
336
|
workspaceType?: NonNullable<WorkspaceYaml['workspace']>['workspaceType'];
|
|
331
337
|
workspaceVisibility?: string;
|
|
332
338
|
workspaceCapabilities?: string[];
|
|
339
|
+
/**
|
|
340
|
+
* THE DECLARED INTENT SINK (Phase 3). `taskIntents` is authored at the TOP
|
|
341
|
+
* level of the template, not inside `workspace`, and is forwarded verbatim to
|
|
342
|
+
* `workspaces.createFromDefinition`, which persists it to
|
|
343
|
+
* `settings.taskIntents` — where the intent resolver joins it against what is
|
|
344
|
+
* actually installed. No shape transform: the authored form IS the wire form.
|
|
345
|
+
*/
|
|
346
|
+
taskIntents?: WorkspaceYaml['taskIntents'];
|
|
347
|
+
/**
|
|
348
|
+
* What the space PRODUCES (`meta.intent`), forwarded verbatim; the pod
|
|
349
|
+
* projects it into `settings.intent`. Same top-level field on the package
|
|
350
|
+
* door.
|
|
351
|
+
*/
|
|
352
|
+
intent?: WorkspaceYaml['meta']['intent'];
|
|
333
353
|
sourceRoles?: NonNullable<WorkspaceYaml['workspace']>['sourceRoles'];
|
|
334
354
|
defaultSources?: NonNullable<WorkspaceYaml['workspace']>['defaultSources'];
|
|
335
355
|
/** Per-workspace onboarding context — written to settings.onboarding. */
|
|
@@ -378,6 +398,30 @@ function toWorkspacePlaybooks(
|
|
|
378
398
|
}));
|
|
379
399
|
}
|
|
380
400
|
|
|
401
|
+
/**
|
|
402
|
+
* A template by slug (the bundled corpus, already compiled) or as authored
|
|
403
|
+
* YAML, with its lifecycle presets compiled (`adopts:` → flat options,
|
|
404
|
+
* generated `enumValues`; lifecycle-presets.ts), so both install doors carry
|
|
405
|
+
* the flat declaration the pod stores. An overlay that does not resolve throws,
|
|
406
|
+
* like an unknown slug: `validateTemplate` names it first on every catalog path.
|
|
407
|
+
*/
|
|
408
|
+
function compiledTemplate(slugOrYaml: string | WorkspaceYaml): WorkspaceYaml {
|
|
409
|
+
const tpl: WorkspaceYaml | undefined =
|
|
410
|
+
typeof slugOrYaml === 'string' ? getWorkspaceTemplate(slugOrYaml) : slugOrYaml;
|
|
411
|
+
if (!tpl) {
|
|
412
|
+
throw new Error(`Workspace template not found: ${String(slugOrYaml)}`);
|
|
413
|
+
}
|
|
414
|
+
const { template, problems } = resolveLifecyclePresets(tpl);
|
|
415
|
+
if (problems.length > 0) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
`Template "${String(tpl.meta?.slug)}": lifecycle presets do not resolve — ${problems
|
|
418
|
+
.map((p) => `${p.path}: ${p.message}`)
|
|
419
|
+
.join('; ')}`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
return template;
|
|
423
|
+
}
|
|
424
|
+
|
|
381
425
|
/**
|
|
382
426
|
* Resolve a template (by slug or YAML object) into the createFromDefinition
|
|
383
427
|
* payload. Pass `nameOverride` to rename the workspace at creation time.
|
|
@@ -386,12 +430,7 @@ export function toWorkspaceDefinition(
|
|
|
386
430
|
slugOrYaml: string | WorkspaceYaml,
|
|
387
431
|
nameOverride?: string
|
|
388
432
|
): 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
|
-
}
|
|
433
|
+
const tpl = compiledTemplate(slugOrYaml);
|
|
395
434
|
|
|
396
435
|
const ws = tpl.workspace;
|
|
397
436
|
const name = nameOverride ?? ws.name;
|
|
@@ -407,6 +446,9 @@ export function toWorkspaceDefinition(
|
|
|
407
446
|
proposalId,
|
|
408
447
|
profiles: tpl.profiles,
|
|
409
448
|
views: tpl.views ?? [],
|
|
449
|
+
// Absent when the template has none, so a template without documents
|
|
450
|
+
// compiles to the same definition (and content hash) as before.
|
|
451
|
+
...(tpl.documents?.length ? { documents: tpl.documents } : {}),
|
|
410
452
|
entityLinks: tpl.entityLinks ?? [],
|
|
411
453
|
seedEntities: tpl.seedEntities ?? [],
|
|
412
454
|
suggestedRelations: tpl.suggestedRelations,
|
|
@@ -432,6 +474,11 @@ export function toWorkspaceDefinition(
|
|
|
432
474
|
workspaceType: ws.workspaceType,
|
|
433
475
|
workspaceVisibility: ws.visibility,
|
|
434
476
|
workspaceCapabilities: ws.capabilities,
|
|
477
|
+
// Forwarded VERBATIM from the template's TOP level (not `workspace`) — the
|
|
478
|
+
// pod persists it to `settings.taskIntents` and reports any gap against what
|
|
479
|
+
// the installed capabilities actually provide. Authored form IS wire form.
|
|
480
|
+
taskIntents: tpl.taskIntents,
|
|
481
|
+
intent: tpl.meta.intent,
|
|
435
482
|
sourceRoles: ws.sourceRoles,
|
|
436
483
|
defaultSources: ws.defaultSources,
|
|
437
484
|
// Workspace creation persists its own compact playbook parameter shape.
|
|
@@ -486,6 +533,7 @@ type WorkspaceDefinitionForwarded =
|
|
|
486
533
|
| 'workspace' // flattened into workspaceName/description/workspaceType/…
|
|
487
534
|
| 'profiles'
|
|
488
535
|
| 'views'
|
|
536
|
+
| 'documents' // → carried verbatim; created with the views by the pod
|
|
489
537
|
| 'entityLinks'
|
|
490
538
|
| 'seedEntities'
|
|
491
539
|
| 'suggestedRelations'
|
|
@@ -500,7 +548,11 @@ type WorkspaceDefinitionForwarded =
|
|
|
500
548
|
| 'actionPlacements' // → settings.actionPlacements via applyPackagePostWorkspace
|
|
501
549
|
| 'automations' // → flowAutomations → shared applyPackagePostWorkspace door
|
|
502
550
|
| 'onboarding'
|
|
503
|
-
| 'dependencies'
|
|
551
|
+
| 'dependencies'
|
|
552
|
+
// Phase 3 gave the declaration a sink: `createWorkspaceFromDefinition`
|
|
553
|
+
// accepts `taskIntents` and persists it verbatim to `settings.taskIntents`,
|
|
554
|
+
// where the intent resolver joins it against what is actually installed.
|
|
555
|
+
| 'taskIntents';
|
|
504
556
|
|
|
505
557
|
/**
|
|
506
558
|
* Keys deliberately NOT forwarded by `toWorkspaceDefinition`. Each needs a
|
|
@@ -508,8 +560,21 @@ type WorkspaceDefinitionForwarded =
|
|
|
508
560
|
* else is a real gap and should say so.
|
|
509
561
|
*/
|
|
510
562
|
type WorkspaceDefinitionDropped =
|
|
563
|
+
// A POST-workspace layer (rule rows via the pod's rule door), carried on the
|
|
564
|
+
// PACKAGE definition (`toPackageDefinition`), not the workspace door's input.
|
|
565
|
+
// The workspace door has no rules field; the rule reconcile converges them.
|
|
566
|
+
| 'rules'
|
|
567
|
+
// Same layer as `rules`: the skill is LINKED to the space by
|
|
568
|
+
// `applyPackagePostWorkspace` (`skills.workspaceId` + `scope:"workspace"`),
|
|
569
|
+
// which owns the row. The workspace door has no skills field, so forwarding
|
|
570
|
+
// it here would be a no-op — it is carried on the PACKAGE definition instead.
|
|
571
|
+
| 'skills'
|
|
511
572
|
// Normalized into `layoutConfig.primarySurface`.
|
|
512
573
|
| 'app'
|
|
574
|
+
// Same layer as `skills`: built into frame cells that `applyPackagePostWorkspace`
|
|
575
|
+
// installs in the space. The workspace door has no cells field; carried on
|
|
576
|
+
// the PACKAGE definition as `cells[]` instead.
|
|
577
|
+
| 'spacePackage'
|
|
513
578
|
// The door DOES accept `extends`, but no template declares it today (0/21).
|
|
514
579
|
// Forwarding is a no-op until one does — wire it then.
|
|
515
580
|
| 'extends';
|
|
@@ -543,6 +608,12 @@ void _workspaceDefinitionLedger;
|
|
|
543
608
|
export interface PackageDefinitionOutput {
|
|
544
609
|
_meta: {
|
|
545
610
|
slug: string;
|
|
611
|
+
/** The PACKAGE's own name (meta.name). For an overlay this differs from
|
|
612
|
+
* `workspaceName`, which names the workspace it composes onto. */
|
|
613
|
+
name?: string;
|
|
614
|
+
/** The PACKAGE's own description (meta.description) — what the catalog
|
|
615
|
+
* lists. Top-level `description` stays the workspace's (install copy). */
|
|
616
|
+
description?: string;
|
|
546
617
|
icon?: string;
|
|
547
618
|
color?: string;
|
|
548
619
|
domain?: string;
|
|
@@ -556,10 +627,37 @@ export interface PackageDefinitionOutput {
|
|
|
556
627
|
workspaceSubtype?: string;
|
|
557
628
|
workspaceVisibility?: string;
|
|
558
629
|
workspaceCapabilities?: string[];
|
|
630
|
+
/**
|
|
631
|
+
* The DECLARED intents, carried to the marketplace/CLI install path too.
|
|
632
|
+
* Without it here, a workspace installed from the marketplace would LOSE its
|
|
633
|
+
* declaration while a bundled one kept it — the same field behaving
|
|
634
|
+
* differently per install route, which is precisely the fork this package
|
|
635
|
+
* exists to prevent.
|
|
636
|
+
*/
|
|
637
|
+
taskIntents?: WorkspaceYaml['taskIntents'];
|
|
638
|
+
/**
|
|
639
|
+
* What the space PRODUCES (`meta.intent`), TOP-LEVEL like `taskIntents` —
|
|
640
|
+
* never in `_meta`, which the CP rebuilds from its row columns on serve
|
|
641
|
+
* (`normalizePublishedDefinition`), so a key there never reaches a pod.
|
|
642
|
+
*/
|
|
643
|
+
intent?: WorkspaceYaml['meta']['intent'];
|
|
644
|
+
/**
|
|
645
|
+
* The onboarding start (`meta.start`), TOP-LEVEL for the same reason as
|
|
646
|
+
* `intent`. Package door only: the pod and the CP read starts off the package.
|
|
647
|
+
*/
|
|
648
|
+
start?: WorkspaceYaml['meta']['start'];
|
|
559
649
|
sourceRoles?: WorkspaceYaml['workspace']['sourceRoles'];
|
|
560
650
|
defaultSources?: WorkspaceYaml['workspace']['defaultSources'];
|
|
561
651
|
profiles: WorkspaceYaml['profiles'];
|
|
562
652
|
views: NonNullable<WorkspaceYaml['views']>;
|
|
653
|
+
/**
|
|
654
|
+
* Template documents — created by the pod with the views
|
|
655
|
+
* (`utils/template-documents.ts`); the Hub `PackageApplySchema` slots them.
|
|
656
|
+
* The CP publish door does NOT yet (it strips the field and refuses a
|
|
657
|
+
* `kind: "document"` sidebar item), so no official template may declare
|
|
658
|
+
* documents until it does.
|
|
659
|
+
*/
|
|
660
|
+
documents?: WorkspaceYaml['documents'];
|
|
563
661
|
suggestedEntities: NonNullable<WorkspaceYaml['seedEntities']>;
|
|
564
662
|
suggestedRelations?: WorkspaceYaml['suggestedRelations'];
|
|
565
663
|
displayTemplates?: WorkspaceYaml['displayTemplates'];
|
|
@@ -596,6 +694,24 @@ export interface PackageDefinitionOutput {
|
|
|
596
694
|
*/
|
|
597
695
|
actionPlacements?: WorkspaceYaml['actionPlacements'];
|
|
598
696
|
onboarding?: WorkspaceYaml['onboarding'];
|
|
697
|
+
/**
|
|
698
|
+
* Rules the template installs in its space — applied by the pod's
|
|
699
|
+
* `applyPackagePostWorkspace` through the ONE rule door and converged by the
|
|
700
|
+
* template-rule reconcile (three-way stamp on `metadata.rule.seed`).
|
|
701
|
+
*/
|
|
702
|
+
rules?: WorkspaceYaml['rules'];
|
|
703
|
+
/**
|
|
704
|
+
* Skills the template declares for its space — linked to the new space by the
|
|
705
|
+
* pod's `applyPackagePostWorkspace` (approved skills only), which is why they
|
|
706
|
+
* travel on the PACKAGE definition and not the workspace door's input.
|
|
707
|
+
*/
|
|
708
|
+
skills?: WorkspaceYaml['skills'];
|
|
709
|
+
/**
|
|
710
|
+
* The space package's built modules (`spacePackage` → `space-packages.ts`),
|
|
711
|
+
* installed by the pod's `applyPackagePostWorkspace` as sandboxed frame cells
|
|
712
|
+
* `cell:<_meta.slug>:<key>` — what the template's `app: {kind: stored}` opens.
|
|
713
|
+
*/
|
|
714
|
+
cells?: SpacePackageCellWire[];
|
|
599
715
|
/** Template-composition dependencies — resolved by the apply endpoint. */
|
|
600
716
|
dependencies?: WorkspaceYaml['dependencies'];
|
|
601
717
|
}
|
|
@@ -609,20 +725,18 @@ export interface PackageDefinitionOutput {
|
|
|
609
725
|
export function toPackageDefinition(
|
|
610
726
|
slugOrYaml: string | WorkspaceYaml
|
|
611
727
|
): 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
|
-
}
|
|
728
|
+
const tpl = compiledTemplate(slugOrYaml);
|
|
618
729
|
|
|
619
730
|
const ws = tpl.workspace;
|
|
620
731
|
// Same routing as the tRPC door — the union is split by `kind`, never dumped
|
|
621
732
|
// whole into one field. See `splitBentoBlocks`.
|
|
622
733
|
const bento = splitBentoBlocks(tpl.bento?.blocks);
|
|
734
|
+
const cells = spacePackageCells(tpl);
|
|
623
735
|
return {
|
|
624
736
|
_meta: {
|
|
625
737
|
slug: tpl.meta.slug,
|
|
738
|
+
name: tpl.meta.name,
|
|
739
|
+
description: tpl.meta.description,
|
|
626
740
|
icon: tpl.meta.icon,
|
|
627
741
|
color: tpl.meta.color,
|
|
628
742
|
domain: tpl.meta.domain,
|
|
@@ -635,10 +749,16 @@ export function toPackageDefinition(
|
|
|
635
749
|
workspaceSubtype: ws.subtype,
|
|
636
750
|
workspaceVisibility: ws.visibility,
|
|
637
751
|
workspaceCapabilities: ws.capabilities,
|
|
752
|
+
// Same declaration, same wire field, on the marketplace path — see the
|
|
753
|
+
// `taskIntents` note on the PackageDefinition type above.
|
|
754
|
+
taskIntents: tpl.taskIntents,
|
|
755
|
+
...(tpl.meta.intent ? { intent: tpl.meta.intent } : {}),
|
|
756
|
+
...(tpl.meta.start ? { start: tpl.meta.start } : {}),
|
|
638
757
|
sourceRoles: ws.sourceRoles,
|
|
639
758
|
defaultSources: ws.defaultSources,
|
|
640
759
|
profiles: tpl.profiles,
|
|
641
760
|
views: tpl.views ?? [],
|
|
761
|
+
...(tpl.documents?.length ? { documents: tpl.documents } : {}),
|
|
642
762
|
suggestedEntities: tpl.seedEntities ?? [],
|
|
643
763
|
suggestedRelations: tpl.suggestedRelations,
|
|
644
764
|
displayTemplates: tpl.displayTemplates,
|
|
@@ -655,6 +775,9 @@ export function toPackageDefinition(
|
|
|
655
775
|
automations: toAutomationWire(tpl.automations),
|
|
656
776
|
actionPlacements: tpl.actionPlacements,
|
|
657
777
|
onboarding: tpl.onboarding,
|
|
778
|
+
...(tpl.rules?.length ? { rules: tpl.rules } : {}),
|
|
779
|
+
...(tpl.skills?.length ? { skills: tpl.skills } : {}),
|
|
780
|
+
...(cells ? { cells } : {}),
|
|
658
781
|
dependencies: tpl.dependencies,
|
|
659
782
|
};
|
|
660
783
|
}
|
|
@@ -672,6 +795,7 @@ type PackageDefinitionForwarded =
|
|
|
672
795
|
| 'workspace' // flattened into workspaceName/description/workspaceType/…
|
|
673
796
|
| 'profiles'
|
|
674
797
|
| 'views'
|
|
798
|
+
| 'documents' // → carried verbatim (Hub PackageApplySchema slots it)
|
|
675
799
|
| 'seedEntities' // → suggestedEntities
|
|
676
800
|
| 'suggestedRelations'
|
|
677
801
|
| 'displayTemplates'
|
|
@@ -686,6 +810,9 @@ type PackageDefinitionForwarded =
|
|
|
686
810
|
| 'automations' // → mapped to the apply door's flowDefinition wire shape
|
|
687
811
|
| 'actionPlacements' // → carried verbatim (Hub inbound schema declares it — parity closed)
|
|
688
812
|
| 'onboarding'
|
|
813
|
+
| 'rules' // → carried verbatim (applied post-workspace via the rule door)
|
|
814
|
+
| 'skills' // → carried verbatim (linked to the space post-workspace)
|
|
815
|
+
| 'spacePackage' // → cells (the built frame modules, installed post-workspace)
|
|
689
816
|
| 'dependencies';
|
|
690
817
|
|
|
691
818
|
/** Keys deliberately NOT forwarded by `toPackageDefinition`. */
|
|
@@ -696,7 +823,9 @@ type PackageDefinitionDropped =
|
|
|
696
823
|
// (package-definition.ts:262), but `PackageDefinitionOutput` omits it. No
|
|
697
824
|
// template declares it today (0/21), so this is latent, not live — wire it
|
|
698
825
|
// when a template first uses one.
|
|
699
|
-
| 'extends'
|
|
826
|
+
| 'extends'
|
|
827
|
+
// Phase 1 declaration with no sink yet — see `WorkspaceDefinitionDropped`.
|
|
828
|
+
| 'taskIntents';
|
|
700
829
|
|
|
701
830
|
type UnclassifiedPackageDefinitionKey = Exclude<
|
|
702
831
|
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
|
+
});
|