@elevasis/sdk 1.4.0 → 1.5.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.
Files changed (31) hide show
  1. package/dist/cli.cjs +75 -8
  2. package/dist/index.d.ts +1464 -749
  3. package/dist/index.js +74 -7
  4. package/dist/types/worker/adapters/crm.d.ts +20 -0
  5. package/dist/types/worker/adapters/index.d.ts +2 -0
  6. package/dist/types/worker/adapters/projects.d.ts +20 -0
  7. package/dist/worker/index.js +41 -1
  8. package/package.json +2 -2
  9. package/reference/_navigation.md +24 -0
  10. package/reference/deployment/provided-features.mdx +64 -25
  11. package/reference/framework/index.mdx +2 -2
  12. package/reference/framework/project-structure.mdx +10 -8
  13. package/reference/index.mdx +3 -3
  14. package/reference/packages/core/src/organization-model/README.md +19 -4
  15. package/reference/resources/patterns.mdx +54 -8
  16. package/reference/scaffold/core/organization-graph.mdx +262 -0
  17. package/reference/scaffold/core/organization-model.mdx +257 -0
  18. package/reference/scaffold/index.mdx +59 -0
  19. package/reference/scaffold/operations/workflow-recipes.md +419 -0
  20. package/reference/scaffold/recipes/add-a-feature.md +142 -0
  21. package/reference/scaffold/recipes/add-a-resource.md +163 -0
  22. package/reference/scaffold/recipes/gate-by-feature-or-admin.md +152 -0
  23. package/reference/scaffold/recipes/index.md +32 -0
  24. package/reference/scaffold/reference/contracts.md +1044 -0
  25. package/reference/scaffold/reference/feature-registry.md +30 -0
  26. package/reference/scaffold/reference/glossary.md +88 -0
  27. package/reference/scaffold/ui/composition-extensibility.mdx +216 -0
  28. package/reference/scaffold/ui/customization.md +239 -0
  29. package/reference/scaffold/ui/feature-flags-and-gating.md +265 -0
  30. package/reference/scaffold/ui/feature-shell.mdx +241 -0
  31. package/reference/scaffold/ui/recipes.md +418 -0
@@ -0,0 +1,419 @@
1
+ ---
2
+ title: Workflow Recipes
3
+ description: Anatomy of a workflow, adapter usage, and trigger patterns -- runnable email-notification example replaces the trivial echo workflow. Resolves eval score 2/5 on workflow authoring.
4
+ ---
5
+
6
+ # Workflow Recipes
7
+
8
+ `🟢 Stable` -- Use these patterns as-is against `@elevasis/sdk ^1.4.0`.
9
+
10
+ ---
11
+
12
+ ## 1. Anatomy of a Workflow
13
+
14
+ Every workflow is a `WorkflowDefinition` object with four top-level keys: `config`, `contract`, `steps`, and `entryPoint`. The `email-notification` workflow at `operations/src/email-notification/index.ts` is used as the running example throughout this section.
15
+
16
+ ### Config
17
+
18
+ ```typescript
19
+ config: {
20
+ resourceId: 'email-notification', // Unique ID -- used in API calls and CLI
21
+ name: 'Email Notification', // Human-readable display name
22
+ type: 'workflow', // Always 'workflow' (vs 'agent')
23
+ description: 'Sends a notification email to a user...',
24
+ version: '1.0.0', // Bump on contract changes
25
+ status: 'dev' // 'dev' or 'prod'
26
+ }
27
+ ```
28
+
29
+ - `resourceId` is the stable identifier used by `pnpm exec elevasis exec` and `/execute` API calls.
30
+ - Bump `version` whenever you change `contract.inputSchema` or `contract.outputSchema`.
31
+
32
+ ### Contract
33
+
34
+ ```typescript
35
+ // foundations/types/index.ts -- shared with frontend
36
+ export const emailNotificationInputSchema = z.object({
37
+ recipientEmail: z.string().email(),
38
+ recipientName: z.string().min(1),
39
+ subject: z.string().min(1).max(200),
40
+ body: z.string().min(1).max(10000),
41
+ category: z.string().min(1).default('operations'),
42
+ actionUrl: z.string().url().optional()
43
+ })
44
+
45
+ export const emailNotificationOutputSchema = z.object({
46
+ delivered: z.boolean(),
47
+ notificationId: z.string().optional(),
48
+ summary: z.string()
49
+ })
50
+
51
+ export type EmailNotificationInput = z.infer<typeof emailNotificationInputSchema>
52
+ export type EmailNotificationOutput = z.infer<typeof emailNotificationOutputSchema>
53
+ ```
54
+
55
+ ```typescript
56
+ // operations/src/email-notification/index.ts
57
+ import { emailNotificationInputSchema, emailNotificationOutputSchema } from '@foundation/types'
58
+
59
+ contract: {
60
+ inputSchema: emailNotificationInputSchema,
61
+ outputSchema: emailNotificationOutputSchema
62
+ }
63
+ ```
64
+
65
+ **Rules:**
66
+
67
+ - Define schemas in `foundations/types/index.ts` -- never inline Zod schemas in workflow files.
68
+ - Both runtimes (frontend and platform) import from `@foundation/types`, keeping validation consistent.
69
+ - `@foundation/types` resolves to `foundations/types/index.ts` via the tsconfig path alias.
70
+
71
+ ### Steps
72
+
73
+ Each step is a `WorkflowStep` with: `id`, `name`, `description`, `handler`, `inputSchema`, `outputSchema`, and `next`.
74
+
75
+ ```typescript
76
+ import { StepType } from '@elevasis/sdk'
77
+
78
+ steps: {
79
+ validate: {
80
+ id: 'validate',
81
+ name: 'Validate Input',
82
+ description: 'Validates recipient and content parameters before sending',
83
+ handler: async (rawInput, context) => {
84
+ const input = rawInput as EmailNotificationInput
85
+ context.logger.info(`[validate] Checking recipient: ${input.recipientEmail}`)
86
+ // ... validation logic ...
87
+ return input // Pass validated data to next step
88
+ },
89
+ inputSchema: emailNotificationInputSchema,
90
+ outputSchema: emailNotificationInputSchema, // Passes full input through to next step
91
+ next: { type: StepType.LINEAR, target: 'notify' }
92
+ },
93
+ notify: {
94
+ id: 'notify',
95
+ name: 'Send Notification',
96
+ handler: async (rawInput, context) => {
97
+ // ...
98
+ return { delivered: true, summary: '...' }
99
+ },
100
+ inputSchema: emailNotificationInputSchema,
101
+ outputSchema: emailNotificationOutputSchema,
102
+ next: null // Terminal step -- ends the workflow
103
+ }
104
+ }
105
+ ```
106
+
107
+ **Key rules:**
108
+
109
+ - `next: null` marks a terminal step.
110
+ - `next: { type: StepType.LINEAR, target: 'stepId' }` chains to another step.
111
+ - Use `context.logger` -- never `console.log`. Platform captures `context.logger.*` only.
112
+ - Cast `rawInput` to your input type: `const input = rawInput as EmailNotificationInput`.
113
+
114
+ ### Entrypoint
115
+
116
+ ```typescript
117
+ entryPoint: 'validate' // Must match a step id in the steps map
118
+ ```
119
+
120
+ The platform starts execution here. For single-step workflows, `entryPoint` points to the only step (e.g., `echo` points to `'echo'`).
121
+
122
+ ### Optional: Interface Form
123
+
124
+ Declare `interface.form` to auto-generate an execution form in AI Studio and Command Center:
125
+
126
+ ```typescript
127
+ interface: {
128
+ form: {
129
+ title: 'Send Email Notification',
130
+ description: 'Sends a notification email to a user.',
131
+ fields: [
132
+ { name: 'recipientEmail', label: 'Recipient Email', type: 'text', required: true },
133
+ { name: 'body', label: 'Body', type: 'text', required: true }
134
+ ],
135
+ submitButton: { label: 'Send notification', loadingLabel: 'Sending...' }
136
+ }
137
+ }
138
+ ```
139
+
140
+ ---
141
+
142
+ ## 2. Adapter Usage
143
+
144
+ Adapters are typed wrappers over `platform.call()`. Import singletons from `@elevasis/sdk/worker`.
145
+
146
+ ### notifications
147
+
148
+ Send a platform notification to the current user. `userId` and `organizationId` are injected server-side -- you never supply them.
149
+
150
+ ```typescript
151
+ import { notifications } from '@elevasis/sdk/worker'
152
+
153
+ await notifications.create({
154
+ category: 'operations', // Any string: 'operations', 'delivery', 'acquisition', etc.
155
+ title: 'Workflow complete',
156
+ message: 'Your email notification was sent successfully.',
157
+ actionUrl: '/operations' // Optional -- link the user can follow
158
+ })
159
+ ```
160
+
161
+ Available methods: `create`.
162
+
163
+ ### llm
164
+
165
+ Generate text or structured output using a language model.
166
+
167
+ ```typescript
168
+ import { llm } from '@elevasis/sdk/worker'
169
+
170
+ const response = await llm.generate({
171
+ provider: 'anthropic', // 'anthropic' | 'openai' | 'openrouter' | 'google'
172
+ model: 'claude-sonnet-4-5',
173
+ messages: [
174
+ { role: 'user', content: 'Summarize this email body: ' + input.body }
175
+ ]
176
+ })
177
+
178
+ const summary = response.output as string
179
+ ```
180
+
181
+ For structured output, pass a JSON Schema as `responseSchema`:
182
+
183
+ ```typescript
184
+ const response = await llm.generate({
185
+ provider: 'anthropic',
186
+ model: 'claude-sonnet-4-5',
187
+ messages: [{ role: 'user', content: 'Extract the key action from: ' + input.body }],
188
+ responseSchema: {
189
+ type: 'object',
190
+ properties: { action: { type: 'string' }, urgency: { type: 'string' } }
191
+ }
192
+ })
193
+ const { action, urgency } = response.output as { action: string; urgency: string }
194
+ ```
195
+
196
+ Available methods: `generate`.
197
+
198
+ ### storage
199
+
200
+ Upload and retrieve files scoped to the organization. All paths are automatically prefixed with the organization's storage prefix server-side.
201
+
202
+ ```typescript
203
+ import { storage } from '@elevasis/sdk/worker'
204
+
205
+ // Upload
206
+ await storage.upload({
207
+ bucket: 'operations',
208
+ path: 'email-logs/2026-04-16/batch-01.json',
209
+ content: Buffer.from(JSON.stringify(logData)).toString('base64'),
210
+ contentType: 'application/json'
211
+ })
212
+
213
+ // Download
214
+ const file = await storage.download({
215
+ bucket: 'operations',
216
+ path: 'email-logs/2026-04-16/batch-01.json'
217
+ })
218
+
219
+ // Signed URL for frontend access
220
+ const { signedUrl } = await storage.createSignedUrl({
221
+ bucket: 'operations',
222
+ path: 'email-logs/2026-04-16/batch-01.json',
223
+ expiresIn: 3600
224
+ })
225
+ ```
226
+
227
+ Available methods: `upload`, `download`, `createSignedUrl`, `delete`, `list`.
228
+
229
+ ### scheduler
230
+
231
+ Schedule future or recurring workflow executions.
232
+
233
+ ```typescript
234
+ import { scheduler } from '@elevasis/sdk/worker'
235
+
236
+ const schedule = await scheduler.createSchedule({
237
+ name: 'Daily email digest',
238
+ target: { resourceType: 'workflow', resourceId: 'email-notification' },
239
+ scheduleConfig: {
240
+ type: 'cron',
241
+ expression: '0 9 * * 1-5' // 9am weekdays
242
+ }
243
+ })
244
+
245
+ context.logger.info(`[schedule] Created schedule: ${schedule.id}`)
246
+ ```
247
+
248
+ Available methods: `createSchedule`, `updateAnchor`, `deleteSchedule`, `getSchedule`, `listSchedules`, `cancelSchedule`, `cancelSchedulesByMetadata`, `cancelScheduleByIdempotencyKey`, `findByIdempotencyKey`, `deleteScheduleByIdempotencyKey`.
249
+
250
+ **Note on other adapters:** Integration adapters (`createResendAdapter`, `createAttioAdapter`, etc.) follow a factory pattern -- bind a credential once, use the instance for all calls:
251
+
252
+ ```typescript
253
+ import { createResendAdapter } from '@elevasis/sdk/worker'
254
+
255
+ const resend = createResendAdapter('my-resend-credential')
256
+ await resend.sendEmail({
257
+ to: input.recipientEmail,
258
+ subject: input.subject,
259
+ html: `<p>${input.body}</p>`
260
+ })
261
+ ```
262
+
263
+ See `operations/node_modules/@elevasis/sdk/reference/` for the full adapter reference.
264
+
265
+ ---
266
+
267
+ ## 3. Trigger Patterns from Frontend
268
+
269
+ ### (a) API call via `useApiClient`
270
+
271
+ Use this pattern in React components and hooks. `apiRequest` automatically attaches the auth token and org context.
272
+
273
+ ```typescript
274
+ // ui/src/features/notifications/hooks/useSendEmailNotification.ts
275
+ import { useMutation } from '@tanstack/react-query'
276
+ import { useApiClient } from '@/shared/api'
277
+ import type { EmailNotificationInput, EmailNotificationOutput } from '@foundation/types'
278
+
279
+ export function useSendEmailNotification() {
280
+ const { apiRequest } = useApiClient()
281
+
282
+ return useMutation({
283
+ mutationFn: async (input: EmailNotificationInput) => {
284
+ return apiRequest<EmailNotificationOutput>('/execute', {
285
+ method: 'POST',
286
+ body: JSON.stringify({
287
+ resourceType: 'workflow',
288
+ resourceId: 'email-notification',
289
+ input
290
+ })
291
+ })
292
+ }
293
+ })
294
+ }
295
+ ```
296
+
297
+ Usage in a component:
298
+
299
+ ```tsx
300
+ // ui/src/features/notifications/components/SendNotificationButton.tsx
301
+ import { useSendEmailNotification } from '../hooks/useSendEmailNotification'
302
+
303
+ function SendNotificationButton() {
304
+ const { mutate, isPending, isSuccess } = useSendEmailNotification()
305
+
306
+ return (
307
+ <Button
308
+ loading={isPending}
309
+ onClick={() =>
310
+ mutate({
311
+ recipientEmail: 'user@example.com',
312
+ recipientName: 'Jane Smith',
313
+ subject: 'Your request is ready',
314
+ body: 'Hi Jane, your workflow has completed.',
315
+ category: 'operations'
316
+ })
317
+ }
318
+ >
319
+ Send Notification
320
+ </Button>
321
+ )
322
+ }
323
+ ```
324
+
325
+ For async execution (long-running workflows), use `/execute-async` instead:
326
+
327
+ ```typescript
328
+ return apiRequest<{ executionId: string }>('/execute-async', {
329
+ method: 'POST',
330
+ body: JSON.stringify({
331
+ resourceType: 'workflow',
332
+ resourceId: 'email-notification',
333
+ input
334
+ })
335
+ })
336
+ // Poll /executions/email-notification/:executionId for status
337
+ ```
338
+
339
+ ### (b) Direct SDK dispatch (CLI or scripts)
340
+
341
+ From the project root, use the platform CLI for manual invocations, testing, and scripting:
342
+
343
+ ```bash
344
+ # Describe the schema before executing
345
+ pnpm exec elevasis describe Elevasis/email-notification
346
+
347
+ # Execute synchronously
348
+ pnpm exec elevasis exec Elevasis/email-notification --input '{
349
+ "recipientEmail": "user@example.com",
350
+ "recipientName": "Jane Smith",
351
+ "subject": "Hello",
352
+ "body": "Hi Jane, this is a test notification."
353
+ }'
354
+
355
+ # Execute asynchronously (for long-running workflows)
356
+ pnpm exec elevasis exec Elevasis/email-notification --async --input '{...}'
357
+
358
+ # View a specific execution
359
+ pnpm exec elevasis execution Elevasis/email-notification <executionId>
360
+ ```
361
+
362
+ The `--prod` flag targets `https://api.elevasis.io` and goes **before** the command:
363
+
364
+ ```bash
365
+ pnpm exec elevasis --prod exec Elevasis/email-notification --input '{...}'
366
+ ```
367
+
368
+ ---
369
+
370
+ ## 4. Registry Pattern
371
+
372
+ Workflows are discovered through `operations/src/index.ts`, which exports a `DeploymentSpec` as its default export.
373
+
374
+ **Pattern:** each feature group in `operations/src/` has its own exports barrel. The top-level spec spreads all groups:
375
+
376
+ ```
377
+ operations/src/
378
+ index.ts # Top-level DeploymentSpec -- never add workflows here directly
379
+ example/
380
+ index.ts # export const workflows = [echo]; export const agents = []
381
+ echo.ts # WorkflowDefinition for 'echo'
382
+ email-notification/
383
+ exports.ts # export const workflows = [emailNotification]; export const agents = []
384
+ index.ts # WorkflowDefinition for 'email-notification'
385
+ ```
386
+
387
+ Top-level registry (`operations/src/index.ts`):
388
+
389
+ ```typescript
390
+ import type { DeploymentSpec } from '@elevasis/sdk'
391
+ import * as example from './example/index.js'
392
+ import * as emailNotification from './email-notification/exports.js'
393
+
394
+ const org: DeploymentSpec = {
395
+ version: '0.1.0',
396
+ workflows: [...example.workflows, ...emailNotification.workflows],
397
+ agents: [...example.agents, ...emailNotification.agents]
398
+ }
399
+ export default org
400
+ ```
401
+
402
+ Feature group barrel (e.g., `email-notification/exports.ts`):
403
+
404
+ ```typescript
405
+ import { emailNotification } from './index.js'
406
+ import type { WorkflowDefinition } from '@elevasis/sdk'
407
+
408
+ export const workflows: WorkflowDefinition[] = [emailNotification]
409
+ export const agents: never[] = []
410
+ ```
411
+
412
+ **Adding a new workflow:**
413
+
414
+ 1. Create `operations/src/<feature>/index.ts` with the `WorkflowDefinition`.
415
+ 2. Create `operations/src/<feature>/exports.ts` with `workflows` and `agents` arrays.
416
+ 3. Import the group barrel in `operations/src/index.ts` and spread into `workflows`/`agents`.
417
+ 4. Run `pnpm -C operations check` to validate, then `pnpm -C operations deploy` to publish.
418
+
419
+ **Note:** Use `.js` extensions in imports even though the source is TypeScript. The TypeScript compiler and esbuild bundler both require this for ESM interoperability.
@@ -0,0 +1,142 @@
1
+ ---
2
+ title: Add a Feature
3
+ description: End-to-end walkthrough for adding a new shell feature (e.g., analytics) from org-model key through manifest, routes, and gating.
4
+ ---
5
+
6
+ # Add a Feature
7
+
8
+ End-to-end: add a new shell feature such as `analytics`. Steps are sequential.
9
+
10
+ See [glossary.md](../reference/glossary.md) for term disambiguation throughout this recipe (Feature has three distinct contexts). See [contracts.md](../reference/contracts.md) for authoritative TypeScript shapes.
11
+
12
+ ---
13
+
14
+ ## 1. Decide the feature shape
15
+
16
+ A shell feature requires an `OrganizationModelFeatureKey` to gate it. You have two options:
17
+
18
+ - **Reuse an existing key** (e.g., `operations`, `monitoring`). The new feature shares an on/off toggle with other features using that key. Fine for sub-features within an existing domain.
19
+ - **Add a new platform key** -- only possible if you are extending `OrganizationModelFeatureKey` itself, which requires a core package change. For template-local keys, use `FoundationLegacyFeatureKey` instead.
20
+
21
+ For a genuinely new capability (e.g., `analytics`), you will extend `FoundationLegacyFeatureKey` in foundations so it becomes a valid `FoundationFeatureKey` and then declare it in the org model.
22
+
23
+ See [glossary.md](../reference/glossary.md) under **Feature** (three contexts) and **OrganizationModelFeatureKey**.
24
+
25
+ ---
26
+
27
+ ## 2. Update the organization model
28
+
29
+ File: `foundations/config/organization-model.ts`
30
+
31
+ Add the key to `FoundationLegacyFeatureKey`, then declare it under `features.enabled`, `features.labels`, and the `featuresEnabled` resolver map.
32
+
33
+ ```ts
34
+ // 1. Extend the local key union
35
+ type FoundationLegacyFeatureKey = 'crm' | 'lead-gen' | 'projects' | 'analytics'
36
+
37
+ // 2. Enable in defineOrganizationModel call
38
+ features: {
39
+ enabled: { ..., analytics: false }, // start disabled
40
+ labels: { ..., analytics: 'Analytics' }
41
+ }
42
+
43
+ // 3. Add to featuresEnabled resolver
44
+ const featuresEnabled: Record<FoundationFeatureKey, boolean> = {
45
+ ...
46
+ analytics: resolvedOrganizationModel.features.enabled.analytics ?? false
47
+ }
48
+ ```
49
+
50
+ Optionally, add a new domain entry and a navigation surface. See `OrganizationModelSemanticDomain` and `OrganizationModelSurface` shapes in [contracts.md](../reference/contracts.md).
51
+
52
+ ---
53
+
54
+ ## 3. Author the FeatureModule
55
+
56
+ Create `ui/src/features/analytics/manifest.ts`. Full `FeatureModule` shape is in [contracts.md](../reference/contracts.md#featuremodule) (`@elevasis/ui/provider`).
57
+
58
+ ```ts
59
+ import type { FeatureModule } from '@elevasis/ui/provider'
60
+ import { IconChartBar } from '@tabler/icons-react'
61
+ import { AnalyticsSidebar } from './sidebar'
62
+
63
+ export const analyticsManifest: FeatureModule = {
64
+ key: 'analytics',
65
+ accessFeatureKey: 'analytics', // must match a FoundationFeatureKey
66
+ navEntry: {
67
+ label: 'Analytics',
68
+ icon: IconChartBar,
69
+ link: '/analytics'
70
+ },
71
+ sidebar: AnalyticsSidebar,
72
+ subshellRoutes: ['/analytics', '/analytics/reports']
73
+ }
74
+ ```
75
+
76
+ Key fields:
77
+
78
+ - `accessFeatureKey` -- the org-model key that gates the entire feature. The provider throws at startup if this key is not declared. See [glossary.md](../reference/glossary.md) under **accessFeatureKey**.
79
+ - `sidebar` -- a component that renders the subshell sidebar. Compose from published primitives. See [customization.md](../ui/customization.md).
80
+ - `subshellRoutes` -- every path that should activate this feature's sidebar.
81
+
82
+ Register the manifest in `ui/src/routes/__root.tsx` by adding it to the `FEATURE_MANIFESTS` array passed to `ElevasisFeaturesProvider`.
83
+
84
+ ---
85
+
86
+ ## 4. Add routes under the subshell
87
+
88
+ Create the TanStack Router layout and child routes.
89
+
90
+ Layout file (`ui/src/routes/analytics.tsx`) owns the subshell guard and renders `<Outlet />`:
91
+
92
+ ```tsx
93
+ import { createFileRoute, Outlet } from '@tanstack/react-router'
94
+ import { ProtectedRoute } from '@/features/auth'
95
+ import { FeatureGuard } from '@/features/auth/guards/FeatureGuard'
96
+
97
+ export const Route = createFileRoute('/analytics')({
98
+ component: AnalyticsLayout
99
+ })
100
+
101
+ function AnalyticsLayout() {
102
+ return (
103
+ <ProtectedRoute>
104
+ <FeatureGuard featureKey="analytics">
105
+ <Outlet />
106
+ </FeatureGuard>
107
+ </ProtectedRoute>
108
+ )
109
+ }
110
+ ```
111
+
112
+ Child pages go under `ui/src/routes/analytics/`. See [UI Recipes](../ui/recipes.md) recipe 2 for the nested page pattern.
113
+
114
+ ---
115
+
116
+ ## 5. Gate the route
117
+
118
+ Two independent mechanisms -- use both:
119
+
120
+ - `FeatureGuard` (feature-level): blocks access when the org model has the feature key disabled or when the member's `MembershipFeatureConfig` disables it. Always nest inside `ProtectedRoute`.
121
+ - `AdminGuard` (admin-level): blocks access for non-admin members. Add this if the feature should only be accessible to admins.
122
+
123
+ Full decision table and import paths are in [gate-by-feature-or-admin.md](gate-by-feature-or-admin.md).
124
+
125
+ ---
126
+
127
+ ## 6. (Optional) Add operations resources that back the feature
128
+
129
+ If the feature drives automation (e.g., an analytics pipeline workflow), create the resources in `operations/src/` and optionally map them into the org model via `resourceMappings`. See [add-a-resource.md](add-a-resource.md).
130
+
131
+ ---
132
+
133
+ ## 7. Verify
134
+
135
+ ```bash
136
+ pnpm -C ui dev
137
+ ```
138
+
139
+ - Feature appears in the nav sidebar.
140
+ - Route is accessible and the subshell sidebar renders.
141
+ - Toggle `features.enabled.analytics` to `false` in `foundations/config/organization-model.ts` and confirm the nav item disappears and the route redirects.
142
+ - Check `FeatureGuard` by navigating directly to `/analytics` with the feature disabled.