@elevasis/sdk 1.45.0 → 1.46.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 (38) hide show
  1. package/dist/cli.cjs +36046 -31587
  2. package/dist/index.d.ts +833 -391
  3. package/dist/index.js +59 -59
  4. package/dist/node/index.d.ts +0 -84
  5. package/dist/node/index.js +2 -2
  6. package/dist/test-utils/index.d.ts +859 -331
  7. package/dist/test-utils/index.js +166 -134
  8. package/dist/worker/index.d.ts +897 -324
  9. package/dist/worker/index.js +68 -35
  10. package/package.json +4 -4
  11. package/reference/_navigation.md +11 -2
  12. package/reference/_reference-manifest.json +42 -0
  13. package/reference/core/exports.mdx +2 -0
  14. package/reference/packages/core/src/business/README.md +4 -1
  15. package/reference/packages/core/src/content/README.md +19 -0
  16. package/reference/packages/core/src/organization-model/README.md +148 -149
  17. package/reference/packages/core/src/organization-model/readiness/README.md +42 -0
  18. package/reference/packages/ui/src/features/README.md +28 -28
  19. package/reference/rules/shared-types.md +21 -0
  20. package/reference/scaffold/core/organization-graph.mdx +2 -3
  21. package/reference/scaffold/core/organization-model.mdx +2 -6
  22. package/reference/scaffold/operations/propagation-pipeline.md +15 -16
  23. package/reference/scaffold/operations/scaffold-maintenance.md +3 -2
  24. package/reference/scaffold/operations/workflow-recipes.md +2 -2
  25. package/reference/scaffold/recipes/customize-crm-actions.md +5 -5
  26. package/reference/scaffold/recipes/extend-content.md +265 -0
  27. package/reference/scaffold/recipes/extend-lead-gen.md +14 -16
  28. package/reference/scaffold/recipes/index.md +4 -1
  29. package/reference/scaffold/reference/contracts.md +18 -55
  30. package/reference/scaffold/reference/feature-registry.md +3 -0
  31. package/reference/scaffold/reference/glossary.md +1 -1
  32. package/reference/scaffold/ui/customization.md +2 -2
  33. package/reference/scaffold/ui/feature-shell.mdx +1 -3
  34. package/reference/sdk/cli-management.mdx +90 -5
  35. package/reference/sdk/cli.mdx +90 -13
  36. package/reference/sdk/framework/agent.mdx +6 -0
  37. package/reference/sdk/platform-tools/adapters-platform.mdx +3 -1
  38. package/reference/ui/exports.mdx +1 -0
@@ -0,0 +1,265 @@
1
+ ---
2
+ title: Build and Extend Content
3
+ description: Map the content platform primitives available to SDK projects: pipeline and step catalogs, shared review/pipeline/distribution pages, data hooks, the content workflow adapter, artifacts as rules documents, and org-model extension boundaries.
4
+ ---
5
+ <!-- @generated by packages/sdk/scripts/copy-reference-docs.mjs -- DO NOT EDIT -->
6
+ <!-- Regenerate: pnpm scaffold:sync -->
7
+
8
+
9
+ # Build and Extend Content
10
+
11
+ Use this recipe when a downstream project wants to build on the shared content system instead of forking it.
12
+
13
+ Good trigger phrases:
14
+
15
+ - "Add a content pipeline for our podcast."
16
+ - "Change the review screen for clip proposals."
17
+ - "Write a workflow that produces the next step of a content item."
18
+ - "Store our style guide so producers read the same rules."
19
+ - "Track where each piece got published."
20
+
21
+ Content is a layered platform surface, not one component:
22
+
23
+ - **Organization OS:** pipelines, steps, statuses, pillars, and platforms are **catalogs** in the content System's ontology, not hardcoded enums. `content.config.defaultPipelineId` names the pipeline the UI opens by default.
24
+ - **Shared UI:** overview, items list, item review, pipelines, pipeline workspace, distributions, and rules-and-artifacts pages live in `@elevasis/ui/features/content`.
25
+ - **Headless hooks:** item, attempt, distribution, artifact, and pipeline-summary hooks live under `@elevasis/ui/hooks`.
26
+ - **Workflow adapter:** `content` from `@elevasis/sdk/worker` lets workflows create items, record attempts, and open distributions through platform tools.
27
+ - **Rules documents:** governing documents (idea banks, ICP docs, style guides, selection briefs) are **artifacts**, reached through the `artifacts` adapter, not a content-specific table.
28
+ - **System API Interface:** `content` uses the flat `system.apiInterface` marker. Readiness is derived from scoped resources, ontology bindings, and required catalogs — `content:object/item` plus the pipeline, step, and status catalogs.
29
+
30
+ ## The one thing to understand first: a pipeline is data
31
+
32
+ Every other system in this scaffold has its structure in code. Content does not. A pipeline is a catalog record, its steps are a second catalog record, and a workflow claims a step **by step key**. Adding a pipeline is therefore an org-model edit plus one workflow per step — no changes to shared UI, no new routes, no fork.
33
+
34
+ ```
35
+ content:catalog/pipeline which pipelines exist
36
+ content:catalog/{pipeline}-steps the ordered steps of one pipeline
37
+ content:catalog/status item lifecycle statuses
38
+ content:catalog/pillar editorial pillars (optional)
39
+ content:catalog/platform distribution targets
40
+ ```
41
+
42
+ A pipeline's step catalog is named `content:catalog/{pipelineId}-steps` and referenced from the pipeline record's `stepCatalog`. Get that name wrong and the workspace renders an empty pipeline with no error — the binding is by string.
43
+
44
+ ## Decision Table
45
+
46
+ | User wants | Start here | Notes |
47
+ | ------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
48
+ | Add a pipeline, step, status, pillar, or platform | `core/config/organization-model.ts` — the content System's `ontology` | Catalog edit. Add the step catalog **and** point the pipeline record's `stepCatalog` at it. |
49
+ | Produce the next step of an item from a workflow | `content` from `@elevasis/sdk/worker` | One workflow per step. Claim the step with `stepKey`; return the next step key. |
50
+ | Change the review screen | `ContentItemReviewPage` slots, or compose `ContentReviewCard` yourself | Prefer slots. The card, action bar, alternates panel, and processing strip are all exported individually. |
51
+ | Add content nav or a content route | `CONTENT_ITEMS`, `ContentSidebar`, `ContentSidebarMiddle` | Same manifest/sidebar composition pattern as CRM and lead-gen. |
52
+ | Store a governing document producers should read | `artifacts` from `@elevasis/sdk/worker` | `kind` MUST start with `content:` or RLS rejects an org-owned write. Pass `pipelineId` or `getActive` never finds it. |
53
+ | Track where a piece was published | `createDistribution` / `updateDistribution` | One distribution row per platform per item. |
54
+ | Add a new persisted content column or table | Platform/API migration work, not just scaffold work | DB, core schemas/types, API service/handlers, hooks, docs, and scaffold contracts move together. |
55
+
56
+ ## Published Content Surfaces
57
+
58
+ | Surface | Import from | Use for |
59
+ | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | ------------------------------------------------- |
60
+ | `contentManifest`, `CONTENT_ITEMS`, `ContentSidebar`, `ContentSidebarTop`, `ContentSidebarMiddle`, `MyReviewQueuePanel` | `@elevasis/ui/features/content` | Feature registration and sidebar composition |
61
+ | `ContentOverviewPage`, `ContentItemsPage`, `ContentItemReviewPage`, `ContentPipelinesPage`, `ContentPipelineWorkspacePage` | `@elevasis/ui/features/content` | Shared pages you can route to or wrap |
62
+ | `ContentSettingsPage`, `ContentDistributionsPage`, `ContentDistributionDetailPage` | `@elevasis/ui/features/content` | Rules-and-artifacts and distribution surfaces |
63
+ | `ContentReviewCard`, `ReviewActionBar`, `PayloadBody`, `AlternatesPanel`, `ProcessingStateStrip` | `@elevasis/ui/features/content` | Review UI primitives for a custom review screen |
64
+ | `useContentConfig`, `resolveContentStepResource` | `@elevasis/ui/features/content` | Read the resolved pipeline/step catalogs from OM |
65
+ | `useContentItems`, `useContentItem`, `useContentItemAttempts`, `useUpdateContentItem`, `useReviewContentItem` | `@elevasis/ui/hooks` | Item data access and review mutations |
66
+ | `useContentDistributions`, `useContentDistribution`, `useUpdateContentDistribution` | `@elevasis/ui/hooks` | Distribution data access |
67
+ | `useContentArtifacts`, `filterContentArtifacts`, `useContentPipelineSummary` | `@elevasis/ui/hooks` | Rules documents and pipeline roll-ups |
68
+ | `content`, `artifacts` | `@elevasis/sdk/worker` | Workflow-side content and rules-document adapters |
69
+
70
+ Read the generated contracts before changing typed boundaries:
71
+
72
+ `operations/node_modules/@elevasis/sdk/reference/scaffold/reference/contracts.md`
73
+
74
+ Look for the **Content Platform Primitives** section: item/attempt/distribution rows, review decisions, the status and pillar vocabularies, and the `ContentToolMap` adapter map.
75
+
76
+ ## 1. Add a Pipeline
77
+
78
+ A pipeline is two catalog records plus the workflows that claim its steps.
79
+
80
+ <!-- doc-snippet:skip: illustrative excerpt -- catalog shape, not a compilable module -->
81
+
82
+ ```ts
83
+ // core/config/organization-model.ts -- inside the content System's `ontology.catalogTypes`
84
+ 'content:catalog/pipeline': {
85
+ entries: {
86
+ 'podcast-to-clips': {
87
+ label: 'Podcast to Clips',
88
+ stepCatalog: 'content:catalog/podcast-to-clips-steps'
89
+ }
90
+ }
91
+ },
92
+ 'content:catalog/podcast-to-clips-steps': {
93
+ entries: {
94
+ 'transcript': { label: 'Transcript', order: 1 },
95
+ 'clip-selection': { label: 'Clip Selection', order: 2 },
96
+ 'publish': { label: 'Publish', order: 3 }
97
+ }
98
+ }
99
+ ```
100
+
101
+ Then set `content.config.defaultPipelineId` if this should be the pipeline the UI opens first.
102
+
103
+ **The step catalog name is load-bearing.** `content:catalog/{pipelineId}-steps` must match the pipeline record's `stepCatalog` exactly. Nothing type-checks that string.
104
+
105
+ ## 2. Produce a Step in a Workflow
106
+
107
+ One workflow per step. The workflow records an **attempt** against the item, tagged with the step it satisfies:
108
+
109
+ <!-- doc-snippet:skip: illustrative excerpt -- `resourceDescriptors` names the project's own resource-descriptors export -->
110
+
111
+ ```ts
112
+ // operations/src/content/clip-selection.ts
113
+ import type { WorkflowDefinition } from '@elevasis/sdk'
114
+ import { content } from '@elevasis/sdk/worker'
115
+ import { resourceDescriptors } from '@core/config/organization-model'
116
+ import { z } from 'zod'
117
+
118
+ const inputSchema = z.object({ contentItemId: z.string().uuid() })
119
+ const outputSchema = z.object({ attemptId: z.string(), nextStepKey: z.string().nullable() })
120
+
121
+ export const clipSelectionWorkflow: WorkflowDefinition = {
122
+ config: {
123
+ resource: resourceDescriptors.clipSelection,
124
+ resourceId: resourceDescriptors.clipSelection.id,
125
+ name: 'Clip Selection',
126
+ description: 'Chooses clip candidates for a podcast episode.',
127
+ type: resourceDescriptors.clipSelection.kind,
128
+ version: '1.0.0',
129
+ status: 'dev'
130
+ },
131
+ contract: { inputSchema, outputSchema },
132
+ steps: {
133
+ select: {
134
+ id: 'select',
135
+ name: 'Select Clips',
136
+ description: 'Records a clip-selection attempt against the item.',
137
+ inputSchema,
138
+ outputSchema,
139
+ next: null,
140
+ handler: async (rawInput, context) => {
141
+ const input = rawInput as z.infer<typeof inputSchema>
142
+ const item = await content.getItem({ contentItemId: input.contentItemId })
143
+
144
+ const attempt = await content.createAttempt({
145
+ contentItemId: item.id,
146
+ // Tags the attempt with the step it satisfies. Omit it and the
147
+ // attempt is stored with a null step -- the review UI's Step column
148
+ // renders "--" and nothing tells you which step produced the row.
149
+ stepKey: 'clip-selection',
150
+ payload: { clips: [] },
151
+ sourceExecutionId: context.executionId
152
+ })
153
+
154
+ return { attemptId: attempt.id, nextStepKey: 'publish' }
155
+ }
156
+ }
157
+ },
158
+ entryPoint: 'select'
159
+ }
160
+ ```
161
+
162
+ `ContentToolMap` is the full surface: `createItem`, `getItem`, `listItems`, `updateItem`, `createAttempt`, `listAttempts`, `createDistribution`, `updateDistribution`. `organizationId` is injected server-side — never pass it from workflow code.
163
+
164
+ ## 3. Store Rules Documents
165
+
166
+ Governing documents are artifacts, not a content table. Two rules decide whether a write is reachable at all:
167
+
168
+ ```ts
169
+ import { artifacts } from '@elevasis/sdk/worker'
170
+
171
+ // `kind` MUST be `content:`-prefixed. The artifacts_manage_content RLS policy
172
+ // grants org-owned writes only for `kind LIKE 'content:%'` -- a non-prefixed
173
+ // kind is rejected server-side with no compile-time warning.
174
+ //
175
+ // `pipelineId` MUST be supplied. getActive filters on
176
+ // (organizationId, pipelineId, kind); an artifact written without one can
177
+ // never be read back by the lookup every consumer uses.
178
+ await artifacts.createArtifact({
179
+ ownerKind: 'organization',
180
+ ownerId: null,
181
+ kind: 'content:style-guide',
182
+ pipelineId: 'podcast-to-clips',
183
+ content: { tone: 'direct', bannedPhrases: ['synergy'] }
184
+ })
185
+
186
+ const { artifact } = await artifacts.getActive({
187
+ pipelineId: 'podcast-to-clips',
188
+ kind: 'content:style-guide'
189
+ })
190
+ ```
191
+
192
+ Writing again for the same `(pipelineId, kind)` supersedes the previous version rather than colliding: the platform deactivates the predecessor and stores the new row at the next version number, atomically. `getActive` always returns the current one, and `ContentSettingsPage` renders the version history.
193
+
194
+ `ContentSettingsPage` scopes its browser to **declared** pipelines. An artifact written under a `pipelineId` that is not in `content:catalog/pipeline` is stored correctly and is invisible in that UI.
195
+
196
+ ## 4. Customize the Review Screen
197
+
198
+ Start with slots on the shared page:
199
+
200
+ ```tsx
201
+ // ui/src/routes/content/items.$itemId.tsx
202
+ import { createFileRoute } from '@tanstack/react-router'
203
+ import { ContentItemReviewPage } from '@elevasis/ui/features/content'
204
+ import { Button } from '@mantine/core'
205
+
206
+ export const Route = createFileRoute('/content/items/$itemId')({ component: ReviewRoute })
207
+
208
+ function ReviewRoute() {
209
+ const { itemId } = Route.useParams()
210
+ return <ContentItemReviewPage itemId={itemId} headerActions={<Button>Send to Legal</Button>} />
211
+ }
212
+ ```
213
+
214
+ When the layout itself has to change, compose the primitives — `ContentReviewCard`, `ReviewActionBar`, `PayloadBody`, `AlternatesPanel`, `ProcessingStateStrip` — with `useContentItem`, `useContentItemAttempts`, and `useReviewContentItem`. Read the pipeline's steps through `useContentConfig` rather than hardcoding them, or a new step silently fails to render.
215
+
216
+ ## 5. Extend Navigation
217
+
218
+ Same pattern as the other systems:
219
+
220
+ ```tsx
221
+ import { contentManifest, CONTENT_ITEMS, ContentSidebar, ContentSidebarMiddle } from '@elevasis/ui/features/content'
222
+ import type { SystemModule } from '@elevasis/ui/provider'
223
+ import type { NavItem } from '@elevasis/ui/layout'
224
+ import { IconChartBar } from '@tabler/icons-react'
225
+
226
+ const customItems: NavItem[] = [
227
+ ...CONTENT_ITEMS,
228
+ { label: 'Performance', to: '/content/performance', icon: IconChartBar, exact: false }
229
+ ]
230
+
231
+ export const customContentManifest: SystemModule = {
232
+ ...contentManifest,
233
+ sidebar: () => (
234
+ <ContentSidebar>
235
+ <ContentSidebarMiddle items={customItems} />
236
+ </ContentSidebar>
237
+ )
238
+ }
239
+ ```
240
+
241
+ Replace `contentManifest` with `customContentManifest` in the local `SYSTEM_MANIFESTS` array and add the matching route under `ui/src/routes/content/`.
242
+
243
+ ## 6. Mark the System API-Ready
244
+
245
+ `content.apiInterface.lifecycle` goes `active` only when the System's scoped resources, `content:object/item`, and the pipeline/step/status catalogs are all in place. Two failure modes are worth knowing before you flip it:
246
+
247
+ - An **empty** `apiInterface.resourceIds` array throws rather than opting out. Omitting the whole `apiInterface` block is the opt-out; a `resourceIds: []` stub is the anomaly.
248
+ - Readiness is derived from a **deployed** snapshot. A model edit that has not been redeployed produces a stale-snapshot 503 that looks like a code bug. Redeploy before debugging.
249
+
250
+ ## Verify
251
+
252
+ Run the checks for the surfaces you touched:
253
+
254
+ ```bash
255
+ pnpm -C ui run check-types
256
+ pnpm -C operations run check
257
+ pnpm -C operations exec elevasis-sdk check
258
+ ```
259
+
260
+ If you changed platform-level content contracts in the monorepo, the platform maintainer must also regenerate and verify scaffold output:
261
+
262
+ ```bash
263
+ pnpm scaffold:sync
264
+ pnpm scaffold:verify
265
+ ```
@@ -349,24 +349,26 @@ Use `useArtifacts({ ownerKind, ownerId })` for durable JSON artifacts like audit
349
349
 
350
350
  Inside deployed workflows, use worker adapters instead of browser hooks or direct database access:
351
351
 
352
- External projects should define workflow input/output schemas in `@shared/types`; the example below assumes those shared schemas already exist.
352
+ External projects should define reusable workflow input/output schemas in `@core/types` (the `@core/*` tsconfig path resolves to the project's own `core/` directory); the example below assumes those shared schemas already exist. There is no `@shared/*` alias in a template-family project — do not write one.
353
353
 
354
- <!-- doc-snippet:skip: illustrative excerpt -- `@shared/types` is a project-local alias for the reader's own shared schema module, `resourceDescriptors` names the project's own resource-descriptors export, and `z` (zod) is shown unimported -->
354
+ **Do not hand-bind the OM Resource descriptor on each workflow.** `projectDeploymentSpec` in `operations/src/index.ts` takes a `getWorkflowResourceDescriptor` resolver and binds `config.resource`, `config.resourceId`, and `config.type` for every workflow centrally. It looks the descriptor up by the `config.resourceId` you author and then **overwrites `resource` and `type` unconditionally** — so setting them per workflow is dead code that reads as a contract and drifts from the model without any error. Author `resourceId` and let the spec bind the rest.
355
+
356
+ <!-- doc-snippet:skip: illustrative excerpt -- `@core/types` resolves to the project's own `core/types/` module via the `@core/*` tsconfig path, `resourceDescriptors` names the project's own resource-descriptors export, and `z` (zod) is shown unimported -->
355
357
 
356
358
  ```ts
357
359
  // operations/src/sales/qualify-list.ts
358
360
  import type { WorkflowDefinition } from '@elevasis/sdk'
359
361
  import { acqDb, list } from '@elevasis/sdk/worker'
360
- import { qualifyListInputSchema, qualifyListOutputSchema } from '@shared/types'
362
+ import { qualifyListInputSchema, qualifyListOutputSchema } from '@core/types'
361
363
  import { resourceDescriptors } from '@core/config/organization-model'
362
364
 
363
365
  export const qualifyListWorkflow: WorkflowDefinition = {
364
366
  config: {
365
- resource: resourceDescriptors.qualifyList,
367
+ // `resourceId` is the only descriptor field you author -- it is the lookup key
368
+ // `projectDeploymentSpec` resolves against your OM. See the note below.
366
369
  resourceId: resourceDescriptors.qualifyList.id,
367
370
  name: 'Qualify List',
368
371
  description: 'Marks list contacts as verified based on qualification checks.',
369
- type: resourceDescriptors.qualifyList.kind,
370
372
  version: '1.0.0',
371
373
  status: 'dev',
372
374
  },
@@ -458,14 +460,10 @@ Practical consequence: you can rename systems, restructure ontology, swap export
458
460
 
459
461
  ### Declaring an OM Readiness Contract
460
462
 
461
- When you define a custom `readinessProfile` id (any string that is not one of the built-in presets `sales.lead-gen.api`, `sales.crm.api`, `sales.lead-gen.crm-handoff`), you must also provide a `readinessContract` on the `apiInterface` so the platform can validate structural readiness without hardcoded constants.
463
+ When you define a custom `readinessProfile` id (any string that is not one of the built-in presets `sales.lead-gen.api`, `sales.crm.api`, `sales.lead-gen.crm-handoff`), you must also provide a `readinessContract` on the `apiInterface` so the platform can validate structural readiness without hardcoded constants. This is mandatory — there is no `registerReadinessProfile` call that lets a custom system skip declaring a `readinessContract`. `registerReadinessProfile` (below) only records that an id has been chosen; it registers no validation logic and does not change how `computeInterfaceReadiness` treats the system, so it is optional and has no effect on whether the contract is required.
462
464
 
463
465
  ```ts
464
466
  // core/config/organization-model.ts
465
- import { registerReadinessProfile } from '@elevasis/sdk'
466
-
467
- // Register the profile id at startup so the platform recognizes it
468
- registerReadinessProfile('acme.prospecting.api')
469
467
 
470
468
  export const organizationModel = {
471
469
  // ...
@@ -497,30 +495,30 @@ export const organizationModel = {
497
495
 
498
496
  `readinessContract.requiredObjects` and `requiredCatalogs` are arrays of ontology IDs owned by this system. The platform validates that each declared object type exists in the OM and that each declared catalog type has at least one entry. `requiredCatalogs` must declare at least one entry — an empty contract asserts nothing and is rejected.
499
497
 
500
- Built-in profile ids (`sales.lead-gen.api`, etc.) derive their requirements from platform code. If you use a built-in profile on a custom system, you do not need a `readinessContract`.
498
+ Built-in profile ids (`sales.lead-gen.api`, etc.) derive their requirements from platform code. **A `readinessContract` declared alongside a built-in profile is ignored outright — not merely unnecessary.** The readiness engine branches on the profile: a registered built-in runs its own validator, and the authored contract is read only on the `else` branch, so anything you declare next to a built-in profile validates nothing and silently drifts from what the platform actually enforces. Real projects have shipped full contracts this way believing they were enforced.
499
+
500
+ Note that the determinant is the **profile**, not the system. A custom system using a built-in profile still gets the built-in's requirements and still ignores an authored contract; a custom profile id is what makes `readinessContract` required (and it is required — a custom profile without one fails readiness with `missing-readiness-contract`).
501
501
 
502
- The `lookupReadinessProfile`, `registerReadinessProfile`, and `isBuiltInReadinessProfile` functions are exported from `@elevasis/sdk` alongside the `SystemApiInterfaceReadinessContract`, `ReadinessProfileKind`, and `ReadinessProfileEntry` types.
502
+ The `lookupReadinessProfile`, `registerReadinessProfile`, and `isBuiltInReadinessProfile` functions are exported from `@elevasis/sdk` alongside the `SystemApiInterfaceReadinessContract`, `ReadinessProfileKind`, and `ReadinessProfileEntry` types. `registerReadinessProfile(profileId)` only acknowledges a custom id for introspection (`lookupReadinessProfile` then resolves it as `kind: 'custom'` instead of `undefined`) — it cannot make `isBuiltInReadinessProfile` return `true` and it is never a substitute for a `readinessContract`. The actual extension point for platform-code-driven readiness (no `readinessContract` needed) is `registerBuiltInReadinessProfile`, exported from `@elevasis/core/organization-model/readiness` — it takes real structural validation logic, not just an id.
503
503
 
504
504
  ### Export Channel as Tenant Business Config
505
505
 
506
506
  The export channel — ClickUp, CSV/XLSX file, platform push, or a custom destination — is an action + credential choice declared in your tenant OM. The platform owns the list-builder workflow primitive and the file storage adapters; you own which workflow id handles export and which credential it uses.
507
507
 
508
- <!-- doc-snippet:skip: illustrative excerpt -- `@shared/types` is a project-local alias for the reader's own shared schema module, and `buildCsvFromContacts` is a tenant-authored helper this recipe does not define -->
508
+ <!-- doc-snippet:skip: illustrative excerpt -- `@core/types` resolves to the project's own `core/types/` module via the `@core/*` tsconfig path, and `buildCsvFromContacts` is a tenant-authored helper this recipe does not define -->
509
509
 
510
510
  ```ts
511
511
  // operations/src/sales/prospecting/export-list.ts
512
512
  import type { WorkflowDefinition } from '@elevasis/sdk'
513
513
  import { acqDb, list, storage } from '@elevasis/sdk/worker'
514
- import { exportListInputSchema, exportListOutputSchema } from '@shared/types'
514
+ import { exportListInputSchema, exportListOutputSchema } from '@core/types'
515
515
  import { resourceDescriptors } from '@core/config/organization-model'
516
516
 
517
517
  export const exportListWorkflow: WorkflowDefinition = {
518
518
  config: {
519
- resource: resourceDescriptors.exportList,
520
519
  resourceId: resourceDescriptors.exportList.id,
521
520
  name: 'Export List',
522
521
  description: 'Exports list contacts to a CSV file in project storage.',
523
- type: 'workflow',
524
522
  version: '1.0.0',
525
523
  status: 'prod'
526
524
  },
@@ -31,6 +31,9 @@ You want to build on the shared CRM without forking it: add CRM routes, compose
31
31
  **[Build and Extend Lead Gen](extend-lead-gen.md)**
32
32
  You want to build on the shared lead-gen system without forking it: add lead-gen routes, compose sidebars/pages, use list/company/contact/artifact hooks, mutate list data from workflows, or understand which contracts and adapters form the extension surface.
33
33
 
34
+ **[Build and Extend Content](extend-content.md)**
35
+ You want to build on the shared content system without forking it: add a pipeline, produce a step from a workflow, customize the review screen, store rules documents, or track distributions. Covers the catalog-driven pipeline model (a pipeline is data, not code), the `content:catalog/{pipeline}-steps` binding, attempt authoring with `stepKey`, artifacts as versioned governing documents with their two silent write constraints, review-page slots and primitives, and the `apiInterface` readiness rules.
36
+
34
37
  **[Customize CRM Actions](customize-crm-actions.md)**
35
38
  You want to add, hide, or replace CRM deal action buttons, configure the shared `crmActions` provider path, or call a project-owned workflow from custom UI when server-side action dispatch constraints require it. Covers `ActionDef`, the caller-supplied `crmActions` catalog, provider wiring, and the current v1 boundary for custom action dispatch.
36
39
 
@@ -45,6 +48,6 @@ You want to browse, inspect, or traverse the OrganizationModel knowledge graph f
45
48
  ## Reference docs these recipes link into
46
49
 
47
50
  - [glossary.md](../reference/glossary.md) -- term disambiguation for System, Resource, systemId, Topology, Settings asymmetry
48
- - [contracts.md](../reference/contracts.md) -- TypeScript shapes: `SystemModule`, `OrganizationModel`, CRM deal types, lead-gen list/member/artifact types, `CrmToolMap`, `LeadToolMap`, `ListToolMap`, `ActionDef`
51
+ - [contracts.md](../reference/contracts.md) -- TypeScript shapes: `SystemModule`, `OrganizationModel`, CRM deal types, lead-gen list/member/artifact types, content item/attempt/distribution types, `CrmToolMap`, `LeadToolMap`, `ListToolMap`, `ContentToolMap`, `ActionDef`
49
52
  - [feature-flags-and-gating.md](../ui/feature-flags-and-gating.md) -- full three-concept gating model
50
53
  - [workflow-recipes.md](../operations/workflow-recipes.md) -- workflow anatomy, adapters, trigger patterns
@@ -402,13 +402,6 @@ export type OrganizationModelSystemApiInterface = z.infer<typeof SystemApiInterf
402
402
  export type OrganizationModelSystemInterfaceRef = z.infer<typeof SystemInterfaceRefSchema>
403
403
  ```
404
404
 
405
- ### `OrganizationModelSystemStatus`
406
-
407
- ```typescript
408
- /** @deprecated Use OrganizationModelSystemLifecycle. Accepted for one publish cycle. */
409
- export type OrganizationModelSystemStatus = z.infer<typeof SystemStatusSchema>
410
- ```
411
-
412
405
  ### `OrganizationModelResources`
413
406
 
414
407
  ```typescript
@@ -651,48 +644,6 @@ export type OrganizationModelEntity = z.infer<typeof EntitySchema>
651
644
  export type OrganizationModelEntities = z.infer<typeof EntitiesDomainSchema>
652
645
  ```
653
646
 
654
- ### `OrganizationModelPolicies`
655
-
656
- ```typescript
657
- export type OrganizationModelPolicies = z.infer<typeof PoliciesDomainSchema>
658
- ```
659
-
660
- ### `OrganizationModelPolicy`
661
-
662
- ```typescript
663
- export type OrganizationModelPolicy = z.infer<typeof PolicySchema>
664
- ```
665
-
666
- ### `OrganizationModelPolicyId`
667
-
668
- ```typescript
669
- export type OrganizationModelPolicyId = z.infer<typeof PolicyIdSchema>
670
- ```
671
-
672
- ### `OrganizationModelPolicyApplicability`
673
-
674
- ```typescript
675
- export type OrganizationModelPolicyApplicability = z.infer<typeof PolicyApplicabilitySchema>
676
- ```
677
-
678
- ### `OrganizationModelPolicyTrigger`
679
-
680
- ```typescript
681
- export type OrganizationModelPolicyTrigger = z.infer<typeof PolicyTriggerSchema>
682
- ```
683
-
684
- ### `OrganizationModelPolicyPredicate`
685
-
686
- ```typescript
687
- export type OrganizationModelPolicyPredicate = z.infer<typeof PolicyPredicateSchema>
688
- ```
689
-
690
- ### `OrganizationModelPolicyEffect`
691
-
692
- ```typescript
693
- export type OrganizationModelPolicyEffect = z.infer<typeof PolicyEffectSchema>
694
- ```
695
-
696
647
  ### `OrganizationModelKnowledge`
697
648
 
698
649
  ```typescript
@@ -852,7 +803,6 @@ export interface ResolvedShellModel {
852
803
  ancestorsOf: (id: string) => ResolvedShellSystem[]
853
804
  parentOf: (id: string) => ResolvedShellSystem | undefined
854
805
  topLevel: () => ResolvedShellSystem[]
855
- requiresAdminFor: (id: string) => boolean
856
806
  devOnlyFor: (id: string) => boolean
857
807
  }
858
808
  ```
@@ -2925,7 +2875,7 @@ export const AcqContactListResponseSchema = z.object({
2925
2875
  ### `AcqArtifactOwnerKindSchema`
2926
2876
 
2927
2877
  ```typescript
2928
- export const AcqArtifactOwnerKindSchema = z.enum(['company', 'contact', 'deal', 'list', 'list_member'])
2878
+ export const AcqArtifactOwnerKindSchema = z.enum(['company', 'contact', 'deal', 'list', 'list_member', 'organization'])
2929
2879
  ```
2930
2880
 
2931
2881
  ### `ListArtifactsQuerySchema`
@@ -2934,7 +2884,7 @@ export const AcqArtifactOwnerKindSchema = z.enum(['company', 'contact', 'deal',
2934
2884
  export const ListArtifactsQuerySchema = z
2935
2885
  .object({
2936
2886
  ownerKind: AcqArtifactOwnerKindSchema,
2937
- ownerId: UuidSchema
2887
+ ownerId: UuidSchema.optional()
2938
2888
  })
2939
2889
  .strict()
2940
2890
  ```
@@ -2945,10 +2895,17 @@ export const ListArtifactsQuerySchema = z
2945
2895
  export const CreateArtifactRequestSchema = z
2946
2896
  .object({
2947
2897
  ownerKind: AcqArtifactOwnerKindSchema,
2948
- ownerId: UuidSchema,
2898
+ // Nullable + optional: an org-owned artifact (ownerKind: 'organization') has no
2899
+ // single owning entity row. See the comment on AcqArtifactOwnerKindSchema above.
2900
+ ownerId: UuidSchema.nullable().optional(),
2949
2901
  kind: z.string().trim().min(1).max(255),
2950
2902
  content: z.record(z.string(), z.unknown()),
2951
- sourceExecutionId: UuidSchema.optional()
2903
+ sourceExecutionId: UuidSchema.optional(),
2904
+ // Nullable + optional: acquisition's own artifacts (audits, proposals, ICP docs) predate the
2905
+ // pipeline concept and have none. `getActiveArtifact` filters on it, so any artifact meant to
2906
+ // be resolvable as "the active document for pipeline X" MUST supply it -- artifacts-platform-tool
2907
+ // .mdx Step 3 defect 1.
2908
+ pipelineId: PipelineIdSchema.nullable().optional()
2952
2909
  })
2953
2910
  .strict()
2954
2911
  ```
@@ -2960,12 +2917,18 @@ export const AcqArtifactResponseSchema = z.object({
2960
2917
  id: z.string(),
2961
2918
  organizationId: z.string(),
2962
2919
  ownerKind: z.string(),
2963
- ownerId: z.string(),
2920
+ // Nullable as of content-pipeline-foundation D14: an org-owned artifact has no owning entity row.
2921
+ // CreateArtifactRequestSchema already accepted a null ownerId; this response schema not doing so
2922
+ // was Step 3 defect 2 -- an org-owned artifact inserted fine and then failed serialization.
2923
+ ownerId: z.string().nullable(),
2964
2924
  kind: z.string(),
2925
+ pipelineId: z.string().nullable(),
2965
2926
  content: z.record(z.string(), z.unknown()),
2966
2927
  sourceExecutionId: z.string().nullable(),
2967
2928
  createdBy: z.string().nullable(),
2968
2929
  createdAt: z.string(),
2930
+ updatedAt: z.string(),
2931
+ isActive: z.boolean(),
2969
2932
  version: z.number().int()
2970
2933
  })
2971
2934
  ```
@@ -14,6 +14,8 @@ description: Auto-generated catalog of registered system manifests. Do not edit
14
14
 
15
15
  | System Key | System ID | Nav Label | Domains | Routes |
16
16
  | --- | --- | --- | --- | --- |
17
+ | `clients` | `client-management` | — | — | — |
18
+ | `content` | `content` | — | — | — |
17
19
  | `crm` | `sales.crm` | — | — | — |
18
20
  | `delivery` | `platform.projects` | — | — | — |
19
21
  | `knowledge` | `—` | — | — | — |
@@ -22,5 +24,6 @@ description: Auto-generated catalog of registered system manifests. Do not edit
22
24
  | `operations` | `—` | — | — | — |
23
25
  | `seo` | `seo` | — | — | — |
24
26
  | `settings` | `—` | — | — | — |
27
+ | `submitted-requests` | `—` | — | — | — |
25
28
 
26
29
  > **Note:** `auth` and `dashboard` are not in the registry — they are app-shell concerns, not gated systems.
@@ -64,7 +64,7 @@ description: Terminology disambiguation for Organization OS concepts used in the
64
64
 
65
65
  **Settings asymmetry** -- Settings is a navigation/app surface whose individual pages are normally governed by admin checks rather than user-facing System toggles.
66
66
 
67
- **Shell model** -- provider output used by sidebars and breadcrumbs. Includes `systems`, `childrenOf`, `ancestorsOf`, `parentOf`, `topLevel`, `findByPath`, `requiresAdminFor`, and `devOnlyFor`.
67
+ **Shell model** -- provider output used by sidebars and breadcrumbs. Includes `systems`, `childrenOf`, `ancestorsOf`, `parentOf`, `topLevel`, `findByPath`, and `devOnlyFor`.
68
68
 
69
69
  **Subshell / Sidebar** -- System- or route-prefix-scoped UI region rendered when the current route matches a module whose manifest supplies a sidebar.
70
70
 
@@ -229,9 +229,9 @@ const MyDeliverySidebar = () => <ProjectsSidebar><MyDeliveryMiddle /></ProjectsS
229
229
 
230
230
  This is the same compose-from-primitives path CRM consumers use for structural changes. No special API -- just `SubshellSidebarSection` + `SubshellNavList` from `@elevasis/ui/layout`.
231
231
 
232
- For broader CRM extension work across pages, hooks, actions, workflows, and org-model boundaries, start with `operations/node_modules/@elevasis/sdk/reference/scaffold/recipes/extend-crm.md`.
232
+ For broader CRM extension work across pages, hooks, actions, workflows, and org-model boundaries, start with the `extend-crm.md` recipe. Internally, read the workspace source at `packages/sdk/docs/scaffold/recipes/extend-crm.md`; a tenant reads the published mirror at `operations/node_modules/@elevasis/sdk/reference/scaffold/recipes/extend-crm.md`, which is a copy made at the last publish and can lag the workspace.
233
233
 
234
- For broader lead-gen extension work across pages, hooks, list/member state, artifacts, workflows, and org-model boundaries, start with `operations/node_modules/@elevasis/sdk/reference/scaffold/recipes/extend-lead-gen.md`.
234
+ For broader lead-gen extension work across pages, hooks, list/member state, artifacts, workflows, and org-model boundaries, the same pair applies: `packages/sdk/docs/scaffold/recipes/extend-lead-gen.md` internally, `operations/node_modules/@elevasis/sdk/reference/scaffold/recipes/extend-lead-gen.md` for a tenant.
235
235
 
236
236
  ## Imports Cheat Sheet
237
237
 
@@ -56,8 +56,6 @@ interface OrganizationGraphSystemBridge {
56
56
  ancestorsOf,
57
57
  parentOf,
58
58
  topLevel,
59
- uiPositionFor,
60
- requiresAdminFor,
61
59
  devOnlyFor
62
60
  }
63
61
  ```
@@ -66,7 +64,7 @@ Sidebar rendering walks `topLevel()` and `childrenOf(id)`. Breadcrumbs resolve `
66
64
 
67
65
  ## Access
68
66
 
69
- System access is keyed by Organization Model system ID. `requiresAdmin` and `devOnly` inherit from ancestor system nodes and are applied when deriving visible sidebar entries.
67
+ System access is keyed by Organization Model system ID. A System is hidden from non-dev viewers when it, or any ancestor, is `lifecycle: 'beta'` that is what `devOnlyFor(id)` reports. Systems carry no `devOnly` or `requiresAdmin` flags of their own; the `devOnly` and `requiresAdmin` fields on a **sidebar surface node** are separate, still authored, and still applied when deriving visible entries.
70
68
 
71
69
  Routes still need guards:
72
70