@synap-core/workspace-templates 0.6.0 → 0.6.1

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/src/define.ts CHANGED
@@ -10,9 +10,107 @@
10
10
  * shape never drifts per call-site.
11
11
  */
12
12
 
13
- import type { WorkspaceYaml } from "./types.js";
13
+ import type { TemplateBentoBlock, WorkspaceYaml } from "./types.js";
14
14
  import { getWorkspaceTemplate } from "./templates.js";
15
15
 
16
+ /** A widget block as the wire expects it: `bentoLayout[]`, keyed by `widgetType`. */
17
+ export interface BentoWidgetBlockWire {
18
+ widgetType: string;
19
+ pos: TemplateBentoBlock['pos'];
20
+ config?: Record<string, unknown>;
21
+ }
22
+
23
+ /** A view block as the wire expects it: `bentoViewBlocks[]`, keyed by `viewName`. */
24
+ export interface BentoViewBlockWire {
25
+ kind: 'view';
26
+ viewName: string;
27
+ pos: TemplateBentoBlock['pos'];
28
+ overrides?: Record<string, unknown>;
29
+ }
30
+
31
+ export interface SplitBentoBlocks {
32
+ bentoLayout: BentoWidgetBlockWire[];
33
+ bentoViewBlocks: BentoViewBlockWire[];
34
+ }
35
+
36
+ /**
37
+ * Split the authored `bento.blocks` DISCRIMINATED UNION into the TWO wire
38
+ * fields the pod expects. This is the whole point: `TemplateBentoBlock` is one
39
+ * array discriminated by `kind`, but the pod does NOT read `kind` — it splits by
40
+ * FIELD (`create-workspace-from-definition.ts`):
41
+ *
42
+ * - `bentoLayout` → mapped as widgets, reads `widget.widgetType`
43
+ * - `bentoViewBlocks` → filtered against `viewMap[vb.viewSlug ?? vb.viewName]`,
44
+ * unknown entries DROPPED with a logger.warn
45
+ *
46
+ * Dumping the whole union into either single field silently mis-routes the other
47
+ * half — which is exactly what both converters used to do:
48
+ * - `toWorkspaceDefinition` sent all blocks as `bentoViewBlocks`, so widgets
49
+ * (no `viewName`) hit `viewMap[undefined]` and were filtered out.
50
+ * - `toPackageDefinition` sent all blocks as `bentoLayout`, so view blocks
51
+ * (no `widgetType`) became widgets with `widgetType: undefined`.
52
+ * Net: nobody had ever seen an authored bento render as authored.
53
+ *
54
+ * Blocks are emitted in the EXACT shape each sink declares (the tRPC
55
+ * `createFromDefinition` zod is strict: `bentoLayout` requires `widgetType`,
56
+ * `bentoViewBlocks` requires `kind: 'view'` + `viewName`), so a widget's
57
+ * `overrides` and a view's `config` are not smuggled into the wrong sink.
58
+ * Authored order is preserved WITHIN each kind; the pod concatenates
59
+ * `[...widgetBlocks, ...viewBlocks]`, so cross-kind order is the pod's to define.
60
+ *
61
+ * `kind` is INFERRED from field presence when absent. `TemplateBentoBlock.kind`
62
+ * is a required field in TypeScript, but the generated `templates.ts` casts the
63
+ * parsed YAML `as unknown as Record<string, WorkspaceYaml>` — so the compiler
64
+ * never checks the authored data against the type, and `agent-fleet.yaml`
65
+ * authored 8 widget blocks with no `kind:` at all (bare `widgetType`). Routing
66
+ * strictly on `kind` would silently DROP those — the exact failure mode this
67
+ * function exists to end. Inferring by field mirrors what the pod itself does
68
+ * (it splits by field and never reads `kind`), so the seam is total: a block
69
+ * that CAN be materialized always is. `scripts/check-converters.mjs` is the
70
+ * strict half — it fails the build if an authored block omits `kind` — so this
71
+ * leniency is a safety net, not a place for drift to hide.
72
+ */
73
+ function resolveBlockKind(block: TemplateBentoBlock): 'widget' | 'view' | null {
74
+ if (block.kind === 'widget' || block.kind === 'view') return block.kind;
75
+ if (block.widgetType) return 'widget';
76
+ if (block.viewName) return 'view';
77
+ return null;
78
+ }
79
+
80
+ export function splitBentoBlocks(
81
+ blocks: TemplateBentoBlock[] | undefined
82
+ ): SplitBentoBlocks {
83
+ const bentoLayout: BentoWidgetBlockWire[] = [];
84
+ const bentoViewBlocks: BentoViewBlockWire[] = [];
85
+
86
+ for (const block of blocks ?? []) {
87
+ const kind = resolveBlockKind(block);
88
+ if (kind === 'widget') {
89
+ // A widget block without `widgetType` cannot be rendered — the pod would
90
+ // create a `widgetType: undefined` block. Drop it loudly rather than ship
91
+ // a broken block; the build-time guard asserts this never happens in the
92
+ // authored templates.
93
+ if (!block.widgetType) continue;
94
+ bentoLayout.push({
95
+ widgetType: block.widgetType,
96
+ pos: block.pos,
97
+ ...(block.config ? { config: block.config } : {}),
98
+ });
99
+ } else if (kind === 'view') {
100
+ // Same reasoning: the pod filters unknown/absent view names anyway.
101
+ if (!block.viewName) continue;
102
+ bentoViewBlocks.push({
103
+ kind: 'view',
104
+ viewName: block.viewName,
105
+ pos: block.pos,
106
+ ...(block.overrides ? { overrides: block.overrides } : {}),
107
+ });
108
+ }
109
+ }
110
+
111
+ return { bentoLayout, bentoViewBlocks };
112
+ }
113
+
16
114
  /** The flat definition object accepted by createFromDefinition. */
17
115
  export interface WorkspaceDefinitionInput {
18
116
  workspaceName: string;
@@ -26,7 +124,16 @@ export interface WorkspaceDefinitionInput {
26
124
  displayTemplates?: WorkspaceYaml['displayTemplates'];
27
125
  layoutConfig: WorkspaceYaml['layout'] | undefined;
28
126
  bentoViewName?: string;
29
- bentoViewBlocks?: NonNullable<WorkspaceYaml['bento']>['blocks'];
127
+ /**
128
+ * WIDGET half of the authored bento union. The door declares this field
129
+ * (`workspaces.createFromDefinition` zod) and the pod maps it as widgets via
130
+ * `widget.widgetType`. It was missing from this interface entirely, which is
131
+ * why `toWorkspaceDefinition` had nowhere to put widgets and dumped the whole
132
+ * union into `bentoViewBlocks` — where every widget was silently filtered out.
133
+ */
134
+ bentoLayout?: BentoWidgetBlockWire[];
135
+ /** VIEW half of the authored bento union — resolved against the viewMap. */
136
+ bentoViewBlocks?: BentoViewBlockWire[];
30
137
  profileEntityBentoTemplates?: WorkspaceYaml['profileEntityBentoTemplates'];
31
138
  // Workspace-level metadata (flattened from the `workspace` block)
32
139
  workspaceSubtype?: string;
@@ -77,6 +184,10 @@ export function toWorkspaceDefinition(
77
184
  const name = nameOverride ?? ws.name;
78
185
  const proposalId = ws.proposalId ?? `${tpl.meta.slug}-v1`;
79
186
 
187
+ // The authored `bento.blocks` union must be ROUTED, not forwarded: the pod
188
+ // splits by field, not by `kind`. See `splitBentoBlocks`.
189
+ const bento = splitBentoBlocks(tpl.bento?.blocks);
190
+
80
191
  const definition: WorkspaceDefinitionInput = {
81
192
  workspaceName: name,
82
193
  description: ws.description,
@@ -89,7 +200,8 @@ export function toWorkspaceDefinition(
89
200
  displayTemplates: tpl.displayTemplates,
90
201
  layoutConfig: tpl.layout,
91
202
  bentoViewName: tpl.bento?.viewName,
92
- bentoViewBlocks: tpl.bento?.blocks,
203
+ bentoLayout: bento.bentoLayout,
204
+ bentoViewBlocks: bento.bentoViewBlocks,
93
205
  profileEntityBentoTemplates: tpl.profileEntityBentoTemplates,
94
206
  workspaceSubtype: ws.subtype,
95
207
  workspaceType: ws.workspaceType,
@@ -114,6 +226,90 @@ export function toWorkspaceDefinition(
114
226
  return { definition, proposalId, workspaceName: name, templateId: tpl.meta.slug };
115
227
  }
116
228
 
229
+ // ─── Exhaustiveness ledger: toWorkspaceDefinition ────────────────────────────
230
+ // `WorkspaceYaml` is read by two converters and nothing forced either to be
231
+ // total — which is how `dependencies` was silently dropped here for months.
232
+ // Every key of `WorkspaceYaml` must be classified below as either FORWARDED
233
+ // onto the wire or explicitly DROPPED with a reason. Adding a field to
234
+ // `WorkspaceYaml` FAILS THE BUILD until it is classified, forcing the author
235
+ // to make the forward-or-drop decision instead of defaulting to silent loss.
236
+ //
237
+ // ⚠️ KNOW WHAT THIS LEDGER DOES NOT CATCH. It is FIELD-PRESENCE-exhaustive, not
238
+ // SEMANTICS-exhaustive. It asks "is every key classified?" — not "does the value
239
+ // reach the right sink?". `bento` was classified as forwarded, and it WAS
240
+ // forwarded: the whole discriminated union was dumped into ONE field on each
241
+ // door (`bentoViewBlocks` here, `bentoLayout` in `toPackageDefinition`). The
242
+ // ledger passed while widgets were filtered out on one door and views became
243
+ // `widgetType: undefined` widgets on the other. A ledger entry is a CLAIM about
244
+ // routing; only a runtime assertion can check the claim.
245
+ // → The counterpart guard for that class is `scripts/check-converters.mjs`,
246
+ // which RUNS both converters over all templates and asserts the per-kind
247
+ // block counts survive the split. Any future union-shaped field (one authored
248
+ // array fanning out to N wire fields) needs an assertion there too — do not
249
+ // let a ledger comment stand in for one.
250
+
251
+ /** Keys `toWorkspaceDefinition` reads and carries onto the wire. */
252
+ type WorkspaceDefinitionForwarded =
253
+ | 'meta' // → proposalId (meta.slug) + templateId; not a wire field itself
254
+ | 'workspace' // flattened into workspaceName/description/workspaceType/…
255
+ | 'profiles'
256
+ | 'views'
257
+ | 'entityLinks'
258
+ | 'seedEntities'
259
+ | 'suggestedRelations'
260
+ | 'displayTemplates'
261
+ | 'layout' // → layoutConfig
262
+ | 'bento' // → bentoViewName + bentoLayout (widgets) + bentoViewBlocks (views)
263
+ | 'profileEntityBentoTemplates'
264
+ | 'onboarding'
265
+ | 'dependencies';
266
+
267
+ /**
268
+ * Keys deliberately NOT forwarded by `toWorkspaceDefinition`. Each needs a
269
+ * one-line WHY. "The door has no such field" = forwarding is a no-op; anything
270
+ * else is a real gap and should say so.
271
+ */
272
+ type WorkspaceDefinitionDropped =
273
+ // UI-only: `workspaces.createFromDefinition` has no `app` field. Consumers
274
+ // read `template.app` straight off the YAML to pick the first screen, so this
275
+ // never belonged on the wire.
276
+ | 'app'
277
+ // The door DOES accept `extends`, but no template declares it today (0/21).
278
+ // Forwarding is a no-op until one does — wire it then.
279
+ | 'extends'
280
+ // The door DOES accept `automations`, but no template declares it today
281
+ // (0/21), and its zod shape ({trigger,action:{playbook_run}}) is not
282
+ // `TemplateAutomation`. Needs a mapper if a template ever uses it.
283
+ // The browser install path installs automations via its own createAutomation door.
284
+ | 'automations'
285
+ // The door has no `integrations` field. Its `tools` field is a DIFFERENT
286
+ // shape ({name,kind,inputSchema} vs {templateKey,params,mode}). Capability
287
+ // templates are seeded via the packages/apply path, not createFromDefinition
288
+ // — see the note on `TemplateIntegration` in types.ts. Forwarding = no-op.
289
+ | 'integrations'
290
+ // REAL GAP (agent-fleet, project-management declare playbooks). The door
291
+ // accepts `playbooks`, but the shapes disagree: template param `type` allows
292
+ // 'text'|'entity'|'choice', the tRPC enum accepts only string|number|boolean
293
+ // (and defaultValue↔default, grants: string[]↔{kind,ref}[]). Raw forwarding =
294
+ // zod BAD_REQUEST. Needs a param mapper — separate wave, not a silent drop.
295
+ | 'playbooks';
296
+
297
+ type UnclassifiedWorkspaceDefinitionKey = Exclude<
298
+ keyof WorkspaceYaml,
299
+ WorkspaceDefinitionForwarded | WorkspaceDefinitionDropped
300
+ >;
301
+
302
+ // If this errors, a new `WorkspaceYaml` field is unclassified. Forward it in
303
+ // `toWorkspaceDefinition` and add it to `WorkspaceDefinitionForwarded`, or add
304
+ // it to `WorkspaceDefinitionDropped` with a one-line WHY.
305
+ const _workspaceDefinitionLedger: [UnclassifiedWorkspaceDefinitionKey] extends [never]
306
+ ? true
307
+ : [
308
+ 'UNCLASSIFIED YAML FIELD — forward it or add it to WorkspaceDefinitionDropped:',
309
+ UnclassifiedWorkspaceDefinitionKey,
310
+ ] = true;
311
+ void _workspaceDefinitionLedger;
312
+
117
313
  /**
118
314
  * The unified `PackageDefinition` shape consumed by `POST /api/hub/packages/apply`
119
315
  * (CLI `synap launch`) and the CP package registry seeder. It composes the
@@ -145,7 +341,17 @@ export interface PackageDefinitionOutput {
145
341
  suggestedRelations?: WorkspaceYaml['suggestedRelations'];
146
342
  displayTemplates?: WorkspaceYaml['displayTemplates'];
147
343
  entityLinks?: WorkspaceYaml['entityLinks'];
148
- bentoLayout?: NonNullable<WorkspaceYaml['bento']>['blocks'];
344
+ /** WIDGET half of the authored bento union — mapped by the pod via `widgetType`. */
345
+ bentoLayout?: BentoWidgetBlockWire[];
346
+ /**
347
+ * VIEW half of the authored bento union. Widgets go to `bentoLayout`, views
348
+ * here — the pod splits by field (create-workspace-from-definition.ts:1373
349
+ * reads `bentoViewBlocks`), so both wire fields must be carried. The apply
350
+ * door's `PackageApplySchema` now declares this field (it is a plain
351
+ * `z.object`, so an undeclared field would be silently stripped) — the inbound
352
+ * seam is closed to match this outbound emit.
353
+ */
354
+ bentoViewBlocks?: BentoViewBlockWire[];
149
355
  bentoViewName?: string;
150
356
  profileEntityBentoTemplates?: WorkspaceYaml['profileEntityBentoTemplates'];
151
357
  layoutConfig?: WorkspaceYaml['layout'];
@@ -173,6 +379,9 @@ export function toPackageDefinition(
173
379
  }
174
380
 
175
381
  const ws = tpl.workspace;
382
+ // Same routing as the tRPC door — the union is split by `kind`, never dumped
383
+ // whole into one field. See `splitBentoBlocks`.
384
+ const bento = splitBentoBlocks(tpl.bento?.blocks);
176
385
  return {
177
386
  _meta: {
178
387
  slug: tpl.meta.slug,
@@ -195,7 +404,8 @@ export function toPackageDefinition(
195
404
  suggestedRelations: tpl.suggestedRelations,
196
405
  displayTemplates: tpl.displayTemplates,
197
406
  entityLinks: tpl.entityLinks,
198
- bentoLayout: tpl.bento?.blocks,
407
+ bentoLayout: bento.bentoLayout,
408
+ bentoViewBlocks: bento.bentoViewBlocks,
199
409
  bentoViewName: tpl.bento?.viewName,
200
410
  profileEntityBentoTemplates: tpl.profileEntityBentoTemplates,
201
411
  layoutConfig: tpl.layout,
@@ -205,3 +415,56 @@ export function toPackageDefinition(
205
415
  dependencies: tpl.dependencies,
206
416
  };
207
417
  }
418
+
419
+ // ─── Exhaustiveness ledger: toPackageDefinition ──────────────────────────────
420
+ // Same forcing function as above, for the OTHER converter. These two are NOT
421
+ // merged on purpose: they target different endpoints with different contracts
422
+ // (`PackageDefinition` is a superset with renames — seedEntities→suggestedEntities,
423
+ // bento.blocks→bentoLayout, integrations→capabilities, plus `_meta`). Keeping
424
+ // two converters is fine; keeping either one lossy-by-accident is not.
425
+
426
+ /** Keys `toPackageDefinition` reads and carries onto the wire. */
427
+ type PackageDefinitionForwarded =
428
+ | 'meta' // → _meta
429
+ | 'workspace' // flattened into workspaceName/description/workspaceType/…
430
+ | 'profiles'
431
+ | 'views'
432
+ | 'seedEntities' // → suggestedEntities
433
+ | 'suggestedRelations'
434
+ | 'displayTemplates'
435
+ | 'entityLinks'
436
+ | 'bento' // → bentoViewName + bentoLayout (widgets) + bentoViewBlocks (views)
437
+ | 'profileEntityBentoTemplates'
438
+ | 'layout' // → layoutConfig
439
+ | 'integrations' // → capabilities
440
+ | 'playbooks'
441
+ | 'onboarding'
442
+ | 'dependencies';
443
+
444
+ /** Keys deliberately NOT forwarded by `toPackageDefinition`. */
445
+ type PackageDefinitionDropped =
446
+ // UI-only, same as the definition door: `PackageDefinition` has no `app`.
447
+ | 'app'
448
+ // REAL GAP: backend `PackageDefinition` DOES declare `extends`
449
+ // (package-definition.ts:262) and `automations` (:268), but
450
+ // `PackageDefinitionOutput` omits both. No template declares either today
451
+ // (0/21), so this is latent, not live — wire them when a template first uses
452
+ // one.
453
+ | 'extends'
454
+ | 'automations';
455
+
456
+ type UnclassifiedPackageDefinitionKey = Exclude<
457
+ keyof WorkspaceYaml,
458
+ PackageDefinitionForwarded | PackageDefinitionDropped
459
+ >;
460
+
461
+ // If this errors, a new `WorkspaceYaml` field is unclassified. Forward it in
462
+ // `toPackageDefinition` and add it to `PackageDefinitionForwarded`, or add it
463
+ // to `PackageDefinitionDropped` with a one-line WHY.
464
+ const _packageDefinitionLedger: [UnclassifiedPackageDefinitionKey] extends [never]
465
+ ? true
466
+ : [
467
+ 'UNCLASSIFIED YAML FIELD — forward it or add it to PackageDefinitionDropped:',
468
+ UnclassifiedPackageDefinitionKey,
469
+ ] = true;
470
+ void _packageDefinitionLedger;
@@ -0,0 +1,183 @@
1
+ # Enterprise OS — the public operating system suite
2
+ # ==================================================
3
+ # A SUITE template: it stands up a whole company's public operating core in one
4
+ # install by REQUIRING its constituent workspaces (deps-first), then adds a thin
5
+ # "command tower" home of its own — a place to track the company's objectives
6
+ # across the lenses it just installed.
7
+ #
8
+ # What it composes (all public, all bundled):
9
+ # • foundation — strategic DNA (mission, audience, positioning, principles)
10
+ # • ecosystem — the market graph (partners, competitors, segments, trends)
11
+ # • operations — how the company runs (the operational lens)
12
+ # • crm — the pipeline (contacts, leads, clients, deals)
13
+ #
14
+ # It uses `require` (not `compose`): each constituent is a REAL workspace, not an
15
+ # overlay. Deps-first resolution installs foundation first (the SSOT role
16
+ # vocabulary provider), so every downstream lens reuses foundation's pod-wide
17
+ # roles rather than forking twins. Installing Enterprise OS a second time is
18
+ # idempotent — the resolver reuses existing constituent workspaces by slug.
19
+
20
+ meta:
21
+ slug: enterprise-os
22
+ name: Enterprise OS
23
+ description: Stand up a company's public operating core in one install — strategy, market, operations, and pipeline — plus a command tower to track objectives across them.
24
+ icon: layout-dashboard
25
+ color: "#4F46E5"
26
+ tags: [suite, enterprise, os, foundation, crm, operations, ecosystem]
27
+ isPublic: true
28
+
29
+ # Deps-first: foundation is the pod-wide role-vocabulary provider, so it must
30
+ # resolve before ecosystem/operations/crm reuse its shared roles. `require` (not
31
+ # `compose`): each is a standalone workspace, not an additive overlay.
32
+ dependencies:
33
+ - slug: foundation
34
+ kind: workspace
35
+ relation: require
36
+ reason: "Strategic DNA + the pod-wide role vocabulary (partner/client/lead) every other lens reuses. Must resolve first."
37
+ - slug: ecosystem
38
+ kind: workspace
39
+ relation: require
40
+ reason: "The market graph — the players, segments, and trends the company operates around."
41
+ - slug: operations
42
+ kind: workspace
43
+ relation: require
44
+ reason: "How the company runs — the operational lens."
45
+ - slug: crm
46
+ kind: workspace
47
+ relation: require
48
+ reason: "The pipeline — contacts, leads, clients, and deals."
49
+
50
+ workspace:
51
+ name: Enterprise OS
52
+ description: The command tower for the company operating system — track strategic objectives across the strategy, market, operations, and pipeline lenses this suite installs.
53
+ subtype: enterprise-os
54
+ visibility: pod_visible
55
+ sourceRoles:
56
+ strategy: consumer
57
+
58
+ profiles:
59
+ # The one native profile the command tower owns: a strategic objective the
60
+ # company is driving across the lenses this suite installs. Deliberately light
61
+ # — the depth lives in the constituent workspaces; this is the cross-cutting
62
+ # scoreboard that ties them together.
63
+ - slug: objective
64
+ displayName: Objective
65
+ icon: target
66
+ color: "#4F46E5"
67
+ description: A company objective the operating system is driving toward — the cross-cutting outcome the lenses serve.
68
+ properties:
69
+ - slug: outcome
70
+ label: Outcome
71
+ valueType: string
72
+ inputType: textarea
73
+ - slug: status
74
+ label: Status
75
+ valueType: string
76
+ inputType: select
77
+ enumValues: [on-track, at-risk, off-track, achieved]
78
+ constraints: { defaultValue: on-track }
79
+ - slug: horizon
80
+ label: Horizon
81
+ valueType: string
82
+ inputType: select
83
+ enumValues: [now, next, later]
84
+ constraints: { defaultValue: now }
85
+ - slug: owner
86
+ label: Owner
87
+ valueType: string
88
+ inputType: text
89
+ - slug: targetDate
90
+ label: Target Date
91
+ valueType: date
92
+ inputType: date
93
+
94
+ views:
95
+ - name: Objectives
96
+ slug: objectives
97
+ type: table
98
+ scopeProfileSlug: objective
99
+ description: The company's strategic objectives across every lens, by horizon and status.
100
+ defaultView: true
101
+ config:
102
+ columns: [title, status, horizon, owner, targetDate]
103
+ - name: Objective Board
104
+ slug: objective-board
105
+ type: kanban
106
+ scopeProfileSlug: objective
107
+ description: Objectives grouped by status — a running scoreboard for the operating system.
108
+ config:
109
+ groupBy: status
110
+
111
+ onboarding:
112
+ goal: >
113
+ Stand up the company's operating core and name the handful of strategic
114
+ objectives it is driving — so strategy, market, operations, and pipeline are
115
+ all in place and pointed at the same outcomes from day one.
116
+ framing: |
117
+ THE CHIEF OF STAFF: Act as a sharp chief of staff standing up the company OS.
118
+ The four lenses (foundation, ecosystem, operations, CRM) are already
119
+ installed — your job is to name the 3-5 objectives that cut across them, so
120
+ the operating system has a scoreboard, not just structure.
121
+ expertise:
122
+ starters:
123
+ - >
124
+ Name few, real objectives. An operating system with 20 objectives has
125
+ none; force the 3-5 that actually decide the year, each with an owner and
126
+ a horizon (now / next / later).
127
+ - >
128
+ Tie each objective to a lens. A good objective names where it lives —
129
+ pipeline growth (CRM), a market move (ecosystem), an operational fix
130
+ (operations), or a positioning shift (foundation) — so the scoreboard
131
+ maps to the structure underneath it.
132
+ blindSpots:
133
+ - >
134
+ Objectives with no owner and no date are wishes. Every one needs a single
135
+ owner and a target date, or it will not move.
136
+ - >
137
+ "Grow the business" is not an objective — it is a category. Push for the
138
+ specific outcome that would make this quarter a win.
139
+ bar: >
140
+ A short, honest scoreboard: 3-5 objectives, each owned, dated, placed on a
141
+ horizon, and tied to the lens that serves it — sitting on top of a fully
142
+ installed operating core.
143
+ collect:
144
+ - profileSlug: objective
145
+ what: The 3-5 strategic objectives the company is driving this horizon.
146
+ cardinality: few
147
+ keyFields: [status, horizon, owner]
148
+ openingQuestions:
149
+ - "What are the 3-5 objectives that will decide this year for the company?"
150
+ - "Who owns each one, and is it a now, next, or later horizon?"
151
+ doneWhen: A few objectives captured, each with an owner and a horizon.
152
+
153
+ app:
154
+ kind: stored
155
+ cellKey: entity-detail
156
+
157
+ layout:
158
+ defaultView: Objectives
159
+ defaultApp: null
160
+ pinnedApps: [dashboard, data]
161
+ sidebarItems:
162
+ - kind: view
163
+ viewName: Objectives
164
+ label: Objectives
165
+ icon: target
166
+ - kind: view
167
+ viewName: Objective Board
168
+ label: Board
169
+ icon: kanban-square
170
+ - kind: app
171
+ appId: data
172
+ label: Data
173
+ icon: database
174
+
175
+ bento:
176
+ viewName: Objectives
177
+ blocks:
178
+ - kind: view
179
+ viewName: Objectives
180
+ pos: { x: 0, y: 0, w: 8, h: 4 }
181
+ - kind: view
182
+ viewName: Objective Board
183
+ pos: { x: 8, y: 0, w: 4, h: 4 }
package/src/index.ts CHANGED
@@ -37,6 +37,7 @@ export type {
37
37
  TemplateAutomation,
38
38
  TemplateExtendsRef,
39
39
  TemplateDependency,
40
+ PackageDependencyKind,
40
41
  TemplateProfileEntityBentoTemplates,
41
42
  TemplateDefaultSource,
42
43
  TemplateSourceRole,
@@ -46,6 +47,12 @@ export type {
46
47
  TemplatePlaybookParam,
47
48
  } from "./types.js";
48
49
 
50
+ // The dependency-kind vocabulary as a VALUE — so consumers (marketplace,
51
+ // onboarding, backend, hub zod, browser, CP zod) can `z.enum(PACKAGE_DEPENDENCY_KINDS)`
52
+ // / derive their type from it instead of hand-copying the list. Six mirrors
53
+ // exist today; this is the door that lets them stop being mirrors.
54
+ export { PACKAGE_DEPENDENCY_KINDS } from "./types.js";
55
+
49
56
  export {
50
57
  WORKSPACE_TEMPLATES,
51
58
  getWorkspaceTemplate,
@@ -55,9 +62,28 @@ export {
55
62
 
56
63
  export type { WorkspaceTemplateSlug } from "./templates.js";
57
64
 
58
- export { toWorkspaceDefinition, toPackageDefinition } from "./define.js";
65
+ export { mergeCatalog } from "./catalog.js";
66
+ export type {
67
+ MergeCatalogInput,
68
+ MergeCatalogResult,
69
+ BundledEntry,
70
+ RemoteEntry,
71
+ RemoteSourceStatus,
72
+ CatalogEntry,
73
+ BundledCatalogEntry,
74
+ RemoteCatalogEntry,
75
+ } from "./catalog.js";
76
+
77
+ export {
78
+ toWorkspaceDefinition,
79
+ toPackageDefinition,
80
+ splitBentoBlocks,
81
+ } from "./define.js";
59
82
  export type {
60
83
  WorkspaceDefinitionInput,
61
84
  ToDefinitionResult,
62
85
  PackageDefinitionOutput,
86
+ SplitBentoBlocks,
87
+ BentoWidgetBlockWire,
88
+ BentoViewBlockWire,
63
89
  } from "./define.js";
@@ -146,16 +146,25 @@ profiles:
146
146
  # foundation already owns (e.g. `leadStage`) with a divergent enum/default —
147
147
  # that is base drift, and the SSOT tripwire fails on it.
148
148
  # (Standards reference inherited base props like `leadStage` directly.)
149
+ #
150
+ # The same reasoning applies to the ROW-LEVEL fields, and more strictly: the
151
+ # row has no overlay mechanism, so this template must not restate what
152
+ # foundation owns. `description`, `semanticSlug` and `applicableKinds` are
153
+ # therefore OMITTED here — they are written only on the create path, which
154
+ # foundation always takes, so declaring them here asserted a value that never
155
+ # applied (this template claimed `applicableKinds: [person]` while the live row
156
+ # says [company, person]).
157
+ # `icon`/`color` are the exception that must be RESTATED rather than dropped:
158
+ # they are also read into `profileHintsMap` to build THIS workspace's auto-bento
159
+ # for `lead`, so omitting them regresses the widgets to a default indigo
160
+ # database icon. They must MATCH foundation's row (`user-plus`/`yellow`) — the
161
+ # old `green` rendered green widgets around a yellow row.
149
162
  - slug: lead
150
163
  displayName: Lead
151
164
  icon: user-plus
152
- color: green
153
- description: "A potential customer or partner contact in your pipeline"
154
- semanticSlug: marketing.lead
165
+ color: yellow
155
166
  scope: shared
156
167
  profileKind: role
157
- applicableKinds:
158
- - person
159
168
  properties:
160
169
  - slug: lead-source
161
170
  label: Source