@synap-core/workspace-templates 0.29.0 → 0.31.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 +12 -4
- package/dist/{define-DqoItRiR.d.ts → define-DHDi8C33.d.ts} +19 -18
- package/dist/index.d.ts +2 -2
- package/dist/index.js +594 -241
- package/dist/index.js.map +1 -1
- package/dist/wire.d.ts +1 -1
- package/package.json +1 -1
- package/src/brand-library.yaml +0 -6
- package/src/builder-dev-fold.template.test.ts +2 -3
- package/src/builder-dev-playbooks.template.test.ts +7 -5
- package/src/builder.yaml +162 -32
- package/src/business-model.yaml +20 -1
- package/src/civic-france.yaml +9 -0
- package/src/content-creation.yaml +1 -1
- package/src/content-os.yaml +22 -18
- package/src/crm-absorbs-outreach.template.test.ts +1 -1
- package/src/crm-build-plan.template.test.ts +1 -1
- package/src/crm.yaml +5 -14
- package/src/decisions-wave.template.test.ts +7 -5
- package/src/define.primary-surface.test.ts +9 -0
- package/src/define.ts +4 -1
- package/src/finance.yaml +10 -6
- package/src/foundation.yaml +1 -2
- package/src/hr.yaml +3 -2
- package/src/intent.test.ts +44 -3
- package/src/internal-runbook.yaml +0 -1
- package/src/kanban-grouping.test.ts +72 -0
- package/src/legal.yaml +4 -4
- package/src/lifecycle-coverage.test.ts +7 -64
- package/src/lifecycle-declarations.test.ts +70 -74
- package/src/marketing.yaml +4 -3
- package/src/meetings-knowledge.yaml +1 -2
- package/src/operations.yaml +13 -9
- package/src/process-contract.validate.test.ts +40 -55
- package/src/project-management.yaml +2 -2
- package/src/publish-official-lib.test.ts +3 -2
- package/src/research-base.yaml +22 -15
- package/src/research-decision.yaml +18 -2
- package/src/ship-a-change.template.test.ts +3 -2
- package/src/social.yaml +4 -1
- package/src/start.test.ts +22 -0
- package/src/templates.ts +490 -185
- package/src/types.ts +28 -18
- package/src/validate.ts +143 -77
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
* - the validator's category list equals `@synap-core/types/lifecycle`;
|
|
8
8
|
* - the mandatory declarations (deal, lead, client, the post that Produce
|
|
9
9
|
* Content drives) carry the founder-confirmed categories;
|
|
10
|
-
* - COVERAGE, derived: every non-system kind a playbook moves
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
10
|
+
* - COVERAGE, derived: every non-system kind a playbook moves (a stage
|
|
11
|
+
* `subjectStatus` or a worker `status`) declares a lifecycle, on every
|
|
12
|
+
* declaration that carries its property — the playbook follows it;
|
|
13
|
+
* - `humanOnly` lives ONLY on the kind's lifecycle (One model decision 4, the
|
|
14
|
+
* playbook's `humanOnlyStatuses` list is retired): the exact ledger below
|
|
15
|
+
* pins every person-only option, so changing one is a deliberate edit;
|
|
16
16
|
* - the whole corpus validates with zero `lifecycle-*` errors, and each rule
|
|
17
17
|
* fires on a broken clone (self-checks).
|
|
18
18
|
*/
|
|
@@ -72,18 +72,24 @@ describe("lifecycle declarations — the mandatory four", () => {
|
|
|
72
72
|
|
|
73
73
|
it("the post Produce Content drives declares post-status", () => {
|
|
74
74
|
const produce = tpl("content-os").playbooks!.find((p) => p.name === "Produce Content")!;
|
|
75
|
-
expect(produce.subjectProfile).
|
|
75
|
+
expect(produce.subjectProfile).toEqual({ profileSlug: "post" });
|
|
76
76
|
const lc = lifecycleOf(tpl("content-os"), "post");
|
|
77
77
|
expect(lc?.property).toBe("post-status");
|
|
78
78
|
expect(categories(lc)).toMatchObject({ Idea: "open", Published: "done", Archived: "lost" });
|
|
79
79
|
});
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
+
/** The lifecycle property a kind declares anywhere in the corpus. */
|
|
83
|
+
const lifecyclePropertyOf = (kind: string) =>
|
|
84
|
+
corpus.flatMap((t) => t.profiles).find((p) => p.slug === kind && p.lifecycle)?.lifecycle?.property;
|
|
85
|
+
|
|
82
86
|
/**
|
|
83
|
-
* Every (kind,
|
|
84
|
-
*
|
|
85
|
-
* lifecycle the
|
|
86
|
-
*
|
|
87
|
+
* Every (kind, lifecycle property, playbook) a catalog playbook moves: a stage
|
|
88
|
+
* with a `subjectStatus` or a worker with a `status`. The property is the
|
|
89
|
+
* kind's declared lifecycle (the playbook names none). `withSystem` false drops
|
|
90
|
+
* the pod's system kinds (task, decision, question…), whose lifecycle the pod
|
|
91
|
+
* seeds itself (`ensure-system-lifecycles.ts`) — except `deal`, a system kind
|
|
92
|
+
* whose lifecycle the CRM template owns.
|
|
87
93
|
*/
|
|
88
94
|
const TEMPLATE_OWNED_SYSTEM_LIFECYCLES = new Set(["deal"]);
|
|
89
95
|
function playbookBindings({ withSystem = false } = {}) {
|
|
@@ -91,12 +97,13 @@ function playbookBindings({ withSystem = false } = {}) {
|
|
|
91
97
|
(t.playbooks ?? [])
|
|
92
98
|
.filter(
|
|
93
99
|
(pb) =>
|
|
94
|
-
pb.subjectProfile?.
|
|
100
|
+
pb.subjectProfile?.profileSlug &&
|
|
101
|
+
((pb.stages ?? []).some((st) => st.subjectStatus) || (pb.activators ?? []).some((a) => a.status)) &&
|
|
95
102
|
(withSystem ||
|
|
96
103
|
!SYSTEM_PROFILES.has(pb.subjectProfile.profileSlug) ||
|
|
97
104
|
TEMPLATE_OWNED_SYSTEM_LIFECYCLES.has(pb.subjectProfile.profileSlug))
|
|
98
105
|
)
|
|
99
|
-
.map((pb) => ({ template: t, pb, kind: pb.subjectProfile!.profileSlug, property: pb.subjectProfile!.
|
|
106
|
+
.map((pb) => ({ template: t, pb, kind: pb.subjectProfile!.profileSlug, property: lifecyclePropertyOf(pb.subjectProfile!.profileSlug) }))
|
|
100
107
|
);
|
|
101
108
|
}
|
|
102
109
|
|
|
@@ -137,7 +144,9 @@ describe("lifecycle declarations — coverage (derived from the playbooks)", ()
|
|
|
137
144
|
});
|
|
138
145
|
|
|
139
146
|
it("every kind a playbook moves declares that property as its lifecycle, wherever it is declared", () => {
|
|
140
|
-
const missing: string[] =
|
|
147
|
+
const missing: string[] = playbookBindings({ withSystem: true })
|
|
148
|
+
.filter((b) => !b.property)
|
|
149
|
+
.map((b) => `${b.template.meta.slug}:${b.pb.name} moves ${b.kind}, which declares no lifecycle`);
|
|
141
150
|
for (const b of playbookBindings())
|
|
142
151
|
for (const t of corpus)
|
|
143
152
|
for (const p of t.profiles.filter((x) => x.slug === b.kind && x.properties.some((pr) => pr.slug === b.property)))
|
|
@@ -146,58 +155,56 @@ describe("lifecycle declarations — coverage (derived from the playbooks)", ()
|
|
|
146
155
|
});
|
|
147
156
|
|
|
148
157
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* - outreach-touch.sent — the agent drafts, a person sends (founder decision
|
|
157
|
-
* 2026-10-20; the CRM absorbed Outreach & Comms 2026-10-23): no agent ever
|
|
158
|
-
* marks a message sent, and the reply reader starts only once a person did.
|
|
158
|
+
* Every person-only option in the catalog, EXACT. Until 7 Nov 2026 this was
|
|
159
|
+
* derived from the binding playbooks' `humanOnlyStatuses`; that list is
|
|
160
|
+
* retired (One model decision 4), so the flags themselves are the source and
|
|
161
|
+
* this ledger makes any change to one a deliberate, reviewed edit. Founder
|
|
162
|
+
* decisions among them: ui-design.approved (design review, DESIGN-DEV-XP-PLAN
|
|
163
|
+
* P6, 2026-10-12), outreach-touch.sent (a person sends, 2026-10-20),
|
|
164
|
+
* devplane_feature.cancelled / devplane_story.dropped (homes, 2026-10-13).
|
|
159
165
|
*/
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
* yet (so no `humanOnlyStatuses` can carry them). Each one a dated decision;
|
|
165
|
-
* EXACT, like the ledger above.
|
|
166
|
-
* - devplane_feature.cancelled / devplane_story.dropped — "Dropped (lost,
|
|
167
|
-
* humanOnly)", object-first homes decision 2026-10-13.
|
|
168
|
-
*/
|
|
169
|
-
const FOUNDER_HUMAN_ONLY = new Set([
|
|
166
|
+
const HUMAN_ONLY = [
|
|
167
|
+
"brand-library:brand-asset.deprecated",
|
|
168
|
+
"brand-library:brand-voice-guide.deprecated",
|
|
169
|
+
"brand-library:content-guideline.deprecated",
|
|
170
170
|
"builder-workspace:devplane_feature.cancelled",
|
|
171
171
|
"builder-workspace:devplane_story.dropped",
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
172
|
+
"builder-workspace:finding.superseded",
|
|
173
|
+
"builder-workspace:finding.wont_fix",
|
|
174
|
+
"builder-workspace:issue.wont_fix",
|
|
175
|
+
"builder-workspace:ui-design.approved",
|
|
176
|
+
"builder-workspace:ui-design.archived",
|
|
177
|
+
"content-os:campaign.cancelled",
|
|
178
|
+
"content-os:campaign.paused",
|
|
179
|
+
"content-os:newsletter.Archived",
|
|
180
|
+
"content-os:post.Archived",
|
|
181
|
+
"crm:campaign.cancelled",
|
|
182
|
+
"crm:campaign.paused",
|
|
183
|
+
"crm:deal.lost",
|
|
184
|
+
"crm:outreach-touch.replaced",
|
|
185
|
+
"crm:outreach-touch.sent",
|
|
186
|
+
"finance:expense.reimbursed",
|
|
187
|
+
"finance:expense.rejected",
|
|
188
|
+
"finance:invoice.void",
|
|
189
|
+
"foundation:lead.converted",
|
|
190
|
+
"foundation:lead.disqualified",
|
|
191
|
+
"marketing-campaign:campaign.cancelled",
|
|
192
|
+
"marketing-campaign:campaign.paused",
|
|
193
|
+
"operations:contract.expired",
|
|
194
|
+
"operations:contract.terminated",
|
|
195
|
+
"operations:deliverable.cancelled",
|
|
196
|
+
"operations:engagement.cancelled",
|
|
197
|
+
"operations:engagement.on-hold",
|
|
198
|
+
"social:post.Archived",
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
it("humanOnly sits on the kind's lifecycle, exactly the ledger", () => {
|
|
202
|
+
const seen = corpus.flatMap((t) =>
|
|
203
|
+
t.profiles.flatMap((p) =>
|
|
204
|
+
(p.lifecycle?.options ?? []).filter((o) => o.humanOnly).map((o) => `${t.meta.slug}:${p.slug}.${o.key}`)
|
|
205
|
+
)
|
|
206
|
+
);
|
|
207
|
+
expect([...new Set(seen)].sort()).toEqual(HUMAN_ONLY);
|
|
201
208
|
});
|
|
202
209
|
});
|
|
203
210
|
|
|
@@ -238,15 +245,4 @@ describe("lifecycle declarations — the validator", () => {
|
|
|
238
245
|
delete campaignIn(c2, "crm").lifecycle;
|
|
239
246
|
expect(lifecycleErrors(crmIn(c2), c2).map((e) => e.rule)).toContain("lifecycle-diverges");
|
|
240
247
|
});
|
|
241
|
-
|
|
242
|
-
it("self-check: a playbook moving another property than the lifecycle is refused", () => {
|
|
243
|
-
const c = clone();
|
|
244
|
-
const produce = c.find((t) => t.meta.slug === "content-os")!.playbooks!.find((p) => p.name === "Produce Content")!;
|
|
245
|
-
const post = c.find((t) => t.meta.slug === "content-os")!.profiles.find((p) => p.slug === "post")!;
|
|
246
|
-
post.lifecycle!.property = "platform";
|
|
247
|
-
produce.subjectProfile!.statusProperty = "post-status";
|
|
248
|
-
expect(lifecycleErrors(c.find((t) => t.meta.slug === "content-os")!, c).map((e) => e.rule)).toContain(
|
|
249
|
-
"lifecycle-status-property"
|
|
250
|
-
);
|
|
251
|
-
});
|
|
252
248
|
});
|
package/src/marketing.yaml
CHANGED
|
@@ -13,6 +13,8 @@ meta:
|
|
|
13
13
|
# What this space PRODUCES (homes): the purpose word, then its kinds/roles.
|
|
14
14
|
intent:
|
|
15
15
|
label: Marketing
|
|
16
|
+
# The ONE primary output (One model decision 3): a run campaign (Run a campaign).
|
|
17
|
+
primary: campaign
|
|
16
18
|
produces:
|
|
17
19
|
- profile: campaign
|
|
18
20
|
|
|
@@ -261,7 +263,8 @@ views:
|
|
|
261
263
|
slug: campaigns
|
|
262
264
|
type: kanban
|
|
263
265
|
scopeProfileSlug: campaign
|
|
264
|
-
|
|
266
|
+
config:
|
|
267
|
+
groupByField: campaign-status
|
|
265
268
|
description: "Campaign kanban board by status."
|
|
266
269
|
- name: Contacts
|
|
267
270
|
slug: contacts
|
|
@@ -399,8 +402,6 @@ playbooks:
|
|
|
399
402
|
person's call.
|
|
400
403
|
subjectProfile:
|
|
401
404
|
profileSlug: campaign
|
|
402
|
-
statusProperty: campaign-status
|
|
403
|
-
humanOnlyStatuses: [paused, cancelled]
|
|
404
405
|
activators:
|
|
405
406
|
- { on: created, mode: propose }
|
|
406
407
|
params:
|
|
@@ -172,10 +172,9 @@ views:
|
|
|
172
172
|
slug: meeting-action-items
|
|
173
173
|
type: kanban
|
|
174
174
|
scopeProfileSlug: task
|
|
175
|
-
groupBy: status
|
|
176
175
|
description: Tasks generated from meetings, by status.
|
|
177
176
|
config:
|
|
178
|
-
|
|
177
|
+
groupByField: status
|
|
179
178
|
|
|
180
179
|
entityLinks:
|
|
181
180
|
- sourceProfileSlug: meeting
|
package/src/operations.yaml
CHANGED
|
@@ -18,7 +18,6 @@ meta:
|
|
|
18
18
|
label: Client delivery
|
|
19
19
|
promise: Every client engagement and where it stands.
|
|
20
20
|
suggestFor: [consultant]
|
|
21
|
-
profile: engagement
|
|
22
21
|
views: [engagements]
|
|
23
22
|
example:
|
|
24
23
|
- { title: "Website redesign for Maison Dupont", values: { engagement-status: active, engagement-value: 18000 } }
|
|
@@ -35,6 +34,8 @@ meta:
|
|
|
35
34
|
# What this space PRODUCES (homes): the purpose word, then its kinds/roles.
|
|
36
35
|
intent:
|
|
37
36
|
label: Delivery
|
|
37
|
+
# The ONE primary output (One model decision 3): a delivered engagement (Deliver a client engagement).
|
|
38
|
+
primary: engagement
|
|
38
39
|
produces:
|
|
39
40
|
- profile: engagement
|
|
40
41
|
- profile: deliverable
|
|
@@ -304,19 +305,28 @@ views:
|
|
|
304
305
|
slug: engagements
|
|
305
306
|
type: kanban
|
|
306
307
|
scopeProfileSlug: engagement
|
|
307
|
-
|
|
308
|
+
config:
|
|
309
|
+
groupByField: engagement-status
|
|
308
310
|
description: "Client engagements on a board by status."
|
|
309
311
|
- name: Deliverables
|
|
310
312
|
slug: deliverables
|
|
311
313
|
type: kanban
|
|
312
314
|
scopeProfileSlug: deliverable
|
|
313
|
-
|
|
315
|
+
config:
|
|
316
|
+
groupByField: deliverable-status
|
|
314
317
|
description: "Deliverables on a board by status."
|
|
315
318
|
- name: Contracts
|
|
316
319
|
slug: contracts
|
|
317
320
|
type: table
|
|
318
321
|
scopeProfileSlug: contract
|
|
319
322
|
description: "Contracts with their status, value, and term."
|
|
323
|
+
- name: Contract Board
|
|
324
|
+
slug: contract-board
|
|
325
|
+
type: kanban
|
|
326
|
+
scopeProfileSlug: contract
|
|
327
|
+
description: "Contracts by status — the flow board of Get a contract signed."
|
|
328
|
+
config:
|
|
329
|
+
groupByField: contract-status
|
|
320
330
|
- name: Client Briefings
|
|
321
331
|
slug: client-briefings
|
|
322
332
|
type: list
|
|
@@ -938,8 +948,6 @@ playbooks:
|
|
|
938
948
|
call.
|
|
939
949
|
subjectProfile:
|
|
940
950
|
profileSlug: engagement
|
|
941
|
-
statusProperty: engagement-status
|
|
942
|
-
humanOnlyStatuses: [on-hold, cancelled]
|
|
943
951
|
activators:
|
|
944
952
|
- { on: created, mode: propose }
|
|
945
953
|
params:
|
|
@@ -1018,8 +1026,6 @@ playbooks:
|
|
|
1018
1026
|
close bills it.
|
|
1019
1027
|
subjectProfile:
|
|
1020
1028
|
profileSlug: deliverable
|
|
1021
|
-
statusProperty: deliverable-status
|
|
1022
|
-
humanOnlyStatuses: [not-started, cancelled]
|
|
1023
1029
|
activators:
|
|
1024
1030
|
- { on: enters_status, status: in-progress, mode: propose }
|
|
1025
1031
|
params:
|
|
@@ -1095,8 +1101,6 @@ playbooks:
|
|
|
1095
1101
|
document. Expiry and termination are a person's call.
|
|
1096
1102
|
subjectProfile:
|
|
1097
1103
|
profileSlug: contract
|
|
1098
|
-
statusProperty: contract-status
|
|
1099
|
-
humanOnlyStatuses: [expired, terminated]
|
|
1100
1104
|
activators:
|
|
1101
1105
|
- { on: created, mode: propose }
|
|
1102
1106
|
params:
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* The process contract a playbook declares is checked at authoring time
|
|
3
3
|
* (`checkProcessContract` in validate.ts), because every one of these mistakes
|
|
4
4
|
* is STORED by the pod and then silently never fires:
|
|
5
|
-
* - a `statusProperty`
|
|
6
|
-
*
|
|
7
|
-
* - a `subjectStatus` / activator `status`
|
|
8
|
-
*
|
|
9
|
-
* value no entity ever enters;
|
|
5
|
+
* - a `statusProperty` / `onEnd` / `humanOnlyStatuses` → retired (One model
|
|
6
|
+
* decision 4): the process follows the kind's declared lifecycle;
|
|
7
|
+
* - a `subjectStatus` / activator `status` that is not an option of the
|
|
8
|
+
* kind's declared lifecycle (or a kind with none) → a write the select
|
|
9
|
+
* refuses, or a rule on a value no entity ever enters;
|
|
10
10
|
* - an `onFail.toStage` naming no stage → a rejected gate with nowhere to go;
|
|
11
11
|
* - a `relationToSubject` naming no relation → an edge type nothing renders.
|
|
12
12
|
*
|
|
@@ -24,21 +24,17 @@ import type { WorkspaceYaml } from "./types.js";
|
|
|
24
24
|
import { readYamlTemplates } from "./generate.mjs";
|
|
25
25
|
|
|
26
26
|
const PROCESS_RULES = new Set([
|
|
27
|
-
"playbook-
|
|
28
|
-
"playbook-status-property-unknown",
|
|
29
|
-
"playbook-status-property-not-select",
|
|
27
|
+
"playbook-field-retired",
|
|
30
28
|
"stage-subject-status-unknown",
|
|
31
|
-
"stage-subject-status-unknown-no-
|
|
29
|
+
"stage-subject-status-unknown-no-lifecycle",
|
|
32
30
|
"stage-onfail-unknown",
|
|
33
31
|
"activator-on-invalid",
|
|
34
32
|
"activator-mode-invalid",
|
|
35
33
|
"activator-no-subject",
|
|
36
34
|
"activator-status-required",
|
|
37
35
|
"activator-status-unknown",
|
|
38
|
-
"activator-status-unknown-no-
|
|
36
|
+
"activator-status-unknown-no-lifecycle",
|
|
39
37
|
"activator-status-unexpected",
|
|
40
|
-
"human-only-status-unknown",
|
|
41
|
-
"human-only-status-unknown-no-property",
|
|
42
38
|
"output-relation-no-subject",
|
|
43
39
|
"output-relation-unknown",
|
|
44
40
|
"output-optional-not-boolean",
|
|
@@ -46,10 +42,19 @@ const PROCESS_RULES = new Set([
|
|
|
46
42
|
]);
|
|
47
43
|
|
|
48
44
|
/**
|
|
49
|
-
* `owner` OWNS the shared kind `widget` (with its lifecycle
|
|
45
|
+
* `owner` OWNS the shared kind `widget` (with its declared lifecycle). `proc`
|
|
50
46
|
* requires it and only re-declares `widget` with no properties — so resolving
|
|
51
|
-
* `widget-stage` proves the
|
|
47
|
+
* `widget-stage` proves the lifecycle is found ACROSS the corpus.
|
|
52
48
|
*/
|
|
49
|
+
const WIDGET_LIFECYCLE = {
|
|
50
|
+
property: "widget-stage",
|
|
51
|
+
options: [
|
|
52
|
+
{ key: "New", category: "open" },
|
|
53
|
+
{ key: "Built", category: "active" },
|
|
54
|
+
{ key: "Shipped", category: "done" },
|
|
55
|
+
{ key: "Archived", category: "lost", humanOnly: true },
|
|
56
|
+
],
|
|
57
|
+
};
|
|
53
58
|
const owner = {
|
|
54
59
|
meta: { slug: "owner", name: "Owner", description: "d", icon: "x", color: "gray" },
|
|
55
60
|
workspace: { name: "Owner", description: "d" },
|
|
@@ -58,6 +63,7 @@ const owner = {
|
|
|
58
63
|
slug: "widget",
|
|
59
64
|
displayName: "Widget",
|
|
60
65
|
scope: "shared",
|
|
66
|
+
lifecycle: WIDGET_LIFECYCLE,
|
|
61
67
|
properties: [
|
|
62
68
|
{ slug: "widget-stage", label: "Stage", valueType: "string", inputType: "select", enumValues: ["New", "Built", "Shipped", "Archived"] },
|
|
63
69
|
{ slug: "widget-notes", label: "Notes", valueType: "string", inputType: "text" },
|
|
@@ -72,8 +78,7 @@ function proc(mutate: (pb: Record<string, any>) => void = () => {}): WorkspaceYa
|
|
|
72
78
|
name: "Build Widget",
|
|
73
79
|
goalTemplate: "Build {widget}",
|
|
74
80
|
params: [{ name: "widget", type: "entity", required: true }],
|
|
75
|
-
subjectProfile: { profileSlug: "widget"
|
|
76
|
-
humanOnlyStatuses: ["New", "Archived"],
|
|
81
|
+
subjectProfile: { profileSlug: "widget" },
|
|
77
82
|
activators: [{ on: "enters_status", status: "Built", mode: "propose" }],
|
|
78
83
|
expectedOutputs: [{ kind: "widget", label: "Spin-off", relationToSubject: "spawned_from" }],
|
|
79
84
|
stages: [
|
|
@@ -112,26 +117,16 @@ describe("process contract — the clean fixture is clean", () => {
|
|
|
112
117
|
});
|
|
113
118
|
|
|
114
119
|
describe("process contract — each rule fires on its own fixture", () => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
// Negative control: each assertion goes red with the `playbook-field-retired`
|
|
121
|
+
// loop removed from checkProcessContract.
|
|
122
|
+
it("each retired field is refused (statusProperty, onEnd, humanOnlyStatuses)", () => {
|
|
123
|
+
expect(rulesOf(proc((pb) => (pb.subjectProfile.statusProperty = "widget-stage")))).toEqual([
|
|
124
|
+
"playbook-field-retired",
|
|
118
125
|
]);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
expect(
|
|
123
|
-
rulesOf(
|
|
124
|
-
proc((pb) => {
|
|
125
|
-
pb.subjectProfile.statusProperty = "widget-notes";
|
|
126
|
-
// Without a resolvable vocabulary the statuses are not re-reported.
|
|
127
|
-
})
|
|
128
|
-
)
|
|
129
|
-
).toEqual(["playbook-status-property-not-select"]);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("statusProperty with no subject kind", () => {
|
|
133
|
-
const rules = rulesOf(proc((pb) => (pb.subjectProfile = { statusProperty: "widget-stage" })));
|
|
134
|
-
expect(rules).toContain("playbook-status-property-no-subject");
|
|
126
|
+
expect(rulesOf(proc((pb) => (pb.subjectProfile.onEnd = { cancelled: "New" })))).toEqual([
|
|
127
|
+
"playbook-field-retired",
|
|
128
|
+
]);
|
|
129
|
+
expect(rulesOf(proc((pb) => (pb.humanOnlyStatuses = ["New"])))).toEqual(["playbook-field-retired"]);
|
|
135
130
|
});
|
|
136
131
|
|
|
137
132
|
it("a stage subjectStatus outside the enum", () => {
|
|
@@ -140,17 +135,14 @@ describe("process contract — each rule fires on its own fixture", () => {
|
|
|
140
135
|
]);
|
|
141
136
|
});
|
|
142
137
|
|
|
143
|
-
it("a stage subjectStatus
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
expect(rules).toEqual([
|
|
152
|
-
"stage-subject-status-unknown-no-property",
|
|
153
|
-
"stage-subject-status-unknown-no-property",
|
|
138
|
+
it("a stage subjectStatus on a kind that declares no lifecycle", () => {
|
|
139
|
+
const noLifecycle = structuredClone(owner) as unknown as { profiles: Array<Record<string, unknown>> };
|
|
140
|
+
delete noLifecycle.profiles[0]!.lifecycle;
|
|
141
|
+
const tpl = proc((pb) => (pb.activators = [{ on: "created", mode: "propose" }]));
|
|
142
|
+
const { errors } = validateTemplate(tpl, { corpus: [noLifecycle as unknown as WorkspaceYaml, tpl] });
|
|
143
|
+
expect(errors.filter((e) => PROCESS_RULES.has(e.rule)).map((e) => e.rule)).toEqual([
|
|
144
|
+
"stage-subject-status-unknown-no-lifecycle",
|
|
145
|
+
"stage-subject-status-unknown-no-lifecycle",
|
|
154
146
|
]);
|
|
155
147
|
});
|
|
156
148
|
|
|
@@ -218,7 +210,6 @@ describe("process contract — each rule fires on its own fixture", () => {
|
|
|
218
210
|
proc((pb) => {
|
|
219
211
|
delete pb.subjectProfile;
|
|
220
212
|
delete pb.stages;
|
|
221
|
-
delete pb.humanOnlyStatuses;
|
|
222
213
|
delete pb.expectedOutputs;
|
|
223
214
|
pb.activators = [{ on: "created", mode: "propose" }];
|
|
224
215
|
})
|
|
@@ -226,12 +217,6 @@ describe("process contract — each rule fires on its own fixture", () => {
|
|
|
226
217
|
expect(rules).toEqual(["activator-no-subject"]);
|
|
227
218
|
});
|
|
228
219
|
|
|
229
|
-
it("a humanOnlyStatuses value outside the enum", () => {
|
|
230
|
-
expect(rulesOf(proc((pb) => (pb.humanOnlyStatuses = ["New", "Retired"])))).toEqual([
|
|
231
|
-
"human-only-status-unknown",
|
|
232
|
-
]);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
220
|
it("a playbook-level relationToSubject the corpus does not define", () => {
|
|
236
221
|
expect(rulesOf(proc((pb) => (pb.expectedOutputs[0].relationToSubject = "spawned_by")))).toEqual([
|
|
237
222
|
"output-relation-unknown",
|
|
@@ -277,7 +262,6 @@ describe("process contract — each rule fires on its own fixture", () => {
|
|
|
277
262
|
const strip = (pb: Record<string, any>) => {
|
|
278
263
|
delete pb.subjectProfile;
|
|
279
264
|
delete pb.stages;
|
|
280
|
-
delete pb.humanOnlyStatuses;
|
|
281
265
|
delete pb.activators;
|
|
282
266
|
};
|
|
283
267
|
expect(rulesOf(proc(strip))).toEqual(["output-relation-no-subject"]);
|
|
@@ -340,7 +324,8 @@ describe("process contract — the shipped corpus", () => {
|
|
|
340
324
|
const corpus = readYamlTemplates().map((t) => t.data as WorkspaceYaml);
|
|
341
325
|
const contentOs = corpus.find((t) => t.meta.slug === "content-os")!;
|
|
342
326
|
const produce = contentOs.playbooks!.find((p) => p.name === "Produce Content")!;
|
|
343
|
-
expect(produce.subjectProfile
|
|
327
|
+
expect(produce.subjectProfile).toEqual({ profileSlug: "post" });
|
|
328
|
+
expect(contentOs.profiles.find((p) => p.slug === "post")?.lifecycle?.property).toBe("post-status");
|
|
344
329
|
expect(produce.stages!.filter((s) => s.subjectStatus).length).toBe(6);
|
|
345
330
|
const errs = corpus.flatMap((t) =>
|
|
346
331
|
validateTemplate(t, { corpus })
|
|
@@ -17,6 +17,8 @@ meta:
|
|
|
17
17
|
# What this space PRODUCES (homes): the purpose word, then its kinds/roles.
|
|
18
18
|
intent:
|
|
19
19
|
label: Planning
|
|
20
|
+
# The ONE primary output (One model decision 3): a finished sprint, its one produced kind.
|
|
21
|
+
primary: sprint
|
|
20
22
|
produces:
|
|
21
23
|
- profile: sprint
|
|
22
24
|
|
|
@@ -192,7 +194,6 @@ views:
|
|
|
192
194
|
slug: sprint-board
|
|
193
195
|
type: kanban
|
|
194
196
|
scopeProfileSlug: task
|
|
195
|
-
groupBy: task-status
|
|
196
197
|
description: "Active sprint tasks in Backlog / Todo / In Progress / Review / Done lanes"
|
|
197
198
|
config:
|
|
198
199
|
groupByField: task-status
|
|
@@ -276,7 +277,6 @@ views:
|
|
|
276
277
|
slug: retrospective-board
|
|
277
278
|
type: kanban
|
|
278
279
|
scopeProfileSlug: sprint
|
|
279
|
-
groupBy: sprint-status
|
|
280
280
|
description: "Sprint retrospectives — completed sprints with retrospective notes attached as documents"
|
|
281
281
|
config:
|
|
282
282
|
groupByField: sprint-status
|
|
@@ -77,7 +77,7 @@ describe("plan → execute — the wire", () => {
|
|
|
77
77
|
.playbooks as any[];
|
|
78
78
|
const produce = yamlPlaybooks.find((p) => p.name === "Produce Content");
|
|
79
79
|
// Non-vacuity: the YAML really declares the contract fields compared below.
|
|
80
|
-
expect(produce.subjectProfile
|
|
80
|
+
expect(produce.subjectProfile).toEqual({ profileSlug: "post" });
|
|
81
81
|
expect(produce.activators).toEqual([{ on: "enters_status", status: "Briefed", mode: "propose" }]);
|
|
82
82
|
expect(produce.stages.find((s: any) => s.key === "review").onFail).toEqual({ toStage: "draft" });
|
|
83
83
|
expect(
|
|
@@ -98,7 +98,8 @@ describe("plan → execute — the wire", () => {
|
|
|
98
98
|
"Scheduled",
|
|
99
99
|
"Published",
|
|
100
100
|
]);
|
|
101
|
-
|
|
101
|
+
// Retired from the catalog (One model decision 4): nothing to carry.
|
|
102
|
+
expect(postedProduce.humanOnlyStatuses).toBeUndefined();
|
|
102
103
|
});
|
|
103
104
|
|
|
104
105
|
it("reports a stamp the CP did not store", async () => {
|
package/src/research-base.yaml
CHANGED
|
@@ -29,9 +29,11 @@ meta:
|
|
|
29
29
|
# What this space PRODUCES (homes): the purpose word, then its kinds/roles.
|
|
30
30
|
intent:
|
|
31
31
|
label: Research
|
|
32
|
+
# The ONE primary output (One model decision 3): a finding is what research yields (questions are its input).
|
|
33
|
+
primary: research-finding
|
|
32
34
|
produces:
|
|
33
35
|
- profile: hypothesis
|
|
34
|
-
- profile: finding
|
|
36
|
+
- profile: research-finding
|
|
35
37
|
|
|
36
38
|
workspace:
|
|
37
39
|
name: Research
|
|
@@ -60,6 +62,15 @@ profiles:
|
|
|
60
62
|
icon: help-circle
|
|
61
63
|
color: blue
|
|
62
64
|
description: An open question driving an investigation — the thing research answers.
|
|
65
|
+
# The pod seeds this lifecycle on every pod (ensure-system-lifecycles.ts);
|
|
66
|
+
# declared here to match the seed exactly, so a playbook can follow it.
|
|
67
|
+
lifecycle:
|
|
68
|
+
property: questionStatus
|
|
69
|
+
options:
|
|
70
|
+
- { key: open, category: open }
|
|
71
|
+
- { key: exploring, category: active }
|
|
72
|
+
- { key: answered, category: done }
|
|
73
|
+
- { key: abandoned, category: lost }
|
|
63
74
|
properties:
|
|
64
75
|
- slug: questionStatus
|
|
65
76
|
label: Status
|
|
@@ -214,7 +225,7 @@ profiles:
|
|
|
214
225
|
valueType: string
|
|
215
226
|
inputType: textarea
|
|
216
227
|
|
|
217
|
-
- slug: finding
|
|
228
|
+
- slug: research-finding
|
|
218
229
|
displayName: Finding
|
|
219
230
|
icon: check-circle
|
|
220
231
|
color: "#10B981"
|
|
@@ -253,7 +264,7 @@ views:
|
|
|
253
264
|
- name: Knowledge Map
|
|
254
265
|
slug: research-knowledge-map
|
|
255
266
|
type: graph
|
|
256
|
-
scopeProfileSlugs: [field, concept, finding]
|
|
267
|
+
scopeProfileSlugs: [field, concept, research-finding]
|
|
257
268
|
description: The cartography — fields and concepts, their overlaps and prerequisites, and the findings attached to them.
|
|
258
269
|
defaultView: true
|
|
259
270
|
config:
|
|
@@ -280,14 +291,13 @@ views:
|
|
|
280
291
|
slug: research-hypotheses
|
|
281
292
|
type: kanban
|
|
282
293
|
scopeProfileSlug: hypothesis
|
|
283
|
-
groupBy: stance
|
|
284
294
|
description: Candidate theories, grouped by where the evidence has left them.
|
|
285
295
|
config:
|
|
286
|
-
|
|
296
|
+
groupByField: stance
|
|
287
297
|
- name: Findings
|
|
288
298
|
slug: research-findings
|
|
289
299
|
type: table
|
|
290
|
-
scopeProfileSlug: finding
|
|
300
|
+
scopeProfileSlug: research-finding
|
|
291
301
|
description: What's been established, with its confidence.
|
|
292
302
|
config:
|
|
293
303
|
columns: [title, confidence, status]
|
|
@@ -349,7 +359,7 @@ entityLinks:
|
|
|
349
359
|
}
|
|
350
360
|
- {
|
|
351
361
|
sourceProfileSlug: research,
|
|
352
|
-
targetProfileSlug: finding,
|
|
362
|
+
targetProfileSlug: research-finding,
|
|
353
363
|
type: concluded,
|
|
354
364
|
label: Concluded,
|
|
355
365
|
}
|
|
@@ -373,19 +383,19 @@ entityLinks:
|
|
|
373
383
|
label: Cites,
|
|
374
384
|
}
|
|
375
385
|
- {
|
|
376
|
-
sourceProfileSlug: finding,
|
|
386
|
+
sourceProfileSlug: research-finding,
|
|
377
387
|
targetProfileSlug: research,
|
|
378
388
|
type: derived_from,
|
|
379
389
|
label: Derived from,
|
|
380
390
|
}
|
|
381
391
|
- {
|
|
382
|
-
sourceProfileSlug: finding,
|
|
392
|
+
sourceProfileSlug: research-finding,
|
|
383
393
|
targetProfileSlug: source,
|
|
384
394
|
type: supported_by,
|
|
385
395
|
label: Supported by,
|
|
386
396
|
}
|
|
387
397
|
- {
|
|
388
|
-
sourceProfileSlug: finding,
|
|
398
|
+
sourceProfileSlug: research-finding,
|
|
389
399
|
targetProfileSlug: concept,
|
|
390
400
|
type: about,
|
|
391
401
|
label: About,
|
|
@@ -410,7 +420,7 @@ playbooks:
|
|
|
410
420
|
rejected with a reason (never silently drop a source). WEIGH: form the
|
|
411
421
|
candidate answers as `hypothesis` entities that answer the question; link
|
|
412
422
|
every source that bears on a hypothesis with `supports` or
|
|
413
|
-
`contradicts`; use the evidence-synthesis skill to emit `finding`
|
|
423
|
+
`contradicts`; use the evidence-synthesis skill to emit `research-finding`
|
|
414
424
|
entities with honestly calibrated confidence, each `supported_by` its
|
|
415
425
|
sources and `derived_from` this research; use the concept-cartography
|
|
416
426
|
skill to place the findings onto the knowledge map. ANSWER: create the
|
|
@@ -420,8 +430,6 @@ playbooks:
|
|
|
420
430
|
proposal-gated and reviewable.
|
|
421
431
|
subjectProfile:
|
|
422
432
|
profileSlug: question
|
|
423
|
-
statusProperty: questionStatus
|
|
424
|
-
humanOnlyStatuses: [abandoned]
|
|
425
433
|
activators:
|
|
426
434
|
- { on: created, mode: propose }
|
|
427
435
|
params:
|
|
@@ -486,8 +494,7 @@ playbooks:
|
|
|
486
494
|
and link each one derived_from the finding. Quote only what the sources
|
|
487
495
|
support.
|
|
488
496
|
subjectProfile:
|
|
489
|
-
profileSlug: finding
|
|
490
|
-
statusProperty: status
|
|
497
|
+
profileSlug: research-finding
|
|
491
498
|
activators:
|
|
492
499
|
- { on: enters_status, status: validated, mode: propose }
|
|
493
500
|
params:
|