@synap-core/workspace-templates 0.11.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 +189 -900
- package/dist/index.js +24654 -21639
- 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 +41 -27
- 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.template.test.ts +25 -19
- package/src/base.yaml +95 -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 +798 -92
- 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 +598 -0
- package/src/catalog-safety.test.ts +198 -0
- package/src/catalog.template-items.test.ts +75 -0
- package/src/catalog.ts +85 -2
- package/src/civic-france.yaml +102 -0
- 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/dist-freshness.test.ts +97 -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 +130 -28
- 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 +57 -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 +65 -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 +176 -0
- package/src/send-message-maps.test.ts +227 -0
- package/src/shared-role-ssot.test.ts +42 -1
- 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 +21639 -21522
- 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 +32 -2
- package/src/validate-onboarding.test.ts +59 -0
- package/src/validate.test.ts +156 -3
- package/src/validate.ts +1384 -7
- package/src/wire-core.ts +231 -0
- package/src/wire.test.ts +159 -0
- package/src/wire.ts +107 -0
- package/src/__fixtures__/report-automation-flow.json +0 -393
- 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,494 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ten base lifecycles, as TEMPLATE-TIME presets.
|
|
3
|
+
* ==================================================
|
|
4
|
+
*
|
|
5
|
+
* A profile's lifecycle may be written in full (`options: [...]`) or adopt one
|
|
6
|
+
* of the ten bases below and say only how it differs:
|
|
7
|
+
*
|
|
8
|
+
* lifecycle:
|
|
9
|
+
* property: brand-status
|
|
10
|
+
* adopts: asset
|
|
11
|
+
* rename: { in-review: studied, deprecated: archived } # base key → this kind's key
|
|
12
|
+
* drop: [in-review] # base keys this kind does not use
|
|
13
|
+
* add: # steps the base does not have
|
|
14
|
+
* - { key: signed, category: active, after: sent } # `after` / `before` a resulting key
|
|
15
|
+
* humanOnly: { deprecated: true } # per resulting key
|
|
16
|
+
* stuckAfterDays: { sent: 14 } # per resulting key
|
|
17
|
+
* dateBinding: { sent: dueDate } # per resulting key
|
|
18
|
+
* waits: { sent: { on: external, waitKind: payment } } # per resulting key
|
|
19
|
+
*
|
|
20
|
+
* `resolveLifecyclePresets` compiles that into the flat declaration the pod
|
|
21
|
+
* already stores (`{ property, options }`), and fills the lifecycle select's
|
|
22
|
+
* `enumValues` from the options when the YAML leaves them out (the pod
|
|
23
|
+
* replaces them with the option keys anyway, `declare-lifecycle.ts`). There is
|
|
24
|
+
* no inheritance at runtime: the pod never sees `adopts`, and a change to a
|
|
25
|
+
* base reaches a kind only when its template is rebuilt and republished.
|
|
26
|
+
*
|
|
27
|
+
* Decision: status-flow plan, document aac47c4a, Q1.3 ("ten bases, as template
|
|
28
|
+
* presets, not a runtime engine"); the steps are the deep dive's table
|
|
29
|
+
* (document 152478c2 §1), `humanOnly` and `stuckAfterDays` where it gives them
|
|
30
|
+
* (§1 table 👤, §7 agreement sketch). The deep dive's "waits on the outside" (⧗)
|
|
31
|
+
* is not encoded in the bases; a kind declares a step's wait (`waits`) and its
|
|
32
|
+
* date (`dateBinding`) itself, on an added step or per resulting key. Their
|
|
33
|
+
* shapes are the validator's (`lifecycleOptionWaitsProblems`, the pod's rules):
|
|
34
|
+
* this compiler only carries them.
|
|
35
|
+
*
|
|
36
|
+
* Callers: `generate.mjs` (build + every corpus test), `validateTemplate` (any
|
|
37
|
+
* authored YAML), `toWorkspaceDefinition` / `toPackageDefinition`, and the
|
|
38
|
+
* catalog loader (through the built dist). The resolver is idempotent: a flat
|
|
39
|
+
* lifecycle with its `enumValues` comes back as the same object.
|
|
40
|
+
*
|
|
41
|
+
* Only `import type` here: `build.mjs` and `generate.mjs` load this file under
|
|
42
|
+
* Node's type stripping, before `dist/` exists.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import type {
|
|
46
|
+
TemplateLifecycle,
|
|
47
|
+
TemplateLifecycleCategory,
|
|
48
|
+
TemplateLifecycleOption,
|
|
49
|
+
TemplateLifecycleOptionWaits,
|
|
50
|
+
TemplateProfile,
|
|
51
|
+
TemplateProperty,
|
|
52
|
+
WorkspaceYaml,
|
|
53
|
+
} from "./types.ts";
|
|
54
|
+
|
|
55
|
+
type BaseStep = Pick<
|
|
56
|
+
TemplateLifecycleOption,
|
|
57
|
+
"key" | "category" | "tone" | "humanOnly" | "stuckAfterDays"
|
|
58
|
+
>;
|
|
59
|
+
|
|
60
|
+
const step = (
|
|
61
|
+
key: string,
|
|
62
|
+
category: TemplateLifecycleCategory,
|
|
63
|
+
extra: Partial<BaseStep> = {}
|
|
64
|
+
): BaseStep => ({
|
|
65
|
+
key,
|
|
66
|
+
category,
|
|
67
|
+
...extra,
|
|
68
|
+
});
|
|
69
|
+
const PERSON = { humanOnly: true } as const;
|
|
70
|
+
/**
|
|
71
|
+
* A lost step that is a DECISION (cancelled, withdrawn, superseded, archived,
|
|
72
|
+
* deprecated), not a failure, wears the muted tone. Without it the pod paints
|
|
73
|
+
* it with the lost category's default, `error` (`LIFECYCLE_CATEGORY_TONE`,
|
|
74
|
+
* `@synap-core/types/lifecycle`). Failures (failed, a lost deal, churned) keep
|
|
75
|
+
* that default. UX review of 2026-10-29.
|
|
76
|
+
*/
|
|
77
|
+
const ENDED = { tone: "textMuted" } as const;
|
|
78
|
+
|
|
79
|
+
/** The ten bases: default steps in picker order (open → active → paused → done → lost). */
|
|
80
|
+
export const BASE_LIFECYCLES = {
|
|
81
|
+
"work-item": [
|
|
82
|
+
step("backlog", "open"),
|
|
83
|
+
step("todo", "open"),
|
|
84
|
+
step("doing", "active"),
|
|
85
|
+
step("review", "active"),
|
|
86
|
+
step("on-hold", "paused"),
|
|
87
|
+
step("done", "done"),
|
|
88
|
+
// Every `cancelled` the corpus declares is a person's call (features, engagements, campaigns).
|
|
89
|
+
step("cancelled", "lost", { ...PERSON, ...ENDED }),
|
|
90
|
+
],
|
|
91
|
+
run: [
|
|
92
|
+
step("queued", "open"),
|
|
93
|
+
step("running", "active"),
|
|
94
|
+
step("waiting", "paused"),
|
|
95
|
+
step("succeeded", "done"),
|
|
96
|
+
step("failed", "lost"),
|
|
97
|
+
step("cancelled", "lost", ENDED),
|
|
98
|
+
],
|
|
99
|
+
decision: [
|
|
100
|
+
step("proposed", "open"),
|
|
101
|
+
step("discussing", "active"),
|
|
102
|
+
step("on-hold", "paused"),
|
|
103
|
+
step("accepted", "done"),
|
|
104
|
+
step("rejected", "lost"),
|
|
105
|
+
step("superseded", "lost", ENDED),
|
|
106
|
+
step("withdrawn", "lost", ENDED),
|
|
107
|
+
step("expired", "lost"),
|
|
108
|
+
],
|
|
109
|
+
// No paused step: an asset is in use or deprecated (plan Q1.2).
|
|
110
|
+
asset: [
|
|
111
|
+
step("draft", "open"),
|
|
112
|
+
step("in-review", "active"),
|
|
113
|
+
step("approved", "done"),
|
|
114
|
+
step("deprecated", "lost", ENDED),
|
|
115
|
+
],
|
|
116
|
+
publication: [
|
|
117
|
+
step("idea", "open"),
|
|
118
|
+
step("drafting", "active"),
|
|
119
|
+
step("review", "active"),
|
|
120
|
+
step("scheduled", "active"),
|
|
121
|
+
step("on-hold", "paused"),
|
|
122
|
+
step("published", "done"),
|
|
123
|
+
step("archived", "lost", ENDED),
|
|
124
|
+
],
|
|
125
|
+
agreement: [
|
|
126
|
+
step("draft", "open"),
|
|
127
|
+
step("sent", "active", { stuckAfterDays: 7 }),
|
|
128
|
+
step("in-progress", "active"),
|
|
129
|
+
step("on-hold", "paused"),
|
|
130
|
+
step("fulfilled", "done"),
|
|
131
|
+
step("declined", "lost", PERSON),
|
|
132
|
+
step("void", "lost", PERSON),
|
|
133
|
+
step("expired", "lost"),
|
|
134
|
+
],
|
|
135
|
+
pipeline: [
|
|
136
|
+
step("new", "open"),
|
|
137
|
+
step("contacted", "active"),
|
|
138
|
+
step("qualified", "active"),
|
|
139
|
+
step("proposal", "active"),
|
|
140
|
+
step("nurturing", "paused"),
|
|
141
|
+
step("won", "done", PERSON),
|
|
142
|
+
step("lost", "lost", PERSON),
|
|
143
|
+
],
|
|
144
|
+
// `former` is a clean end, `churned` a loss (plan Q1.4).
|
|
145
|
+
relationship: [
|
|
146
|
+
step("prospect", "open"),
|
|
147
|
+
step("active", "active"),
|
|
148
|
+
step("paused", "paused"),
|
|
149
|
+
step("former", "done"),
|
|
150
|
+
step("churned", "lost"),
|
|
151
|
+
],
|
|
152
|
+
offering: [
|
|
153
|
+
step("planned", "open"),
|
|
154
|
+
step("building", "active"),
|
|
155
|
+
step("live", "active"),
|
|
156
|
+
step("paused", "paused"),
|
|
157
|
+
step("retired", "done"),
|
|
158
|
+
step("abandoned", "lost"),
|
|
159
|
+
],
|
|
160
|
+
initiative: [
|
|
161
|
+
step("proposed", "open"),
|
|
162
|
+
step("agreed", "open", PERSON),
|
|
163
|
+
step("ready", "open"),
|
|
164
|
+
step("active", "active"),
|
|
165
|
+
step("on-hold", "paused"),
|
|
166
|
+
step("completed", "done", PERSON),
|
|
167
|
+
step("dropped", "lost"),
|
|
168
|
+
],
|
|
169
|
+
} as const satisfies Record<string, readonly BaseStep[]>;
|
|
170
|
+
|
|
171
|
+
export type BaseLifecycleName = keyof typeof BASE_LIFECYCLES;
|
|
172
|
+
|
|
173
|
+
/** A step a kind adds to its base. */
|
|
174
|
+
export interface TemplateLifecycleStepAdd extends TemplateLifecycleOption {
|
|
175
|
+
/** Insert after this resulting key (default: after the last step of its category). */
|
|
176
|
+
after?: string;
|
|
177
|
+
/** Insert before this resulting key. */
|
|
178
|
+
before?: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The authored form of a lifecycle that adopts a base; compiled to `TemplateLifecycle`. */
|
|
182
|
+
export interface TemplateLifecycleAdoption {
|
|
183
|
+
property: string;
|
|
184
|
+
adopts: BaseLifecycleName;
|
|
185
|
+
/** Base key → this kind's key. */
|
|
186
|
+
rename?: Record<string, string>;
|
|
187
|
+
/** Base keys this kind does not use. */
|
|
188
|
+
drop?: string[];
|
|
189
|
+
add?: TemplateLifecycleStepAdd[];
|
|
190
|
+
/** Per resulting key; `false` removes a base's person-only mark. */
|
|
191
|
+
humanOnly?: Record<string, boolean>;
|
|
192
|
+
/** Per resulting key. */
|
|
193
|
+
stuckAfterDays?: Record<string, number>;
|
|
194
|
+
/** Per resulting key: the date property the step is bound to. */
|
|
195
|
+
dateBinding?: Record<string, string>;
|
|
196
|
+
/** Per resulting key: the wait the step declares. */
|
|
197
|
+
waits?: Record<string, TemplateLifecycleOptionWaits>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface LifecyclePresetProblem {
|
|
201
|
+
path: string;
|
|
202
|
+
message: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface LifecyclePresetResolution {
|
|
206
|
+
/** The template with every resolvable lifecycle flat; the same object when nothing changed. */
|
|
207
|
+
template: WorkspaceYaml;
|
|
208
|
+
/** Overlays that could not be resolved; their lifecycle is left as authored. */
|
|
209
|
+
problems: LifecyclePresetProblem[];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const ADOPTION_KEYS = new Set([
|
|
213
|
+
"property",
|
|
214
|
+
"adopts",
|
|
215
|
+
"rename",
|
|
216
|
+
"drop",
|
|
217
|
+
"add",
|
|
218
|
+
"humanOnly",
|
|
219
|
+
"stuckAfterDays",
|
|
220
|
+
"dateBinding",
|
|
221
|
+
"waits",
|
|
222
|
+
]);
|
|
223
|
+
const ADD_KEYS = new Set([
|
|
224
|
+
"key",
|
|
225
|
+
"label",
|
|
226
|
+
"category",
|
|
227
|
+
"tone",
|
|
228
|
+
"humanOnly",
|
|
229
|
+
"stuckAfterDays",
|
|
230
|
+
"dateBinding",
|
|
231
|
+
"waits",
|
|
232
|
+
"after",
|
|
233
|
+
"before",
|
|
234
|
+
]);
|
|
235
|
+
const CATEGORY_ORDER: readonly TemplateLifecycleCategory[] = [
|
|
236
|
+
"open",
|
|
237
|
+
"active",
|
|
238
|
+
"paused",
|
|
239
|
+
"done",
|
|
240
|
+
"lost",
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
const isRecord = (v: unknown): v is Record<string, unknown> =>
|
|
244
|
+
!!v && typeof v === "object" && !Array.isArray(v);
|
|
245
|
+
|
|
246
|
+
/** One option in the key order the corpus writes: key, label, category, tone, humanOnly, stuckAfterDays, dateBinding, waits. */
|
|
247
|
+
function option(o: Partial<TemplateLifecycleOption>): TemplateLifecycleOption {
|
|
248
|
+
const out: Record<string, unknown> = { key: o.key };
|
|
249
|
+
if (o.label !== undefined) out.label = o.label;
|
|
250
|
+
out.category = o.category;
|
|
251
|
+
if (o.tone !== undefined) out.tone = o.tone;
|
|
252
|
+
if (o.humanOnly) out.humanOnly = true;
|
|
253
|
+
if (o.stuckAfterDays !== undefined) out.stuckAfterDays = o.stuckAfterDays;
|
|
254
|
+
if (o.dateBinding !== undefined) out.dateBinding = o.dateBinding;
|
|
255
|
+
if (o.waits !== undefined) out.waits = o.waits;
|
|
256
|
+
return out as unknown as TemplateLifecycleOption;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Compile one `adopts:` lifecycle; `null` options when it has problems. */
|
|
260
|
+
function resolveAdoption(
|
|
261
|
+
lc: Record<string, unknown>,
|
|
262
|
+
at: string,
|
|
263
|
+
problems: LifecyclePresetProblem[]
|
|
264
|
+
): TemplateLifecycle | null {
|
|
265
|
+
const before = problems.length;
|
|
266
|
+
const fail = (path: string, message: string) =>
|
|
267
|
+
problems.push({ path: `${at}${path}`, message });
|
|
268
|
+
|
|
269
|
+
for (const k of Object.keys(lc))
|
|
270
|
+
if (!ADOPTION_KEYS.has(k))
|
|
271
|
+
fail(
|
|
272
|
+
`.${k}`,
|
|
273
|
+
k === "options"
|
|
274
|
+
? "a lifecycle either adopts a base or lists its options, not both"
|
|
275
|
+
: `"${k}" is not a lifecycle overlay key (${[...ADOPTION_KEYS].join(", ")})`
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const name = lc.adopts as string;
|
|
279
|
+
if (!Object.hasOwn(BASE_LIFECYCLES, name)) {
|
|
280
|
+
fail(
|
|
281
|
+
".adopts",
|
|
282
|
+
`"${String(name)}" is not a base lifecycle (${Object.keys(BASE_LIFECYCLES).join(", ")})`
|
|
283
|
+
);
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const base: readonly BaseStep[] = BASE_LIFECYCLES[name as BaseLifecycleName];
|
|
287
|
+
const baseKeys = new Set(base.map((s) => s.key));
|
|
288
|
+
const isBaseKey = (k: unknown, path: string) => {
|
|
289
|
+
if (typeof k === "string" && baseKeys.has(k)) return true;
|
|
290
|
+
fail(
|
|
291
|
+
path,
|
|
292
|
+
`"${String(k)}" is not a step of the ${name} base (${[...baseKeys].join(", ")})`
|
|
293
|
+
);
|
|
294
|
+
return false;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const rename = isRecord(lc.rename)
|
|
298
|
+
? (lc.rename as Record<string, unknown>)
|
|
299
|
+
: {};
|
|
300
|
+
if (lc.rename !== undefined && !isRecord(lc.rename))
|
|
301
|
+
fail(".rename", "rename maps a base key to this kind's key");
|
|
302
|
+
for (const [from, to] of Object.entries(rename)) {
|
|
303
|
+
isBaseKey(from, `.rename.${from}`);
|
|
304
|
+
if (typeof to !== "string" || to.length === 0)
|
|
305
|
+
fail(`.rename.${from}`, "a renamed key is a non-empty string");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const drop = Array.isArray(lc.drop) ? (lc.drop as unknown[]) : [];
|
|
309
|
+
if (lc.drop !== undefined && !Array.isArray(lc.drop))
|
|
310
|
+
fail(".drop", "drop lists base keys");
|
|
311
|
+
drop.forEach((k, i) => {
|
|
312
|
+
if (!isBaseKey(k, `.drop[${i}]`)) return;
|
|
313
|
+
if (Object.hasOwn(rename, k as string))
|
|
314
|
+
fail(`.drop[${i}]`, `"${String(k)}" is both renamed and dropped`);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const steps: TemplateLifecycleOption[] = base
|
|
318
|
+
.filter((s) => !drop.includes(s.key))
|
|
319
|
+
.map((s) =>
|
|
320
|
+
option({ ...s, key: (rename[s.key] as string | undefined) ?? s.key })
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const adds = Array.isArray(lc.add) ? (lc.add as unknown[]) : [];
|
|
324
|
+
if (lc.add !== undefined && !Array.isArray(lc.add))
|
|
325
|
+
fail(".add", "add lists steps");
|
|
326
|
+
adds.forEach((raw, i) => {
|
|
327
|
+
const path = `.add[${i}]`;
|
|
328
|
+
if (
|
|
329
|
+
!isRecord(raw) ||
|
|
330
|
+
typeof raw.key !== "string" ||
|
|
331
|
+
typeof raw.category !== "string"
|
|
332
|
+
) {
|
|
333
|
+
fail(path, "an added step has a key and a category");
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const unknownKeys = Object.keys(raw).filter((k) => !ADD_KEYS.has(k));
|
|
337
|
+
if (unknownKeys.length > 0) {
|
|
338
|
+
fail(
|
|
339
|
+
path,
|
|
340
|
+
`${unknownKeys.map((k) => `"${k}"`).join(", ")} is not a step field (${[...ADD_KEYS].join(", ")})`
|
|
341
|
+
);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const {
|
|
345
|
+
after,
|
|
346
|
+
before: beforeKey,
|
|
347
|
+
...rest
|
|
348
|
+
} = raw as unknown as TemplateLifecycleStepAdd;
|
|
349
|
+
if (after !== undefined && beforeKey !== undefined)
|
|
350
|
+
fail(path, "an added step goes after OR before a step, not both");
|
|
351
|
+
const anchor = after ?? beforeKey;
|
|
352
|
+
let index: number;
|
|
353
|
+
if (anchor !== undefined) {
|
|
354
|
+
const found = steps.findIndex((s) => s.key === anchor);
|
|
355
|
+
if (found < 0) {
|
|
356
|
+
fail(
|
|
357
|
+
`${path}.${after !== undefined ? "after" : "before"}`,
|
|
358
|
+
`"${anchor}" is not a step of this lifecycle`
|
|
359
|
+
);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
index = after !== undefined ? found + 1 : found;
|
|
363
|
+
} else {
|
|
364
|
+
// After the last step of its category, else before the first step of a later one.
|
|
365
|
+
const rank = CATEGORY_ORDER.indexOf(rest.category);
|
|
366
|
+
const later = steps.findIndex(
|
|
367
|
+
(s) => CATEGORY_ORDER.indexOf(s.category) > rank
|
|
368
|
+
);
|
|
369
|
+
index = later < 0 ? steps.length : later;
|
|
370
|
+
}
|
|
371
|
+
steps.splice(index, 0, option(rest));
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const keys = steps.map((s) => s.key);
|
|
375
|
+
keys.forEach((k, i) => {
|
|
376
|
+
if (keys.indexOf(k) !== i)
|
|
377
|
+
fail("", `step "${k}" appears twice after the overlay`);
|
|
378
|
+
});
|
|
379
|
+
const byKey = new Map(steps.map((s) => [s.key, s]));
|
|
380
|
+
// Each override's TYPE is checked here; a `dateBinding` / `waits` shape is
|
|
381
|
+
// the validator's (`lifecycleOptionWaitsProblems`), which runs on the result.
|
|
382
|
+
const OVERRIDE_TYPE = {
|
|
383
|
+
humanOnly: ["boolean", "a boolean"],
|
|
384
|
+
stuckAfterDays: ["number", "a number of days"],
|
|
385
|
+
dateBinding: ["string", "a date property's slug"],
|
|
386
|
+
waits: ["object", "an object { on, waitKind?, refFrom?, dateBinding? }"],
|
|
387
|
+
} as const;
|
|
388
|
+
for (const field of ["humanOnly", "stuckAfterDays", "dateBinding", "waits"] as const) {
|
|
389
|
+
const overrides = lc[field];
|
|
390
|
+
if (overrides === undefined) continue;
|
|
391
|
+
if (!isRecord(overrides)) {
|
|
392
|
+
fail(`.${field}`, `${field} maps a step key to its value`);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
for (const [k, v] of Object.entries(overrides)) {
|
|
396
|
+
const s = byKey.get(k);
|
|
397
|
+
if (!s) {
|
|
398
|
+
fail(
|
|
399
|
+
`.${field}.${k}`,
|
|
400
|
+
`"${k}" is not a step of this lifecycle (${keys.join(", ")})`
|
|
401
|
+
);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
const [type, words] = OVERRIDE_TYPE[field];
|
|
405
|
+
if (typeof v !== type || v === null || Array.isArray(v)) {
|
|
406
|
+
fail(`.${field}.${k}`, `${field} of "${k}" must be ${words}`);
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
const next = option({ ...s, [field]: v });
|
|
410
|
+
steps[steps.indexOf(s)] = next;
|
|
411
|
+
byKey.set(k, next);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (!steps.some((s) => s.category === "done"))
|
|
415
|
+
fail("", "a lifecycle keeps at least one done step");
|
|
416
|
+
|
|
417
|
+
return problems.length === before
|
|
418
|
+
? { property: lc.property as string, options: steps }
|
|
419
|
+
: null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** The property with its `enumValues` generated from the options, placed after `inputType`. */
|
|
423
|
+
function withEnumValues(
|
|
424
|
+
prop: TemplateProperty,
|
|
425
|
+
keys: string[]
|
|
426
|
+
): TemplateProperty {
|
|
427
|
+
const out: Record<string, unknown> = {};
|
|
428
|
+
for (const [k, v] of Object.entries(prop)) {
|
|
429
|
+
out[k] = v;
|
|
430
|
+
if (k === "inputType") out.enumValues = keys;
|
|
431
|
+
}
|
|
432
|
+
if (!("enumValues" in out)) out.enumValues = keys;
|
|
433
|
+
return out as unknown as TemplateProperty;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function resolveProfile(
|
|
437
|
+
profile: TemplateProfile,
|
|
438
|
+
problems: LifecyclePresetProblem[]
|
|
439
|
+
): TemplateProfile {
|
|
440
|
+
const authored = profile.lifecycle as unknown;
|
|
441
|
+
if (!isRecord(authored)) return profile;
|
|
442
|
+
const at = `profiles.${profile.slug}.lifecycle`;
|
|
443
|
+
const adopted = authored.adopts !== undefined;
|
|
444
|
+
const lifecycle = adopted
|
|
445
|
+
? resolveAdoption(authored, at, problems)
|
|
446
|
+
: (authored as unknown as TemplateLifecycle);
|
|
447
|
+
if (!lifecycle) return profile;
|
|
448
|
+
|
|
449
|
+
// Only a well-formed option list generates enumValues; anything else is left
|
|
450
|
+
// to the validator, which names what is wrong with it.
|
|
451
|
+
const optionKeys: unknown[] | null = Array.isArray(lifecycle.options)
|
|
452
|
+
? lifecycle.options.map((o: unknown) => (isRecord(o) ? o.key : undefined))
|
|
453
|
+
: null;
|
|
454
|
+
const keys =
|
|
455
|
+
optionKeys && optionKeys.every((k): k is string => typeof k === "string")
|
|
456
|
+
? optionKeys
|
|
457
|
+
: null;
|
|
458
|
+
const properties = profile.properties ?? [];
|
|
459
|
+
const index = properties.findIndex((p) => p.slug === lifecycle.property);
|
|
460
|
+
const prop = properties[index];
|
|
461
|
+
const fillEnum =
|
|
462
|
+
!!keys &&
|
|
463
|
+
!!prop &&
|
|
464
|
+
prop.inputType === "select" &&
|
|
465
|
+
prop.enumValues === undefined;
|
|
466
|
+
|
|
467
|
+
if (!adopted && !fillEnum) return profile;
|
|
468
|
+
const next: TemplateProfile = { ...profile, lifecycle };
|
|
469
|
+
if (fillEnum)
|
|
470
|
+
next.properties = properties.map((p, i) =>
|
|
471
|
+
i === index ? withEnumValues(p, keys) : p
|
|
472
|
+
);
|
|
473
|
+
return next;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Compile every `adopts:` lifecycle of a template into its flat declaration and
|
|
478
|
+
* generate the lifecycle select's `enumValues` where the YAML omits them. Pure;
|
|
479
|
+
* never throws. An unresolvable overlay is reported and left as authored, so
|
|
480
|
+
* `validateTemplate` refuses it.
|
|
481
|
+
*/
|
|
482
|
+
export function resolveLifecyclePresets(
|
|
483
|
+
tpl: WorkspaceYaml
|
|
484
|
+
): LifecyclePresetResolution {
|
|
485
|
+
const problems: LifecyclePresetProblem[] = [];
|
|
486
|
+
if (!tpl || !Array.isArray(tpl.profiles)) return { template: tpl, problems };
|
|
487
|
+
let changed = false;
|
|
488
|
+
const profiles = tpl.profiles.map((p) => {
|
|
489
|
+
const next = resolveProfile(p, problems);
|
|
490
|
+
if (next !== p) changed = true;
|
|
491
|
+
return next;
|
|
492
|
+
});
|
|
493
|
+
return { template: changed ? { ...tpl, profiles } : tpl, problems };
|
|
494
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `stuckAfterDays` on a catalog lifecycle option (surfaces W1.5): the
|
|
3
|
+
* validator accepts a whole number of days on an option that can still move,
|
|
4
|
+
* and refuses a bad value or a done/lost option — the same rules the pod's
|
|
5
|
+
* `parseLifecycleOptions` applies, so a template never publishes a lifecycle
|
|
6
|
+
* the pod would refuse at install.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import { validateTemplate } from "./validate.js";
|
|
10
|
+
import type { WorkspaceYaml } from "./types.js";
|
|
11
|
+
import { readYamlTemplates } from "./generate.mjs";
|
|
12
|
+
|
|
13
|
+
const crm = readYamlTemplates()
|
|
14
|
+
.map((t) => t.data as WorkspaceYaml)
|
|
15
|
+
.find((t) => t.meta.slug === "crm")!;
|
|
16
|
+
|
|
17
|
+
function withStuck(byKey: Record<string, unknown>): WorkspaceYaml {
|
|
18
|
+
const clone = structuredClone(crm);
|
|
19
|
+
const deal = clone.profiles.find((p) => p.slug === "deal")!;
|
|
20
|
+
deal.lifecycle!.options = deal.lifecycle!.options.map((o) =>
|
|
21
|
+
o.key in byKey ? { ...o, stuckAfterDays: byKey[o.key] as number } : o
|
|
22
|
+
);
|
|
23
|
+
return clone;
|
|
24
|
+
}
|
|
25
|
+
const stuckErrors = (t: WorkspaceYaml) =>
|
|
26
|
+
validateTemplate(t).errors.filter((e) => e.path.endsWith(".stuckAfterDays"));
|
|
27
|
+
|
|
28
|
+
describe("lifecycle stuckAfterDays", () => {
|
|
29
|
+
it("accepts whole days on open/active options", () => {
|
|
30
|
+
expect(stuckErrors(withStuck({ draft: 14, negotiating: 7 }))).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("refuses a non-integer, a zero, a string and a done/lost option", () => {
|
|
34
|
+
const errs = stuckErrors(
|
|
35
|
+
withStuck({ draft: 2.5, proposed: 0, negotiating: "7", won: 5 })
|
|
36
|
+
);
|
|
37
|
+
expect(errs.map((e) => e.path.match(/options\[(\d)\]/)?.[1])).toEqual([
|
|
38
|
+
"0",
|
|
39
|
+
"1",
|
|
40
|
+
"2",
|
|
41
|
+
"3",
|
|
42
|
+
]);
|
|
43
|
+
expect(errs[3]!.message).toMatch(/cannot be stuck/);
|
|
44
|
+
expect(errs.every((e) => e.rule === "lifecycle-option-shape")).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
});
|