@synap-core/workspace-templates 0.10.0 → 0.12.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/dist/index.d.ts +74 -34
- package/dist/index.js +1177 -11
- package/dist/index.js.map +1 -1
- package/package.json +16 -25
- package/src/base.template.test.ts +25 -19
- package/src/base.yaml +12 -2
- package/src/blockchain-ecosystem.yaml +106 -0
- package/src/builder.yaml +19 -1
- package/src/business-model.yaml +351 -0
- package/src/catalog.ts +55 -0
- package/src/civic-france.yaml +102 -0
- package/src/dist-freshness.test.ts +97 -0
- package/src/foundation.yaml +62 -0
- package/src/grants.yaml +259 -1
- package/src/life-os.yaml +14 -0
- package/src/networking.yaml +14 -3
- package/src/seed-obligation.test.ts +181 -0
- package/src/shared-role-ssot.test.ts +4 -1
- package/src/templates.ts +1132 -15
- package/src/the-arch.yaml +4 -0
- package/src/types.ts +32 -34
- package/src/validate-flow.ts +11 -1
- package/src/validate.test.ts +86 -0
- package/src/validate.ts +49 -0
- package/src/__fixtures__/report-automation-flow.json +0 -393
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# Business Model workspace template
|
|
2
|
+
#
|
|
3
|
+
# WHY THIS TEMPLATE EXISTS (read before adding another strategy pack):
|
|
4
|
+
# The nine-component business-model interrogation (porteur, offer, market,
|
|
5
|
+
# money, means, and the conventions binding them) maps almost entirely onto
|
|
6
|
+
# vocabulary that ALREADY EXISTS in this corpus — `mission`/`vision`/`value`
|
|
7
|
+
# (foundation), `audience`/`pain-point`/`positioning`/`offer` (foundation,
|
|
8
|
+
# shared + pod-scoped), `client`/`partner`/`lead` (foundation shared roles),
|
|
9
|
+
# and the whole IBIS apparatus of `question`/`hypothesis`/`decision`/`source`
|
|
10
|
+
# (research-base + research-decision). Minting a parallel `business-model-*`
|
|
11
|
+
# vocabulary would have been a vocabulary fork — the exact failure
|
|
12
|
+
# `concept-guard.ts` was written to catch.
|
|
13
|
+
#
|
|
14
|
+
# So this pack adds only what is genuinely MISSING, which is three things:
|
|
15
|
+
# 1. `convention` — the binding external rule the model rests on (an
|
|
16
|
+
# eligibility threshold, a deontological duty, a contractual floor). Not
|
|
17
|
+
# a decision (nobody chose it) and not a finding (it is not evidence, it
|
|
18
|
+
# is a constraint). Nothing in the corpus expressed this.
|
|
19
|
+
# 2. `projection` — a quantified scenario with its assumptions attached, so
|
|
20
|
+
# a number can be challenged by challenging what it assumed.
|
|
21
|
+
# 3. `rests_on` / `refutes` — the two edges that make a business model a
|
|
22
|
+
# GRAPH instead of a document. `rests_on` is deliberately NOT `depends_on`
|
|
23
|
+
# or `blocks`: those are acyclic scheduling edges, and a business model is
|
|
24
|
+
# legitimately circular (the offer rests on the means, the means rest on
|
|
25
|
+
# the money, the money rests on the offer). `rests_on` carries support and
|
|
26
|
+
# staleness, not sequencing, so a cycle is information rather than a bug.
|
|
27
|
+
#
|
|
28
|
+
# The porteur and funder ROLES it needs are declared in foundation.yaml, the
|
|
29
|
+
# shared-role SSOT — see the note there.
|
|
30
|
+
|
|
31
|
+
meta:
|
|
32
|
+
slug: business-model
|
|
33
|
+
domain: strategy
|
|
34
|
+
name: Business Model
|
|
35
|
+
description: Interrogate a venture as a model that rests on things — components supported by evidence, bound by real conventions, and quantified by projections whose assumptions stay visible.
|
|
36
|
+
icon: layout-grid
|
|
37
|
+
color: "#0EA5E9"
|
|
38
|
+
tags: [strategy, business-model, grp, ibis, viability]
|
|
39
|
+
isPublic: true
|
|
40
|
+
|
|
41
|
+
workspace:
|
|
42
|
+
name: Foundation
|
|
43
|
+
description: Adds business-model interrogation to the strategic substrate — what the model rests on, which conventions bind it, and which numbers assume what.
|
|
44
|
+
subtype: foundation
|
|
45
|
+
|
|
46
|
+
# ONE compose base only — `validate.ts` requires every compose dependency to
|
|
47
|
+
# equal `workspace.subtype`, which is correct: an overlay is layered into one
|
|
48
|
+
# workspace, and a thing cannot be layered into two. The business model belongs
|
|
49
|
+
# in Foundation, because its components ARE the strategic DNA. The evidence
|
|
50
|
+
# apparatus it borrows (hypothesis, source, finding) lives in the Research
|
|
51
|
+
# workspace and is reached by RELATION, not by merging two lenses into one.
|
|
52
|
+
dependencies:
|
|
53
|
+
- slug: foundation
|
|
54
|
+
relation: compose
|
|
55
|
+
reason: The model's components ARE the foundation vocabulary — mission, audience, pain-point, positioning, offer, and the client/partner/porteur/funder roles. This pack interrogates them; it does not restate them.
|
|
56
|
+
|
|
57
|
+
profiles:
|
|
58
|
+
# ── Re-declared from dependencies, verbatim-compatible ─────────────────────
|
|
59
|
+
# Re-declaring a composed profile is how a pack states what it operates on;
|
|
60
|
+
# the apply layer resolves shared slugs to the ONE pod-wide row. Shapes here
|
|
61
|
+
# are strict subsets of the owner's shape, so no overlay conflict arises.
|
|
62
|
+
- slug: question
|
|
63
|
+
displayName: Question
|
|
64
|
+
icon: help-circle
|
|
65
|
+
profileKind: kind
|
|
66
|
+
properties:
|
|
67
|
+
- slug: questionStatus
|
|
68
|
+
label: Status
|
|
69
|
+
valueType: string
|
|
70
|
+
inputType: select
|
|
71
|
+
enumValues: [open, exploring, answered, abandoned]
|
|
72
|
+
- slug: decision
|
|
73
|
+
displayName: Decision
|
|
74
|
+
icon: gavel
|
|
75
|
+
profileKind: kind
|
|
76
|
+
properties:
|
|
77
|
+
- slug: decisionStatus
|
|
78
|
+
label: Status
|
|
79
|
+
valueType: string
|
|
80
|
+
inputType: select
|
|
81
|
+
enumValues: [proposed, accepted, superseded, rejected]
|
|
82
|
+
|
|
83
|
+
# ── What this pack actually adds ───────────────────────────────────────────
|
|
84
|
+
- slug: convention
|
|
85
|
+
displayName: Convention
|
|
86
|
+
icon: scale
|
|
87
|
+
color: rose
|
|
88
|
+
description: A binding rule the model must satisfy and did not choose — an eligibility threshold, a professional duty, a contractual floor, a platform policy. Distinct from a decision (nobody decided it) and from a finding (it constrains rather than informs).
|
|
89
|
+
profileKind: kind
|
|
90
|
+
properties:
|
|
91
|
+
- slug: rule
|
|
92
|
+
label: The Rule
|
|
93
|
+
valueType: string
|
|
94
|
+
inputType: textarea
|
|
95
|
+
description: State the constraint in the terms the issuer uses, not in a paraphrase. Paraphrase is where eligibility collisions hide.
|
|
96
|
+
- slug: issuer
|
|
97
|
+
label: Issued By
|
|
98
|
+
valueType: string
|
|
99
|
+
inputType: text
|
|
100
|
+
description: Who imposes it — a regulator, a funder, an ordre professionnel, a platform, a counterparty.
|
|
101
|
+
- slug: conventionType
|
|
102
|
+
label: Type
|
|
103
|
+
valueType: string
|
|
104
|
+
inputType: select
|
|
105
|
+
enumValues: [regulatory, deontological, eligibility, contractual, platform, fiscal, other]
|
|
106
|
+
- slug: bindingStatus
|
|
107
|
+
label: Binding Status
|
|
108
|
+
valueType: string
|
|
109
|
+
inputType: select
|
|
110
|
+
enumValues: [verified, assumed, contested, expired]
|
|
111
|
+
description: "`assumed` is the dangerous default — it means nobody has read the source text yet."
|
|
112
|
+
- slug: sourceReference
|
|
113
|
+
label: Source Reference
|
|
114
|
+
valueType: string
|
|
115
|
+
inputType: text
|
|
116
|
+
description: The cahier des charges, article, or guide this comes from. A convention with no reference is an assumption wearing a rule's clothes.
|
|
117
|
+
- slug: expiresAt
|
|
118
|
+
label: Expires / Review By
|
|
119
|
+
valueType: date
|
|
120
|
+
inputType: date
|
|
121
|
+
- slug: consequenceIfBreached
|
|
122
|
+
label: Consequence If Breached
|
|
123
|
+
valueType: string
|
|
124
|
+
inputType: textarea
|
|
125
|
+
|
|
126
|
+
- slug: projection
|
|
127
|
+
displayName: Projection
|
|
128
|
+
icon: trending-up
|
|
129
|
+
color: violet
|
|
130
|
+
description: A quantified scenario with its assumptions attached. The number is never the artifact — the assumptions are, because that is the only part anyone can argue with.
|
|
131
|
+
profileKind: kind
|
|
132
|
+
properties:
|
|
133
|
+
- slug: scenario
|
|
134
|
+
label: Scenario
|
|
135
|
+
valueType: string
|
|
136
|
+
inputType: select
|
|
137
|
+
enumValues: [pessimistic, base, optimistic]
|
|
138
|
+
- slug: horizon
|
|
139
|
+
label: Horizon
|
|
140
|
+
valueType: string
|
|
141
|
+
inputType: select
|
|
142
|
+
enumValues: [month, quarter, year, three-year]
|
|
143
|
+
- slug: assumptions
|
|
144
|
+
label: Assumptions
|
|
145
|
+
valueType: string
|
|
146
|
+
inputType: textarea
|
|
147
|
+
description: Every input this number needs to be true. When one changes, whatever rests on this projection goes stale.
|
|
148
|
+
- slug: revenueEstimate
|
|
149
|
+
label: Revenue Estimate
|
|
150
|
+
valueType: number
|
|
151
|
+
inputType: number
|
|
152
|
+
- slug: costEstimate
|
|
153
|
+
label: Cost Estimate
|
|
154
|
+
valueType: number
|
|
155
|
+
inputType: number
|
|
156
|
+
- slug: breakEvenUnits
|
|
157
|
+
label: Break-Even Units
|
|
158
|
+
valueType: number
|
|
159
|
+
inputType: number
|
|
160
|
+
- slug: confidence
|
|
161
|
+
label: Confidence
|
|
162
|
+
valueType: string
|
|
163
|
+
inputType: select
|
|
164
|
+
enumValues: [low, medium, high]
|
|
165
|
+
|
|
166
|
+
# Relation TYPE vocabulary. Declared here (not as bare entityLinks) because
|
|
167
|
+
# these two carry description + inverse labels that the entity-link path drops.
|
|
168
|
+
relationDefs:
|
|
169
|
+
- slug: rests_on
|
|
170
|
+
displayName: Rests On
|
|
171
|
+
description: "The source depends on the target for its validity — not for its scheduling. Cycles are LEGAL and meaningful here: a business model is mutually supporting by nature, and forcing it into an acyclic dependency graph destroys the information. When the target changes, everything resting on it is stale until re-checked."
|
|
172
|
+
isDirectional: true
|
|
173
|
+
uiHints:
|
|
174
|
+
category: reference
|
|
175
|
+
inverseLabel: Supports
|
|
176
|
+
- slug: refutes
|
|
177
|
+
displayName: Refutes
|
|
178
|
+
description: The source is evidence that the target is false. Kept as a first-class edge so a refuted component stays in the graph with its refutation attached, instead of being quietly deleted — the reason a road was not taken is worth more than the road.
|
|
179
|
+
isDirectional: true
|
|
180
|
+
uiHints:
|
|
181
|
+
category: reference
|
|
182
|
+
inverseLabel: Refuted By
|
|
183
|
+
|
|
184
|
+
entityLinks:
|
|
185
|
+
- sourceProfileSlug: offer
|
|
186
|
+
targetProfileSlug: convention
|
|
187
|
+
type: rests_on
|
|
188
|
+
label: Rests on
|
|
189
|
+
- sourceProfileSlug: offer
|
|
190
|
+
targetProfileSlug: audience
|
|
191
|
+
type: rests_on
|
|
192
|
+
label: Rests on
|
|
193
|
+
- sourceProfileSlug: projection
|
|
194
|
+
targetProfileSlug: offer
|
|
195
|
+
type: rests_on
|
|
196
|
+
label: Rests on
|
|
197
|
+
- sourceProfileSlug: decision
|
|
198
|
+
targetProfileSlug: convention
|
|
199
|
+
type: rests_on
|
|
200
|
+
label: Rests on
|
|
201
|
+
- sourceProfileSlug: decision
|
|
202
|
+
targetProfileSlug: projection
|
|
203
|
+
type: rests_on
|
|
204
|
+
label: Rests on
|
|
205
|
+
- sourceProfileSlug: convention
|
|
206
|
+
targetProfileSlug: offer
|
|
207
|
+
type: refutes
|
|
208
|
+
label: Refutes
|
|
209
|
+
- sourceProfileSlug: convention
|
|
210
|
+
targetProfileSlug: audience
|
|
211
|
+
type: refutes
|
|
212
|
+
label: Refutes
|
|
213
|
+
- sourceProfileSlug: convention
|
|
214
|
+
targetProfileSlug: question
|
|
215
|
+
type: rests_on
|
|
216
|
+
label: Rests on
|
|
217
|
+
|
|
218
|
+
views:
|
|
219
|
+
- name: Conventions
|
|
220
|
+
slug: business-model-conventions
|
|
221
|
+
type: table
|
|
222
|
+
scopeProfileSlug: convention
|
|
223
|
+
description: Every rule the model must satisfy, who imposes it, and whether anyone has actually verified it.
|
|
224
|
+
defaultView: true
|
|
225
|
+
config:
|
|
226
|
+
columns: [title, conventionType, issuer, bindingStatus, expiresAt]
|
|
227
|
+
- name: Unverified Conventions
|
|
228
|
+
slug: business-model-assumed-conventions
|
|
229
|
+
type: list
|
|
230
|
+
scopeProfileSlug: convention
|
|
231
|
+
description: Conventions still marked assumed or contested — the model rests on these and nobody has read the source text.
|
|
232
|
+
config:
|
|
233
|
+
filters:
|
|
234
|
+
- field: bindingStatus
|
|
235
|
+
operator: in
|
|
236
|
+
value: [assumed, contested]
|
|
237
|
+
- name: Projections
|
|
238
|
+
slug: business-model-projections
|
|
239
|
+
type: table
|
|
240
|
+
scopeProfileSlug: projection
|
|
241
|
+
description: Scenarios with their assumptions in the open, so a number can be argued with.
|
|
242
|
+
config:
|
|
243
|
+
columns: [title, scenario, horizon, revenueEstimate, costEstimate, confidence]
|
|
244
|
+
- name: Open Interrogation
|
|
245
|
+
slug: business-model-open-questions
|
|
246
|
+
type: kanban
|
|
247
|
+
scopeProfileSlug: question
|
|
248
|
+
groupBy: questionStatus
|
|
249
|
+
description: The nine component questions and everything they spawned, by status.
|
|
250
|
+
|
|
251
|
+
# ── Seeds: the interrogation, not fake data ────────────────────────────────
|
|
252
|
+
# A strategy template that seeds example companies teaches nothing and gets
|
|
253
|
+
# deleted. What is actually reusable is the QUESTION SET — the workspace should
|
|
254
|
+
# arrive with the model's nine interrogations already open, so the first session
|
|
255
|
+
# has somewhere to start and the kanban is not an empty box.
|
|
256
|
+
seedEntities:
|
|
257
|
+
- profileSlug: question
|
|
258
|
+
title: Who is the porteur, and what does the model rest on them for?
|
|
259
|
+
properties:
|
|
260
|
+
questionStatus: open
|
|
261
|
+
content: Name the person, then name the specific competence, network, capital, or legitimacy the model cannot survive without. If the answer is generic, the model has no porteur — it has a job posting.
|
|
262
|
+
- profileSlug: question
|
|
263
|
+
title: What does the porteur's situation forbid?
|
|
264
|
+
properties:
|
|
265
|
+
questionStatus: open
|
|
266
|
+
content: Runway, hours available, geography, non-compete, dependants, health. Constraints discovered in month four were knowable in week one.
|
|
267
|
+
- profileSlug: question
|
|
268
|
+
title: What is the offer, stated as the problem it ends?
|
|
269
|
+
properties:
|
|
270
|
+
questionStatus: open
|
|
271
|
+
content: Not a description of what is built — a statement of the situation a buyer is in before, and after. If the offer can only be described by its features, the buyer has not been found yet.
|
|
272
|
+
- profileSlug: question
|
|
273
|
+
title: Who exactly buys it, and who merely benefits from it?
|
|
274
|
+
properties:
|
|
275
|
+
questionStatus: open
|
|
276
|
+
content: Link to `audience` entities. Payer, user, and beneficiary are frequently three different parties, and a model that conflates them forecasts revenue from people who never pay.
|
|
277
|
+
- profileSlug: question
|
|
278
|
+
title: Which conventions bind this model, and which are still assumed?
|
|
279
|
+
properties:
|
|
280
|
+
questionStatus: open
|
|
281
|
+
content: Eligibility thresholds, professional duties, platform policies, fiscal regimes. Every convention starts `assumed`; the work is reading the source text and moving it to `verified`. An eligibility collision — where the segment with the most pull sits outside the scheme that would fund it — only becomes visible once the numbers are pulled.
|
|
282
|
+
- profileSlug: question
|
|
283
|
+
title: What does one unit of delivery actually cost to produce?
|
|
284
|
+
properties:
|
|
285
|
+
questionStatus: open
|
|
286
|
+
content: Time, tooling, subcontracting, the unbilled parts. Link the answer to a `projection` so the assumptions stay attached to the number.
|
|
287
|
+
- profileSlug: question
|
|
288
|
+
title: Where does the money come from before revenue does?
|
|
289
|
+
properties:
|
|
290
|
+
questionStatus: open
|
|
291
|
+
content: Attach `funder` roles to the companies or schemes involved, with their eligibility conditions. Subsidy is a credential and a discount; it is rarely a pipeline.
|
|
292
|
+
- profileSlug: question
|
|
293
|
+
title: What means — people, tools, licences, premises — must exist first?
|
|
294
|
+
properties:
|
|
295
|
+
questionStatus: open
|
|
296
|
+
content: The means rest on the money and the money rests on the offer. That circularity is the model, not a planning error; record it with `rests_on` and stop trying to linearise it.
|
|
297
|
+
- profileSlug: question
|
|
298
|
+
title: What observation would refute this model, and by when?
|
|
299
|
+
properties:
|
|
300
|
+
questionStatus: open
|
|
301
|
+
content: Set the falsifiable test now, while it can still be set honestly. A model with no refuting observation is a belief with a spreadsheet attached.
|
|
302
|
+
|
|
303
|
+
# The circularity, declared. These edges deliberately form cycles — that is the
|
|
304
|
+
# point of `rests_on`, and the reason this pack does not use `depends_on`.
|
|
305
|
+
suggestedRelations:
|
|
306
|
+
- sourceRef: What is the offer, stated as the problem it ends?
|
|
307
|
+
targetRef: Who exactly buys it, and who merely benefits from it?
|
|
308
|
+
type: rests_on
|
|
309
|
+
- sourceRef: What is the offer, stated as the problem it ends?
|
|
310
|
+
targetRef: What means — people, tools, licences, premises — must exist first?
|
|
311
|
+
type: rests_on
|
|
312
|
+
- sourceRef: What means — people, tools, licences, premises — must exist first?
|
|
313
|
+
targetRef: Where does the money come from before revenue does?
|
|
314
|
+
type: rests_on
|
|
315
|
+
- sourceRef: Where does the money come from before revenue does?
|
|
316
|
+
targetRef: What is the offer, stated as the problem it ends?
|
|
317
|
+
type: rests_on
|
|
318
|
+
- sourceRef: Where does the money come from before revenue does?
|
|
319
|
+
targetRef: Which conventions bind this model, and which are still assumed?
|
|
320
|
+
type: rests_on
|
|
321
|
+
- sourceRef: Who exactly buys it, and who merely benefits from it?
|
|
322
|
+
targetRef: Which conventions bind this model, and which are still assumed?
|
|
323
|
+
type: rests_on
|
|
324
|
+
- sourceRef: What does one unit of delivery actually cost to produce?
|
|
325
|
+
targetRef: What does the porteur's situation forbid?
|
|
326
|
+
type: rests_on
|
|
327
|
+
- sourceRef: What is the offer, stated as the problem it ends?
|
|
328
|
+
targetRef: Who is the porteur, and what does the model rest on them for?
|
|
329
|
+
type: rests_on
|
|
330
|
+
|
|
331
|
+
playbooks:
|
|
332
|
+
- name: Business Model Interrogation
|
|
333
|
+
description: Walk the nine components as questions, resolve each into a decision backed by evidence, and record the conventions and projections the model rests on — including the cycles.
|
|
334
|
+
goalTemplate: "Interrogate the business model for {venture} and record what it rests on. Steps: (1) Walk each of the nine seeded `question` entities in this workspace; where one is already answered, mark it and move on. (2) Resolve each into a `decision`, keeping the options that lost attached via `refutes` rather than deleting them. Where the evidence itself needs gathering, do that in the Research workspace and link back — do not restate sources here. (3) Every external rule discovered becomes a `convention` with its `issuer` and `sourceReference`; leave `bindingStatus` at `assumed` until the source text has actually been read, then `verified`. (4) Every number becomes a `projection` with its `assumptions` filled in — never a bare figure. (5) Wire the graph with `rests_on` — offer on audience, projection on offer, decision on convention. Do NOT try to make the graph acyclic: if the offer rests on the means and the means rest on the money and the money rests on the offer, record all three. (6) Attach `porteur` and `funder` roles to the real people and organisations instead of creating new entities for them. (7) Close by listing which conventions are still `assumed` and which projections carry `low` confidence — that list is the actual state of the model."
|
|
335
|
+
icon: layout-grid
|
|
336
|
+
|
|
337
|
+
actionPlacements:
|
|
338
|
+
- profileSlug: question
|
|
339
|
+
surface: entity-detail
|
|
340
|
+
kind: playbook
|
|
341
|
+
ref: Business Model Interrogation
|
|
342
|
+
label: Interrogate as model component
|
|
343
|
+
- profileSlug: convention
|
|
344
|
+
surface: entity-detail
|
|
345
|
+
kind: playbook
|
|
346
|
+
ref: Business Model Interrogation
|
|
347
|
+
label: Re-check what rests on this
|
|
348
|
+
|
|
349
|
+
integrations:
|
|
350
|
+
- templateKey: research-methods
|
|
351
|
+
- templateKey: exa
|
package/src/catalog.ts
CHANGED
|
@@ -33,6 +33,20 @@ import type { WorkspaceYaml } from "./types.js";
|
|
|
33
33
|
// Inputs
|
|
34
34
|
// ============================================================================
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* A lightweight, QUERY-TIME summary of one sub-object a package carries (a cell
|
|
38
|
+
* today; the shape generalizes to views/automations without change). Rides the
|
|
39
|
+
* catalog LIST projection so a surface can match/surface a sub-item WITHOUT the
|
|
40
|
+
* full `definition` — it carries `key + name` only, never the sub-object's code.
|
|
41
|
+
* `kind` is DERIVED (in the CP SELECT, from which sub-array the row came from),
|
|
42
|
+
* never a stored taxonomy.
|
|
43
|
+
*/
|
|
44
|
+
export interface PackageItemSummary {
|
|
45
|
+
kind: string;
|
|
46
|
+
key: string;
|
|
47
|
+
name: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
/**
|
|
37
51
|
* A bundled template — the canonical `WorkspaceYaml` this package ships. Callers
|
|
38
52
|
* pass `listPublicTemplates()` (18) or `listWorkspaceTemplates()` (21); the door
|
|
@@ -69,6 +83,20 @@ export interface RemoteEntry {
|
|
|
69
83
|
* never "unknown-so-hide".
|
|
70
84
|
*/
|
|
71
85
|
isPrivate?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* The minimum subscription tier required to INSTALL this package (`solo` | `pro`
|
|
88
|
+
* | `team` | `enterprise`); `null`/absent ⇒ ungated. DISPLAY only here — the real
|
|
89
|
+
* install floor is the pod's `assertPackageTierAccess`. Kept a bare `string` so
|
|
90
|
+
* this pure/shared door stays enum-agnostic (like `category`).
|
|
91
|
+
*/
|
|
92
|
+
requiredTier?: string | null;
|
|
93
|
+
/**
|
|
94
|
+
* Lightweight computed summary of the row's sub-objects (cells), carried on the
|
|
95
|
+
* LIST projection so a surface can surface a matching sub-item without the full
|
|
96
|
+
* `definition`. `undefined` on sources that don't compute it (bundled) — treated
|
|
97
|
+
* as "no known sub-items". See `PackageItemSummary`.
|
|
98
|
+
*/
|
|
99
|
+
items?: PackageItemSummary[];
|
|
72
100
|
/**
|
|
73
101
|
* The CP `definition` jsonb — opaque install payload. The door reads only
|
|
74
102
|
* `definition.sourcePackage.version` (the winner signal). Absent on browse rows
|
|
@@ -128,6 +156,13 @@ export interface CpPackageRow {
|
|
|
128
156
|
icon?: string | null;
|
|
129
157
|
/** Present only on `/mine` rows. `false` ⇒ private. Browse rows omit it. */
|
|
130
158
|
isPublic?: boolean;
|
|
159
|
+
/** The package's `required_tier` gate (`solo`|`pro`|…); `null`/absent ⇒ ungated. */
|
|
160
|
+
requiredTier?: string | null;
|
|
161
|
+
/**
|
|
162
|
+
* Computed sub-object summary from `GET /api/packages` / `/mine` (query-time,
|
|
163
|
+
* not a stored column). Carries `key + name` per cell; never the cell code.
|
|
164
|
+
*/
|
|
165
|
+
items?: PackageItemSummary[];
|
|
131
166
|
}
|
|
132
167
|
|
|
133
168
|
/**
|
|
@@ -154,6 +189,8 @@ export function rowToRemoteEntry(
|
|
|
154
189
|
tags: row.tags ?? [],
|
|
155
190
|
icon: row.icon ?? null,
|
|
156
191
|
isPrivate,
|
|
192
|
+
requiredTier: row.requiredTier ?? null,
|
|
193
|
+
items: row.items,
|
|
157
194
|
// Both list routes strip `definition`; absence ⇒ bundle wins a slug conflict,
|
|
158
195
|
// and the install path fetches the full definition per-slug when needed.
|
|
159
196
|
};
|
|
@@ -175,6 +212,18 @@ interface CatalogEntryBase {
|
|
|
175
212
|
icon?: string | null;
|
|
176
213
|
/** `true` ⇒ not publicly listed (internal bundled template, or private CP row). */
|
|
177
214
|
isPrivate: boolean;
|
|
215
|
+
/**
|
|
216
|
+
* Minimum install tier (`solo`|`pro`|…); `null` ⇒ ungated. Bundled templates are
|
|
217
|
+
* always `null` (they ship in-client, never gated). DISPLAY only — a surface
|
|
218
|
+
* shows a "requires <tier>" badge; the real floor is the pod's install check.
|
|
219
|
+
*/
|
|
220
|
+
requiredTier?: string | null;
|
|
221
|
+
/**
|
|
222
|
+
* The entry's sub-object summary (cells). Always present (defaults to `[]`) so a
|
|
223
|
+
* consumer can match sub-items uniformly. Bundled workspace templates carry no
|
|
224
|
+
* cells → `[]` (honest); a remote row carries its CP-computed `items`.
|
|
225
|
+
*/
|
|
226
|
+
items: PackageItemSummary[];
|
|
178
227
|
}
|
|
179
228
|
|
|
180
229
|
/** A catalog entry backed by a bundled template. Install via `toWorkspaceDefinition()` / `toPackageDefinition()` on `template`. */
|
|
@@ -256,6 +305,10 @@ function bundledToEntry(tpl: WorkspaceYaml): BundledCatalogEntry {
|
|
|
256
305
|
tags: tpl.meta.tags ?? [],
|
|
257
306
|
icon: tpl.meta.icon,
|
|
258
307
|
isPrivate: tpl.meta.isPublic === false,
|
|
308
|
+
// Bundled templates ship in-client and are never tier-gated.
|
|
309
|
+
requiredTier: null,
|
|
310
|
+
// Bundled workspace templates carry no cells — be honest, not `undefined`.
|
|
311
|
+
items: [],
|
|
259
312
|
template: tpl,
|
|
260
313
|
};
|
|
261
314
|
}
|
|
@@ -271,6 +324,8 @@ function remoteToEntry(row: RemoteEntry): RemoteCatalogEntry {
|
|
|
271
324
|
tags: row.tags ?? [],
|
|
272
325
|
icon: row.icon ?? null,
|
|
273
326
|
isPrivate: row.isPrivate ?? false,
|
|
327
|
+
requiredTier: row.requiredTier ?? null,
|
|
328
|
+
items: row.items ?? [],
|
|
274
329
|
remote: row,
|
|
275
330
|
};
|
|
276
331
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# France civic open-data research overlay — STUB (enhance later)
|
|
2
|
+
#
|
|
3
|
+
# Thin overlay that composes onto research-base for investigating French public
|
|
4
|
+
# data (open data / Licence Ouverte). NOT a government product, NOT a marketplace
|
|
5
|
+
# civic-tech app. Private workspace template only.
|
|
6
|
+
#
|
|
7
|
+
# Data access: consume official APIs only — data.gouv.fr, api.gouv.fr, PISTE.
|
|
8
|
+
# Never scrape HTML. Do not invent a public pod host or resource_shares.
|
|
9
|
+
# No automations that fetch the web in this stub.
|
|
10
|
+
|
|
11
|
+
meta:
|
|
12
|
+
slug: civic-france
|
|
13
|
+
domain: knowledge
|
|
14
|
+
name: France Civic Research
|
|
15
|
+
description: Overlay for investigating French public data (open data / Licence Ouverte). Composes onto Research. Not a government product — a private research stub.
|
|
16
|
+
icon: landmark
|
|
17
|
+
color: "#1D4ED8"
|
|
18
|
+
tags: [research, civic, france, open-data, licence-ouverte]
|
|
19
|
+
isPublic: false
|
|
20
|
+
|
|
21
|
+
workspace:
|
|
22
|
+
name: Research
|
|
23
|
+
description: Adds a French civic open-data lens onto the Research substrate — track public datasets consulted during investigations.
|
|
24
|
+
subtype: research-base
|
|
25
|
+
|
|
26
|
+
# Layer additively onto the research-base workspace (no second workspace).
|
|
27
|
+
# Visibility and capabilities OMITTED so the base Research workspace keeps its own.
|
|
28
|
+
dependencies:
|
|
29
|
+
- slug: research-base
|
|
30
|
+
relation: compose
|
|
31
|
+
reason: France civic research is a research method — it needs the Research substrate (question / research / source) to work over.
|
|
32
|
+
|
|
33
|
+
profiles:
|
|
34
|
+
# Slugs this overlay reuses from research-base — declared for the per-file
|
|
35
|
+
# validator (base owns their full schema; the applier skips already-existing
|
|
36
|
+
# properties). Mirror research-decision's reuse pattern.
|
|
37
|
+
- slug: question
|
|
38
|
+
displayName: Question
|
|
39
|
+
icon: help-circle
|
|
40
|
+
color: blue
|
|
41
|
+
description: An open question driving a civic-data investigation.
|
|
42
|
+
properties:
|
|
43
|
+
- slug: questionStatus
|
|
44
|
+
label: Status
|
|
45
|
+
valueType: string
|
|
46
|
+
inputType: select
|
|
47
|
+
enumValues: [open, exploring, answered, abandoned]
|
|
48
|
+
constraints: { defaultValue: open }
|
|
49
|
+
|
|
50
|
+
- slug: research
|
|
51
|
+
displayName: Research
|
|
52
|
+
icon: microscope
|
|
53
|
+
color: violet
|
|
54
|
+
description: An investigation into French public data — sources, findings, conclusion.
|
|
55
|
+
properties:
|
|
56
|
+
- slug: researchStatus
|
|
57
|
+
label: Status
|
|
58
|
+
valueType: string
|
|
59
|
+
inputType: select
|
|
60
|
+
enumValues: [ongoing, concluded, abandoned]
|
|
61
|
+
constraints: { defaultValue: ongoing }
|
|
62
|
+
|
|
63
|
+
- slug: source
|
|
64
|
+
displayName: Source
|
|
65
|
+
icon: file-text
|
|
66
|
+
color: cyan
|
|
67
|
+
description: Anything consulted — API payload, dataset page, decree, report.
|
|
68
|
+
properties:
|
|
69
|
+
- slug: url
|
|
70
|
+
label: URL
|
|
71
|
+
valueType: string
|
|
72
|
+
inputType: url
|
|
73
|
+
|
|
74
|
+
# Workspace-owned catalog entry for a French public dataset (thin stub).
|
|
75
|
+
- slug: public_dataset
|
|
76
|
+
displayName: Public dataset
|
|
77
|
+
icon: database
|
|
78
|
+
color: blue
|
|
79
|
+
description: A French public dataset consulted via official open-data APIs (data.gouv.fr / api.gouv.fr / PISTE) — never scraped HTML.
|
|
80
|
+
scope: WORKSPACE
|
|
81
|
+
properties:
|
|
82
|
+
- slug: sourceUrl
|
|
83
|
+
label: Source URL
|
|
84
|
+
valueType: string
|
|
85
|
+
inputType: url
|
|
86
|
+
- slug: license
|
|
87
|
+
label: License
|
|
88
|
+
valueType: string
|
|
89
|
+
inputType: text
|
|
90
|
+
- slug: publisher
|
|
91
|
+
label: Publisher
|
|
92
|
+
valueType: string
|
|
93
|
+
inputType: text
|
|
94
|
+
|
|
95
|
+
views:
|
|
96
|
+
- name: Public datasets
|
|
97
|
+
slug: civic-france-public-datasets
|
|
98
|
+
type: table
|
|
99
|
+
scopeProfileSlug: public_dataset
|
|
100
|
+
description: French public datasets tracked in this research workspace.
|
|
101
|
+
config:
|
|
102
|
+
columns: [title, publisher, license, sourceUrl]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FRESHNESS GATE for the BUILT `dist/` artifact.
|
|
3
|
+
*
|
|
4
|
+
* WHY (read before deleting):
|
|
5
|
+
* `templates-freshness.test.ts` (sibling file) proves `src/templates.ts`
|
|
6
|
+
* matches `src/*.yaml`. That is necessary but not sufficient: `dist/` — built
|
|
7
|
+
* by `tsup` from `src/templates.ts` — is what `synap-backend` and every other
|
|
8
|
+
* consumer actually imports as the published `@synap-core/workspace-templates`
|
|
9
|
+
* npm dependency (see this package's `exports` field: `dist/index.js`).
|
|
10
|
+
* Nothing regenerates `dist/` automatically, and it is committed independently
|
|
11
|
+
* of `src/templates.ts`. A YAML edit can reach `src/templates.ts` (green on
|
|
12
|
+
* the sibling test) and NOT reach `dist/` — the sibling test stays green while
|
|
13
|
+
* the backend keeps reading stale data, because it never looks at `dist/` at
|
|
14
|
+
* all. That gap is exactly what happened on 2026-09-05.
|
|
15
|
+
*
|
|
16
|
+
* This test closes it by importing the BUILT `dist/index.js` module (not
|
|
17
|
+
* re-parsing its text — a bundled/minified file has no byte-for-byte
|
|
18
|
+
* relationship to `src/templates.ts`) and asserting its exported
|
|
19
|
+
* `WORKSPACE_TEMPLATES` deep-equals what `src/*.yaml` renders to right now.
|
|
20
|
+
* That is the same non-vacuous shape as the sibling test: it re-derives the
|
|
21
|
+
* expected value from the YAML source on every run rather than trusting any
|
|
22
|
+
* stored marker, and it asserts the corpus is non-empty so a broken import
|
|
23
|
+
* can't silently compare `{}` to `{}`.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { describe, expect, it } from "vitest";
|
|
27
|
+
import { readYamlTemplates } from "./generate.mjs";
|
|
28
|
+
|
|
29
|
+
const FIX_COMMAND =
|
|
30
|
+
"cd synap-app/packages/workspace-templates && pnpm build";
|
|
31
|
+
|
|
32
|
+
describe("dist/ freshness", () => {
|
|
33
|
+
it("dist/index.js's WORKSPACE_TEMPLATES matches what src/*.yaml renders to", async () => {
|
|
34
|
+
const expectedEntries = readYamlTemplates().map((t) => [t.slug, t.data]);
|
|
35
|
+
const expected = Object.fromEntries(expectedEntries);
|
|
36
|
+
|
|
37
|
+
// Guard against a vacuous pass: if the YAML corpus itself came back empty
|
|
38
|
+
// (a broken readdir, an emptied src/ directory), an empty-vs-empty
|
|
39
|
+
// comparison below would prove nothing. Fail loudly instead.
|
|
40
|
+
if (expectedEntries.length === 0) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"readYamlTemplates() returned zero templates — src/*.yaml did not " +
|
|
43
|
+
"parse. This test cannot verify dist/ freshness against an empty " +
|
|
44
|
+
"expected corpus; fix the YAML read before trusting this gate.",
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let dist: { WORKSPACE_TEMPLATES?: Record<string, unknown> };
|
|
49
|
+
try {
|
|
50
|
+
// dist/index.js is the BUILT artifact — import it live so we're
|
|
51
|
+
// comparing against exactly what a consumer (synap-backend, the CLI)
|
|
52
|
+
// resolves via this package's `exports` field, not a re-parse of its
|
|
53
|
+
// source text.
|
|
54
|
+
dist = await import("../dist/index.js");
|
|
55
|
+
} catch (err) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
[
|
|
58
|
+
"",
|
|
59
|
+
"❌ dist/index.js could not be imported — it is missing or fails to load.",
|
|
60
|
+
"",
|
|
61
|
+
` FIX — build it:`,
|
|
62
|
+
` ${FIX_COMMAND}`,
|
|
63
|
+
"",
|
|
64
|
+
` (original error: ${(err as Error).message})`,
|
|
65
|
+
"",
|
|
66
|
+
].join("\n"),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const actual = dist.WORKSPACE_TEMPLATES;
|
|
71
|
+
if (!actual || Object.keys(actual).length === 0) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"dist/index.js loaded but exported no WORKSPACE_TEMPLATES (or an " +
|
|
74
|
+
`empty object) — it is stale or broken.\n\n FIX:\n ${FIX_COMMAND}\n`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
[
|
|
81
|
+
"",
|
|
82
|
+
"❌ dist/index.js is STALE — its WORKSPACE_TEMPLATES no longer matches",
|
|
83
|
+
" what src/*.yaml renders to. synap-backend and every other consumer",
|
|
84
|
+
" resolve this package to dist/, not src/, so a stale dist/ means the",
|
|
85
|
+
" backend is reading templates that no longer exist on disk — even",
|
|
86
|
+
" though src/templates.ts (and templates-freshness.test.ts) are fresh.",
|
|
87
|
+
"",
|
|
88
|
+
" FIX — rebuild and commit the result:",
|
|
89
|
+
` ${FIX_COMMAND}`,
|
|
90
|
+
"",
|
|
91
|
+
].join("\n"),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
expect(actual).toEqual(expected);
|
|
96
|
+
});
|
|
97
|
+
});
|