@elevasis/sdk 1.45.0 → 1.47.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/cli.cjs +36454 -31490
- package/dist/index.d.ts +964 -358
- package/dist/index.js +59 -59
- package/dist/node/index.d.ts +0 -84
- package/dist/node/index.js +2 -2
- package/dist/test-utils/index.d.ts +993 -301
- package/dist/test-utils/index.js +177 -134
- package/dist/worker/index.d.ts +1043 -277
- package/dist/worker/index.js +74 -35
- package/package.json +2 -2
- package/reference/_navigation.md +12 -3
- package/reference/_reference-manifest.json +42 -0
- package/reference/core/exports.mdx +2 -0
- package/reference/packages/core/src/business/README.md +4 -1
- package/reference/packages/core/src/content/README.md +24 -0
- package/reference/packages/core/src/organization-model/README.md +148 -149
- package/reference/packages/core/src/organization-model/readiness/README.md +42 -0
- package/reference/packages/ui/src/features/README.md +28 -28
- package/reference/rules/shared-types.md +21 -0
- package/reference/scaffold/core/organization-graph.mdx +2 -3
- package/reference/scaffold/core/organization-model.mdx +2 -6
- package/reference/scaffold/operations/propagation-pipeline.md +15 -16
- package/reference/scaffold/operations/scaffold-maintenance.md +3 -2
- package/reference/scaffold/operations/workflow-recipes.md +2 -2
- package/reference/scaffold/recipes/customize-crm-actions.md +5 -5
- package/reference/scaffold/recipes/extend-content.md +301 -0
- package/reference/scaffold/recipes/extend-lead-gen.md +14 -16
- package/reference/scaffold/recipes/index.md +4 -1
- package/reference/scaffold/reference/contracts.md +18 -55
- package/reference/scaffold/reference/feature-registry.md +3 -0
- package/reference/scaffold/reference/glossary.md +1 -1
- package/reference/scaffold/ui/customization.md +2 -2
- package/reference/scaffold/ui/feature-shell.mdx +1 -3
- package/reference/sdk/cli-management.mdx +199 -30
- package/reference/sdk/cli.mdx +90 -13
- package/reference/sdk/framework/agent.mdx +6 -0
- package/reference/sdk/platform-tools/adapters-platform.mdx +3 -1
- package/reference/sdk/platform-tools/index.mdx +0 -2
- package/reference/sdk/resources/patterns.mdx +14 -6
- package/reference/ui/exports.mdx +1 -0
|
@@ -0,0 +1,301 @@
|
|
|
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, 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
|
+
- "Track where each piece got published."
|
|
19
|
+
|
|
20
|
+
Content is a layered platform surface, not one component:
|
|
21
|
+
|
|
22
|
+
- **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.
|
|
23
|
+
- **Shared UI:** overview, items list, item review, pipelines, pipeline workspace, and distribution pages live in `@elevasis/ui/features/content`.
|
|
24
|
+
- **Headless hooks:** item, attempt, distribution, and pipeline-summary hooks live under `@elevasis/ui/hooks`.
|
|
25
|
+
- **Workflow adapter:** `content` from `@elevasis/sdk/worker` lets workflows create items, record and patch attempts, manage source assets, and open distributions through platform tools.
|
|
26
|
+
- **Operator surface:** `elevasis-sdk content:*` is the read and review surface for humans and agents outside a session -- list items, inspect one, see what is waiting on a `queued` review gate, read a pipeline's step contract, and clear a gate. See `apps/docs/content/docs/sdk/sdk/cli-management.mdx` for the full command reference.
|
|
27
|
+
- **Producer instructions live in the prompt.** Content has no governing-document tier. Voice, constraints, and style belong in the generating workflow's own prompt, where they behave identically and are versioned with the code that reads them. The removed tier had no producer and no consumer — nothing called `getActive` and nothing wrote back measured performance.
|
|
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
|
+
| Give producers a style guide or voice constraints | The generating workflow's own prompt | Content has no rules-document tier. Put the instructions where the model reads them. |
|
|
53
|
+
| Track where a piece was published | `createDistribution` / `updateDistribution` | One distribution row per platform per item. |
|
|
54
|
+
| Pick source material for a new item | `createSourceAsset` / `getSourceAsset` / `listSourceAssets` / `updateSourceAsset` | Raw material a producer works from -- an uploaded episode, a transcript, a reference file. |
|
|
55
|
+
| Operate the pipeline from outside a browser | `elevasis-sdk content:*` | Read-only plus one write (`content:review`). Not how a workflow produces content -- see the boundary below. |
|
|
56
|
+
| 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. |
|
|
57
|
+
|
|
58
|
+
## Published Content Surfaces
|
|
59
|
+
|
|
60
|
+
| Surface | Import from | Use for |
|
|
61
|
+
| -------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | ------------------------------------------------ |
|
|
62
|
+
| `contentManifest`, `CONTENT_ITEMS`, `ContentSidebar`, `ContentSidebarMiddle`, `MyReviewQueuePanel` | `@elevasis/ui/features/content` | Feature registration and sidebar composition |
|
|
63
|
+
| `ContentOverviewPage`, `ContentItemsPage`, `ContentItemReviewPage`, `ContentPipelinesPage`, `ContentPipelineWorkspacePage` | `@elevasis/ui/features/content` | Shared pages you can route to or wrap |
|
|
64
|
+
| `ContentDistributionsPage`, `ContentDistributionDetailPage` | `@elevasis/ui/features/content` | Distribution surfaces |
|
|
65
|
+
| `ContentReviewCard`, `ReviewActionBar`, `PayloadBody`, `AlternatesPanel`, `ProcessingStateStrip` | `@elevasis/ui/features/content` | Review UI primitives for a custom review screen |
|
|
66
|
+
| `useContentConfig`, `resolveContentStepResource` | `@elevasis/ui/features/content` | Read the resolved pipeline/step catalogs from OM |
|
|
67
|
+
| `useContentItems`, `useContentItem`, `useContentItemAttempts`, `useUpdateContentItem`, `useReviewContentItem` | `@elevasis/ui/hooks` | Item data access and review mutations |
|
|
68
|
+
| `useContentDistributions`, `useContentDistribution`, `useUpdateContentDistribution` | `@elevasis/ui/hooks` | Distribution data access |
|
|
69
|
+
| `useContentPipelineSummary` | `@elevasis/ui/hooks` | Pipeline roll-ups |
|
|
70
|
+
| `content` | `@elevasis/sdk/worker` | Workflow-side content adapter |
|
|
71
|
+
|
|
72
|
+
Read the generated contracts before changing typed boundaries:
|
|
73
|
+
|
|
74
|
+
`operations/node_modules/@elevasis/sdk/reference/scaffold/reference/contracts.md`
|
|
75
|
+
|
|
76
|
+
Look for the **Content Platform Primitives** section: item/attempt/distribution rows, review decisions, the status and pillar vocabularies, and the `ContentToolMap` adapter map.
|
|
77
|
+
|
|
78
|
+
## 1. Add a Pipeline
|
|
79
|
+
|
|
80
|
+
A pipeline is two catalog records plus the workflows that claim its steps.
|
|
81
|
+
|
|
82
|
+
<!-- doc-snippet:skip: illustrative excerpt -- catalog shape, not a compilable module -->
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
// core/config/organization-model.ts -- inside the content System's `ontology.catalogTypes`
|
|
86
|
+
'content:catalog/pipeline': {
|
|
87
|
+
entries: {
|
|
88
|
+
'podcast-to-clips': {
|
|
89
|
+
label: 'Podcast to Clips',
|
|
90
|
+
stepCatalog: 'content:catalog/podcast-to-clips-steps'
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
'content:catalog/podcast-to-clips-steps': {
|
|
95
|
+
entries: {
|
|
96
|
+
'transcript': { label: 'Transcript', order: 1 },
|
|
97
|
+
'clip-selection': { label: 'Clip Selection', order: 2 },
|
|
98
|
+
'publish': { label: 'Publish', order: 3 }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Then set `content.config.defaultPipelineId` if this should be the pipeline the UI opens first.
|
|
104
|
+
|
|
105
|
+
**The step catalog name is load-bearing.** `content:catalog/{pipelineId}-steps` must match the pipeline record's `stepCatalog` exactly. Nothing type-checks that string.
|
|
106
|
+
|
|
107
|
+
## 2. Produce a Step in a Workflow
|
|
108
|
+
|
|
109
|
+
One workflow per step. The workflow records an **attempt** against the item, tagged with the step it satisfies.
|
|
110
|
+
|
|
111
|
+
**Attempt lifecycle: write at start, patch at end.** Call `createAttempt` with `status: 'running'` and the `stepKey` _before_ doing the step's work, then call `updateAttempt` on that same row with the terminal status and the final payload once the work finishes. This is **one row per step execution, patched in place** -- never a start row plus a separate finish row. `attempt_number` counts per item, not per step, and there is a live unique constraint on the pair, so two rows for one step execution collide with genuine retries of that step and cannot be told apart from them.
|
|
112
|
+
|
|
113
|
+
A crashed step correctly leaves its row at `status: 'running'` forever. There is deliberately no reaper that flips a stale `running` row to `error` -- a stuck item is information an operator needs to see (`content:get` or `content:queue` surface it as-is), not a signal to paper over. If a nudge is wanted later it is a separate SLA concern, not something this write path should hide.
|
|
114
|
+
|
|
115
|
+
<!-- doc-snippet:skip: illustrative excerpt -- `resourceDescriptors` names the project's own resource-descriptors export -->
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
// operations/src/content/clip-selection.ts
|
|
119
|
+
import type { WorkflowDefinition } from '@elevasis/sdk'
|
|
120
|
+
import { content } from '@elevasis/sdk/worker'
|
|
121
|
+
import { resourceDescriptors } from '@core/config/organization-model'
|
|
122
|
+
import { z } from 'zod'
|
|
123
|
+
|
|
124
|
+
const inputSchema = z.object({ contentItemId: z.string().uuid() })
|
|
125
|
+
const outputSchema = z.object({ attemptId: z.string(), nextStepKey: z.string().nullable() })
|
|
126
|
+
|
|
127
|
+
export const clipSelectionWorkflow: WorkflowDefinition = {
|
|
128
|
+
config: {
|
|
129
|
+
resource: resourceDescriptors.clipSelection,
|
|
130
|
+
resourceId: resourceDescriptors.clipSelection.id,
|
|
131
|
+
name: 'Clip Selection',
|
|
132
|
+
description: 'Chooses clip candidates for a podcast episode.',
|
|
133
|
+
type: resourceDescriptors.clipSelection.kind,
|
|
134
|
+
version: '1.0.0',
|
|
135
|
+
status: 'dev'
|
|
136
|
+
},
|
|
137
|
+
contract: { inputSchema, outputSchema },
|
|
138
|
+
steps: {
|
|
139
|
+
select: {
|
|
140
|
+
id: 'select',
|
|
141
|
+
name: 'Select Clips',
|
|
142
|
+
description: 'Records a clip-selection attempt against the item.',
|
|
143
|
+
inputSchema,
|
|
144
|
+
outputSchema,
|
|
145
|
+
next: null,
|
|
146
|
+
handler: async (rawInput, context) => {
|
|
147
|
+
const input = rawInput as z.infer<typeof inputSchema>
|
|
148
|
+
const item = await content.getItem({ contentItemId: input.contentItemId })
|
|
149
|
+
|
|
150
|
+
// Write-at-start: create the row before doing any work, so a crash
|
|
151
|
+
// mid-step still leaves a `running` row an operator can find.
|
|
152
|
+
const attempt = await content.createAttempt({
|
|
153
|
+
contentItemId: item.id,
|
|
154
|
+
// Tags the attempt with the step it satisfies. Omit it and the
|
|
155
|
+
// attempt is stored with a null step -- the review UI's Step column
|
|
156
|
+
// renders "--" and nothing tells you which step produced the row.
|
|
157
|
+
stepKey: 'clip-selection',
|
|
158
|
+
status: 'running',
|
|
159
|
+
payload: {},
|
|
160
|
+
sourceExecutionId: context.executionId
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const clips = await selectClips(item)
|
|
164
|
+
|
|
165
|
+
// Patch-at-end: the same row, moved to its terminal status with the
|
|
166
|
+
// real payload. Never a second createAttempt call for this step.
|
|
167
|
+
await content.updateAttempt({
|
|
168
|
+
attemptId: attempt.id,
|
|
169
|
+
status: 'success',
|
|
170
|
+
payload: { clips }
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
return { attemptId: attempt.id, nextStepKey: 'publish' }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
entryPoint: 'select'
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`ContentToolMap` is the full surface, 13 methods: `createItem`, `getItem`, `listItems`, `updateItem`, `createAttempt`, `listAttempts`, `updateAttempt`, `createSourceAsset`, `getSourceAsset`, `listSourceAssets`, `updateSourceAsset`, `createDistribution`, `updateDistribution`. `organizationId` is injected server-side — never pass it from workflow code.
|
|
182
|
+
|
|
183
|
+
**`reviewItem` is deliberately not on this list.** Clearing a `queued` review gate is operator work, done through `elevasis-sdk content:review` (see `apps/docs/content/docs/sdk/sdk/cli-management.mdx`), not something a producer workflow can call. A producer approving its own output collapses the gate the review step exists to provide.
|
|
184
|
+
|
|
185
|
+
### Source Assets
|
|
186
|
+
|
|
187
|
+
`createSourceAsset`, `getSourceAsset`, `listSourceAssets`, and `updateSourceAsset` manage the raw material a producer works from -- an uploaded podcast episode, a transcript, a reference file -- independent of any content item. A producer lists or fetches source assets to pick material, then references the chosen one (`sourceAssetId`) when it calls `createItem`. Source assets have their own `kind`-keyed payload envelope, declared per-kind in the content System's OM catalog, the same pattern `createItem`'s payload uses.
|
|
188
|
+
|
|
189
|
+
## 3. Give Producers Their Instructions
|
|
190
|
+
|
|
191
|
+
There is no rules-document tier and no settings page. Voice, banned phrases, structural constraints, and the craft checklist go in the **producing workflow's own prompt**.
|
|
192
|
+
|
|
193
|
+
That is not a downgrade from a stored document — it is where the instructions were always read. The tier that used to sit here stored versioned prose that no step ever loaded and no measurement ever rewrote, so its contents reached the model only by being copied into a prompt anyway. Putting them in the prompt directly means they version with the code that reads them and cannot silently disagree with it.
|
|
194
|
+
|
|
195
|
+
Two consequences worth knowing:
|
|
196
|
+
|
|
197
|
+
- **Per-idea constraints belong with the idea, not in a separate document.** When the substance of a piece and the constraints on that substance come apart, a model will invent the substance to satisfy the constraints. Keep an entry and its rules as one object in whatever the workflow reads.
|
|
198
|
+
- **The `artifacts` platform tool still exists** and is a generic, kind-keyed document store. It is no longer part of content's story, and `artifacts_manage_content` is gone, so a `content:`-prefixed org-owned write is no longer granted by `content.manage`. Reach for it as a general capability, not as content's rules tier.
|
|
199
|
+
|
|
200
|
+
## 4. Customize the Review Screen
|
|
201
|
+
|
|
202
|
+
Start with slots on the shared page:
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
// ui/src/routes/content/items.$itemId.tsx
|
|
206
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
207
|
+
import { ContentItemReviewPage } from '@elevasis/ui/features/content'
|
|
208
|
+
import { Button } from '@mantine/core'
|
|
209
|
+
|
|
210
|
+
export const Route = createFileRoute('/content/items/$itemId')({ component: ReviewRoute })
|
|
211
|
+
|
|
212
|
+
function ReviewRoute() {
|
|
213
|
+
const { itemId } = Route.useParams()
|
|
214
|
+
return <ContentItemReviewPage itemId={itemId} headerActions={<Button>Send to Legal</Button>} />
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
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.
|
|
219
|
+
|
|
220
|
+
## 5. Extend Navigation
|
|
221
|
+
|
|
222
|
+
The content sidebar has two sections. **`Create` is where your own authoring surfaces go**; the
|
|
223
|
+
pipeline section below it is the shared review and tracking nav. Which one you are adding to decides
|
|
224
|
+
which prop you pass.
|
|
225
|
+
|
|
226
|
+
### Adding an authoring page (the common case)
|
|
227
|
+
|
|
228
|
+
Content creation is tenant-specific by nature — every tenant writes a different kind of thing,
|
|
229
|
+
through a different screen — so `Create` starts empty and you fill it. Pass `createItems`:
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
import { contentManifest, ContentSidebar } from '@elevasis/ui/features/content'
|
|
233
|
+
import type { SystemModule } from '@elevasis/ui/provider'
|
|
234
|
+
import type { NavItem } from '@elevasis/ui/layout'
|
|
235
|
+
import { IconPencil } from '@tabler/icons-react'
|
|
236
|
+
|
|
237
|
+
const createItems: NavItem[] = [{ label: 'Draft a Post', to: '/content/write', icon: IconPencil, exact: false }]
|
|
238
|
+
|
|
239
|
+
export const customContentManifest: SystemModule = {
|
|
240
|
+
...contentManifest,
|
|
241
|
+
sidebar: () => <ContentSidebar createItems={createItems} />
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Add as many as you like — a drafting screen, a brief builder, a batch generator. Until you declare
|
|
246
|
+
one, the section renders a "No authoring pages declared yet" hint rather than an empty gap.
|
|
247
|
+
|
|
248
|
+
**`Create` is a fixed platform label and is not vocabulary-driven.** Rename the page, not the
|
|
249
|
+
section.
|
|
250
|
+
|
|
251
|
+
### Adding to the pipeline nav (rare)
|
|
252
|
+
|
|
253
|
+
`items` replaces the pipeline list wholesale, so you must restate the shared entries:
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
import { CONTENT_ITEMS, ContentSidebar, ContentSidebarMiddle } from '@elevasis/ui/features/content'
|
|
257
|
+
import { IconChartBar } from '@tabler/icons-react'
|
|
258
|
+
|
|
259
|
+
const customItems: NavItem[] = [
|
|
260
|
+
...CONTENT_ITEMS,
|
|
261
|
+
{ label: 'Performance', to: '/content/performance', icon: IconChartBar, exact: false }
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
sidebar: () => (
|
|
265
|
+
<ContentSidebar>
|
|
266
|
+
<ContentSidebarMiddle createItems={createItems} items={customItems} />
|
|
267
|
+
</ContentSidebar>
|
|
268
|
+
)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**Prefer `createItems` whenever the page is an authoring surface.** `CONTENT_ITEMS` is a static
|
|
272
|
+
snapshot built from the default vocabulary, so spreading it pins the shared labels at their defaults
|
|
273
|
+
— a tenant that later renames its pipelines to "Campaigns" through `system.config.vocabulary` keeps
|
|
274
|
+
rendering "Pipelines" here, and nothing fails to tell you.
|
|
275
|
+
|
|
276
|
+
Replace `contentManifest` with `customContentManifest` in the local `SYSTEM_MANIFESTS` array and add
|
|
277
|
+
the matching route under `ui/src/routes/content/`.
|
|
278
|
+
|
|
279
|
+
## 6. Mark the System API-Ready
|
|
280
|
+
|
|
281
|
+
`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:
|
|
282
|
+
|
|
283
|
+
- 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.
|
|
284
|
+
- 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.
|
|
285
|
+
|
|
286
|
+
## Verify
|
|
287
|
+
|
|
288
|
+
Run the checks for the surfaces you touched:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
pnpm -C ui run check-types
|
|
292
|
+
pnpm -C operations run check
|
|
293
|
+
pnpm -C operations exec elevasis-sdk check
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
If you changed platform-level content contracts in the monorepo, the platform maintainer must also regenerate and verify scaffold output:
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
pnpm scaffold:sync
|
|
300
|
+
pnpm scaffold:verify
|
|
301
|
+
```
|
|
@@ -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 `@
|
|
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
|
-
|
|
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 '@
|
|
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
|
-
|
|
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.
|
|
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 -- `@
|
|
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 '@
|
|
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
|
-
|
|
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
|
-
|
|
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`,
|
|
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,
|
|
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. `
|
|
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
|
|