@elevasis/core 0.21.0 → 0.23.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 (132) hide show
  1. package/dist/index.d.ts +2518 -2169
  2. package/dist/index.js +2495 -1095
  3. package/dist/knowledge/index.d.ts +706 -1044
  4. package/dist/knowledge/index.js +9 -9
  5. package/dist/organization-model/index.d.ts +2518 -2169
  6. package/dist/organization-model/index.js +2495 -1095
  7. package/dist/test-utils/index.d.ts +826 -1014
  8. package/dist/test-utils/index.js +1894 -1032
  9. package/package.json +3 -3
  10. package/src/__tests__/template-core-compatibility.test.ts +11 -79
  11. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +852 -397
  12. package/src/auth/multi-tenancy/permissions.ts +20 -8
  13. package/src/business/README.md +2 -2
  14. package/src/business/acquisition/api-schemas.test.ts +175 -2
  15. package/src/business/acquisition/api-schemas.ts +132 -16
  16. package/src/business/acquisition/build-templates.test.ts +4 -4
  17. package/src/business/acquisition/build-templates.ts +72 -30
  18. package/src/business/acquisition/crm-state-actions.test.ts +13 -11
  19. package/src/business/acquisition/index.ts +12 -0
  20. package/src/business/acquisition/types.ts +7 -3
  21. package/src/business/clients/api-schemas.test.ts +115 -0
  22. package/src/business/clients/api-schemas.ts +158 -0
  23. package/src/business/clients/index.ts +1 -0
  24. package/src/business/deals/api-schemas.ts +8 -0
  25. package/src/business/index.ts +5 -2
  26. package/src/business/projects/types.ts +19 -0
  27. package/src/execution/engine/__tests__/fixtures/test-agents.ts +10 -8
  28. package/src/execution/engine/agent/core/__tests__/agent.test.ts +16 -12
  29. package/src/execution/engine/agent/core/__tests__/error-passthrough.test.ts +4 -3
  30. package/src/execution/engine/agent/core/types.ts +25 -15
  31. package/src/execution/engine/agent/index.ts +6 -4
  32. package/src/execution/engine/agent/reasoning/__tests__/request-builder.test.ts +24 -18
  33. package/src/execution/engine/index.ts +3 -0
  34. package/src/execution/engine/workflow/types.ts +9 -2
  35. package/src/knowledge/README.md +8 -7
  36. package/src/knowledge/__tests__/queries.test.ts +74 -73
  37. package/src/knowledge/format.ts +10 -9
  38. package/src/knowledge/index.ts +1 -1
  39. package/src/knowledge/published.ts +1 -1
  40. package/src/knowledge/queries.ts +26 -25
  41. package/src/organization-model/README.md +73 -26
  42. package/src/organization-model/__tests__/content-kinds-registry.test.ts +210 -0
  43. package/src/organization-model/__tests__/defaults.test.ts +76 -96
  44. package/src/organization-model/__tests__/domains/actions.test.ts +56 -0
  45. package/src/organization-model/__tests__/domains/customers.test.ts +299 -295
  46. package/src/organization-model/__tests__/domains/entities.test.ts +56 -0
  47. package/src/organization-model/__tests__/domains/goals.test.ts +493 -479
  48. package/src/organization-model/__tests__/domains/identity.test.ts +280 -279
  49. package/src/organization-model/__tests__/domains/navigation.test.ts +268 -212
  50. package/src/organization-model/__tests__/domains/offerings.test.ts +414 -419
  51. package/src/organization-model/__tests__/domains/policies.test.ts +323 -0
  52. package/src/organization-model/__tests__/domains/resource-mappings.test.ts +271 -271
  53. package/src/organization-model/__tests__/domains/resources.test.ts +310 -0
  54. package/src/organization-model/__tests__/domains/roles.test.ts +463 -347
  55. package/src/organization-model/__tests__/domains/statuses.test.ts +246 -243
  56. package/src/organization-model/__tests__/domains/systems.test.ts +209 -0
  57. package/src/organization-model/__tests__/flatten-additive-merge.test.ts +361 -0
  58. package/src/organization-model/__tests__/foundation.test.ts +74 -102
  59. package/src/organization-model/__tests__/get-resources-for-system.test.ts +144 -0
  60. package/src/organization-model/__tests__/graph.test.ts +899 -71
  61. package/src/organization-model/__tests__/knowledge.test.ts +209 -49
  62. package/src/organization-model/__tests__/lookup-helpers.test.ts +438 -0
  63. package/src/organization-model/__tests__/migration-helpers.test.ts +591 -0
  64. package/src/organization-model/__tests__/prospecting-ssot.test.ts +36 -27
  65. package/src/organization-model/__tests__/recursive-system-schema.test.ts +520 -0
  66. package/src/organization-model/__tests__/resolve.test.ts +174 -23
  67. package/src/organization-model/__tests__/schema.test.ts +291 -114
  68. package/src/organization-model/__tests__/surface-projection.test.ts +207 -97
  69. package/src/organization-model/catalogs/lead-gen.ts +144 -0
  70. package/src/organization-model/content-kinds/config.ts +36 -0
  71. package/src/organization-model/content-kinds/index.ts +74 -0
  72. package/src/organization-model/content-kinds/pipeline.ts +68 -0
  73. package/src/organization-model/content-kinds/registry.ts +44 -0
  74. package/src/organization-model/content-kinds/status.ts +71 -0
  75. package/src/organization-model/content-kinds/template.ts +83 -0
  76. package/src/organization-model/content-kinds/types.ts +117 -0
  77. package/src/organization-model/contracts.ts +13 -3
  78. package/src/organization-model/defaults.ts +499 -86
  79. package/src/organization-model/domains/actions.ts +239 -0
  80. package/src/organization-model/domains/customers.ts +78 -75
  81. package/src/organization-model/domains/entities.ts +144 -0
  82. package/src/organization-model/domains/goals.ts +83 -80
  83. package/src/organization-model/domains/knowledge.ts +76 -17
  84. package/src/organization-model/domains/navigation.ts +107 -384
  85. package/src/organization-model/domains/offerings.ts +71 -66
  86. package/src/organization-model/domains/policies.ts +102 -0
  87. package/src/organization-model/domains/projects.ts +14 -48
  88. package/src/organization-model/domains/prospecting.ts +62 -181
  89. package/src/organization-model/domains/resources.ts +145 -0
  90. package/src/organization-model/domains/roles.ts +96 -55
  91. package/src/organization-model/domains/sales.ts +10 -219
  92. package/src/organization-model/domains/shared.ts +57 -57
  93. package/src/organization-model/domains/statuses.ts +339 -130
  94. package/src/organization-model/domains/systems.ts +203 -0
  95. package/src/organization-model/foundation.ts +54 -67
  96. package/src/organization-model/graph/build.ts +682 -54
  97. package/src/organization-model/graph/link.ts +1 -1
  98. package/src/organization-model/graph/schema.ts +24 -9
  99. package/src/organization-model/graph/types.ts +20 -7
  100. package/src/organization-model/helpers.ts +231 -26
  101. package/src/organization-model/icons.ts +1 -0
  102. package/src/organization-model/index.ts +118 -5
  103. package/src/organization-model/migration-helpers.ts +249 -0
  104. package/src/organization-model/organization-graph.mdx +16 -15
  105. package/src/organization-model/organization-model.mdx +111 -44
  106. package/src/organization-model/published.ts +172 -19
  107. package/src/organization-model/resolve.ts +117 -54
  108. package/src/organization-model/schema.ts +654 -112
  109. package/src/organization-model/surface-projection.ts +116 -122
  110. package/src/organization-model/types.ts +146 -20
  111. package/src/platform/api/types.ts +38 -35
  112. package/src/platform/constants/versions.ts +1 -1
  113. package/src/platform/registry/__tests__/command-view.test.ts +6 -8
  114. package/src/platform/registry/__tests__/resource-link.test.ts +13 -8
  115. package/src/platform/registry/__tests__/resource-registry.integration.test.ts +16 -31
  116. package/src/platform/registry/__tests__/resource-registry.nested-systems.test.ts +245 -0
  117. package/src/platform/registry/__tests__/resource-registry.test.ts +2053 -2005
  118. package/src/platform/registry/__tests__/validation.test.ts +1347 -1086
  119. package/src/platform/registry/index.ts +14 -0
  120. package/src/platform/registry/resource-registry.ts +52 -2
  121. package/src/platform/registry/serialization.ts +241 -202
  122. package/src/platform/registry/serialized-types.ts +1 -0
  123. package/src/platform/registry/types.ts +411 -361
  124. package/src/platform/registry/validation.ts +745 -513
  125. package/src/projects/api-schemas.ts +290 -267
  126. package/src/reference/_generated/contracts.md +853 -397
  127. package/src/reference/glossary.md +23 -18
  128. package/src/supabase/database.types.ts +181 -0
  129. package/src/test-utils/test-utils.test.ts +1 -6
  130. package/src/organization-model/__tests__/domains/operations.test.ts +0 -203
  131. package/src/organization-model/domains/features.ts +0 -31
  132. package/src/organization-model/domains/operations.ts +0 -85
@@ -1,4 +1,4 @@
1
- import type { OrgKnowledgeNode } from '../organization-model/domains/knowledge'
1
+ import type { OrgKnowledgeNode } from '../organization-model/domains/knowledge'
2
2
  import type { KnowledgeMount, ParsedKnowledgePath } from './queries'
3
3
 
4
4
  // ---------------------------------------------------------------------------
@@ -9,7 +9,7 @@ import type { KnowledgeMount, ParsedKnowledgePath } from './queries'
9
9
  * Renders a list of `OrgKnowledgeNode` results as a human-friendly text table.
10
10
  *
11
11
  * Output format (one row per node):
12
- * `<kind> <id> <title> <summary (truncated to 80 chars)>`
12
+ * `<kind> <id> <title> — <summary (truncated to 80 chars)>`
13
13
  *
14
14
  * Returns `"(no results)"` when the array is empty.
15
15
  */
@@ -26,7 +26,7 @@ export function formatText(results: OrgKnowledgeNode[]): string {
26
26
 
27
27
  const rows = results.map((n) => {
28
28
  const summary = n.summary.length > 80 ? n.summary.slice(0, 77) + '...' : n.summary
29
- return `${n.kind.padEnd(kindWidth)} ${n.id.padEnd(idWidth)} ${n.title} ${summary}`
29
+ return `${n.kind.padEnd(kindWidth)} ${n.id.padEnd(idWidth)} ${n.title} — ${summary}`
30
30
  })
31
31
 
32
32
  return [header, divider, ...rows].join('\n')
@@ -41,10 +41,10 @@ export function formatText(results: OrgKnowledgeNode[]): string {
41
41
  *
42
42
  * Shape: `{ path, mount, args, results }`
43
43
  *
44
- * - `path` the original path string passed to `parsePath`
45
- * - `mount` the resolved mount axis
46
- * - `args` the parsed arguments array
47
- * - `results` the query results (array of `OrgKnowledgeNode` or string IDs)
44
+ * - `path` — the original path string passed to `parsePath`
45
+ * - `mount` — the resolved mount axis
46
+ * - `args` — the parsed arguments array
47
+ * - `results` — the query results (array of `OrgKnowledgeNode` or string IDs)
48
48
  */
49
49
  export interface KnowledgeJsonEnvelope {
50
50
  path: string
@@ -57,10 +57,10 @@ export interface KnowledgeJsonEnvelope {
57
57
  * Formats query results as a wrapped JSON envelope string.
58
58
  *
59
59
  * The envelope shape is `{ path, mount, args, results }`. This is intentionally
60
- * NOT flat `{ results }` consumers (agent skills, jq pipelines) need the
60
+ * NOT flat `{ results }` — consumers (agent skills, jq pipelines) need the
61
61
  * mount + args to know how to interpret the results array.
62
62
  *
63
- * @param input.path - The original path string (e.g. `"/by-feature/sales.crm"`).
63
+ * @param input.path - The original path string (e.g. `"/by-system/sales.crm"`).
64
64
  * @param input.mount - The resolved mount axis.
65
65
  * @param input.args - The parsed argument array.
66
66
  * @param input.results - The query results.
@@ -97,3 +97,4 @@ export function formatIdsOnly(results: OrgKnowledgeNode[] | string[]): string {
97
97
  const ids = results.map((r) => (typeof r === 'string' ? r : r.id))
98
98
  return ids.join('\n')
99
99
  }
100
+
@@ -1,4 +1,4 @@
1
- export { byFeature, byKind, byOwner, governs, governedBy, parsePath } from './queries'
1
+ export { bySystem, byKind, byOwner, governs, governedBy, parsePath } from './queries'
2
2
  export type { KnowledgeMount, ParsedKnowledgePath } from './queries'
3
3
 
4
4
  export { formatText, formatJson, formatIdsOnly } from './format'
@@ -1,4 +1,4 @@
1
- export { byFeature, byKind, byOwner, governs, governedBy, parsePath } from './queries'
1
+ export { bySystem, byKind, byOwner, governs, governedBy, parsePath } from './queries'
2
2
  export type { KnowledgeMount, ParsedKnowledgePath } from './queries'
3
3
 
4
4
  export { formatText, formatJson, formatIdsOnly } from './format'
@@ -1,4 +1,4 @@
1
- import type { OrganizationGraph } from '../organization-model/graph/types'
1
+ import type { OrganizationGraph } from '../organization-model/graph/types'
2
2
  import type { OrgKnowledgeKind, OrgKnowledgeNode } from '../organization-model/domains/knowledge'
3
3
 
4
4
  // ---------------------------------------------------------------------------
@@ -35,17 +35,17 @@ function buildKnowledgeSourceIdMap(graph: OrganizationGraph): Map<string, string
35
35
 
36
36
  /**
37
37
  * Returns all knowledge nodes whose `governs` edges point to the given
38
- * featureId (graph node ID: `feature:<featureId>`).
38
+ * systemId (graph node ID: `system:<systemId>`).
39
39
  *
40
40
  * @param graph - The built OrganizationGraph.
41
- * @param featureId - The dotted feature id (e.g. `sales.crm`).
41
+ * @param systemId - The dotted system id (e.g. `sales.crm`).
42
42
  */
43
- export function byFeature(
43
+ export function bySystem(
44
44
  graph: OrganizationGraph,
45
- featureId: string,
45
+ systemId: string,
46
46
  knowledgeNodes: OrgKnowledgeNode[]
47
47
  ): OrgKnowledgeNode[] {
48
- const targetGraphNodeId = `feature:${featureId}`
48
+ const targetGraphNodeId = `system:${systemId}`
49
49
 
50
50
  // Find all knowledge graph node IDs that have a `governs` edge to the target
51
51
  const governingKnowledgeNodeIds = new Set<string>()
@@ -107,7 +107,7 @@ export function byOwner(
107
107
  * @param graph - The built OrganizationGraph.
108
108
  * @param nodeId - The OM knowledge node id (e.g. `knowledge.outreach-playbook`).
109
109
  * Also accepts the graph node ID format (`knowledge:knowledge.outreach-playbook`).
110
- * @returns Array of target graph node IDs (e.g. `['feature:sales.crm', ...]`).
110
+ * @returns Array of target graph node IDs (e.g. `['system:sales.crm', ...]`).
111
111
  */
112
112
  export function governs(graph: OrganizationGraph, nodeId: string): string[] {
113
113
  const graphNodeId = nodeId.startsWith('knowledge:') ? nodeId : toGraphNodeId(nodeId)
@@ -126,16 +126,16 @@ export function governs(graph: OrganizationGraph, nodeId: string): string[] {
126
126
  * (incoming `governs` edges pointing to `nodeId`).
127
127
  *
128
128
  * @param graph - The built OrganizationGraph.
129
- * @param nodeId - The target graph node ID (e.g. `feature:sales.crm`) or a
130
- * bare feature id (e.g. `sales.crm` will be prefixed with `feature:`).
129
+ * @param nodeId - The target graph node ID (e.g. `system:sales.crm`) or a
130
+ * bare system id (e.g. `sales.crm` — will be prefixed with `system:`).
131
131
  * @returns Array of source graph node IDs (e.g. `['knowledge:knowledge.outreach-playbook', ...]`).
132
132
  */
133
133
  export function governedBy(graph: OrganizationGraph, nodeId: string): string[] {
134
- // Accept both `feature:sales.crm` and bare `sales.crm`
134
+ // Accept both `system:sales.crm` and bare `sales.crm`
135
135
  const targetId =
136
- nodeId.startsWith('feature:') || nodeId.startsWith('knowledge:') || nodeId.startsWith('resource:')
136
+ nodeId.startsWith('system:') || nodeId.startsWith('knowledge:') || nodeId.startsWith('resource:')
137
137
  ? nodeId
138
- : `feature:${nodeId}`
138
+ : `system:${nodeId}`
139
139
 
140
140
  const results: string[] = []
141
141
  for (const edge of graph.edges) {
@@ -151,7 +151,7 @@ export function governedBy(graph: OrganizationGraph, nodeId: string): string[] {
151
151
  // ---------------------------------------------------------------------------
152
152
 
153
153
  /** The recognized mount axes for Knowledge Map paths. */
154
- export type KnowledgeMount = 'by-feature' | 'by-kind' | 'by-owner' | 'graph' | 'node'
154
+ export type KnowledgeMount = 'by-system' | 'by-kind' | 'by-owner' | 'graph' | 'node'
155
155
 
156
156
  /**
157
157
  * The result of parsing a Knowledge Map path string.
@@ -159,7 +159,7 @@ export type KnowledgeMount = 'by-feature' | 'by-kind' | 'by-owner' | 'graph' | '
159
159
  * Shape: `{ mount: KnowledgeMount, args: string[] }`
160
160
  *
161
161
  * Per-mount arg arrays:
162
- * - `by-feature`: `[featureId]` (e.g. `['sales.crm']`)
162
+ * - `by-system`: `[systemId]` (e.g. `['sales.crm']`)
163
163
  * - `by-kind`: `[kind]` (e.g. `['playbook']`)
164
164
  * - `by-owner`: `[ownerId]` (e.g. `['role.ops-lead']`)
165
165
  * - `graph`: `[nodeId, verb]` where verb is `'governs'` or `'governed-by'`
@@ -174,12 +174,12 @@ export interface ParsedKnowledgePath {
174
174
  * Parses a Knowledge Map path string into a `{ mount, args }` descriptor.
175
175
  *
176
176
  * Supported path patterns:
177
- * `/by-feature/<featureId>` `{ mount: 'by-feature', args: ['<featureId>'] }`
178
- * `/by-kind/<kind>` `{ mount: 'by-kind', args: ['<kind>'] }`
179
- * `/by-owner/<ownerId>` `{ mount: 'by-owner', args: ['<ownerId>'] }`
180
- * `/graph/<nodeId>/governs` `{ mount: 'graph', args: ['<nodeId>', 'governs'] }`
181
- * `/graph/<nodeId>/governed-by` `{ mount: 'graph', args: ['<nodeId>', 'governed-by'] }`
182
- * `/<nodeId>` `{ mount: 'node', args: ['<nodeId>'] }`
177
+ * `/by-system/<systemId>` -> `{ mount: 'by-system', args: ['<systemId>'] }`
178
+ * `/by-kind/<kind>` → `{ mount: 'by-kind', args: ['<kind>'] }`
179
+ * `/by-owner/<ownerId>` → `{ mount: 'by-owner', args: ['<ownerId>'] }`
180
+ * `/graph/<nodeId>/governs` → `{ mount: 'graph', args: ['<nodeId>', 'governs'] }`
181
+ * `/graph/<nodeId>/governed-by` → `{ mount: 'graph', args: ['<nodeId>', 'governed-by'] }`
182
+ * `/<nodeId>` → `{ mount: 'node', args: ['<nodeId>'] }`
183
183
  *
184
184
  * The path MUST start with `/`. Trailing slashes are stripped before parsing.
185
185
  *
@@ -205,12 +205,12 @@ export function parsePath(pathString: string): ParsedKnowledgePath {
205
205
 
206
206
  const [first, ...rest] = segments
207
207
 
208
- // /by-feature/<featureId>
209
- if (first === 'by-feature') {
208
+ // /by-system/<systemId>
209
+ if (first === 'by-system') {
210
210
  if (rest.length === 0) {
211
- throw new Error(`parsePath: /by-feature requires a featureId argument, got: "${pathString}"`)
211
+ throw new Error(`parsePath: /by-system requires a systemId argument, got: "${pathString}"`)
212
212
  }
213
- return { mount: 'by-feature', args: [rest.join('/')] }
213
+ return { mount: 'by-system', args: [rest.join('/')] }
214
214
  }
215
215
 
216
216
  // /by-kind/<kind>
@@ -251,6 +251,7 @@ export function parsePath(pathString: string): ParsedKnowledgePath {
251
251
  }
252
252
 
253
253
  throw new Error(
254
- `parsePath: unrecognized path pattern "${pathString}". Supported: /by-feature/<id>, /by-kind/<kind>, /by-owner/<id>, /graph/<nodeId>/governs, /graph/<nodeId>/governed-by, /<nodeId>`
254
+ `parsePath: unrecognized path pattern "${pathString}". Supported: /by-system/<id>, /by-kind/<kind>, /by-owner/<id>, /graph/<nodeId>/governs, /graph/<nodeId>/governed-by, /<nodeId>`
255
255
  )
256
256
  }
257
+
@@ -1,20 +1,19 @@
1
1
  # Organization Model
2
2
 
3
- The organization model is the published semantic contract that maps a tenant's product shape to flat feature hierarchy, shell navigation, business semantics, and graph bindings.
3
+ The organization model is the published semantic contract that maps a tenant's System hierarchy, shell navigation, business semantics, resource governance, and graph bindings.
4
4
 
5
- Use this module when resolving or validating an organization's contract before wiring UI shells, routing, feature gates, or domain-specific capability checks.
5
+ Use this module when resolving or validating an organization's contract before wiring UI shells, routing, system gates, or domain-specific capability checks.
6
6
 
7
7
  ## Published Exports
8
8
 
9
9
  The public entry point exposes:
10
10
 
11
11
  - `OrganizationModelSchema`
12
- - `FeatureSchema`
13
12
  - `DEFAULT_ORGANIZATION_MODEL`
14
13
  - `defineOrganizationModel`
15
14
  - `resolveOrganizationModel`
16
15
  - `createFoundationOrganizationModel`
17
- - node ID and feature helper types
16
+ - node ID and System helper types
18
17
  - `OrganizationModel` and supporting domain types
19
18
 
20
19
  Import it from the published subpath:
@@ -36,8 +35,8 @@ The model is versioned and currently validates against `version: 1`.
36
35
  Top-level fields:
37
36
 
38
37
  - `version`
38
+ - `domainMetadata`
39
39
  - `branding`
40
- - `features`
41
40
  - `navigation`
42
41
  - `sales`
43
42
  - `prospecting`
@@ -47,35 +46,83 @@ Top-level fields:
47
46
  - `offerings`
48
47
  - `roles`
49
48
  - `goals`
49
+ - `systems`
50
+ - `resources`
51
+ - `capabilities`
52
+ - `policies`
50
53
  - `statuses`
51
- - `operations`
54
+ - `knowledge`
52
55
 
53
- Resources bind to the graph from deployment metadata through `links` and `category`.
56
+ The pure collection domains are id-keyed maps: `systems`, `roles`, `goals`, `customers`, `offerings`, `resources`, `capabilities`, `policies`, and `statuses`. The map key must match the entry `id`. Entries carry `order` for deterministic ordered views; use `listDomain(record)` when order matters.
54
57
 
55
- ## Feature Set
58
+ Resource identity is authored in `resources`. Runtime workflows, agents, integrations, and scripts import those descriptors, derive `resourceId` and kind from them, and attach executable behavior in operations code.
56
59
 
57
- Feature hierarchy is authored as a flat array. Dotted IDs define parent/child relationships:
60
+ ## System Set
61
+
62
+ System hierarchy is authored as an id-keyed `systems` map. Dotted IDs and `parentSystemId` define parent/child relationships:
58
63
 
59
64
  ```ts
60
- features: [
61
- { id: 'dashboard', label: 'Dashboard', enabled: true, path: '/', uiPosition: 'sidebar-primary' },
62
- { id: 'sales', label: 'Sales', enabled: true, uiPosition: 'sidebar-primary' },
63
- { id: 'sales.crm', label: 'CRM', enabled: true, path: '/crm' },
64
- { id: 'operations.resources', label: 'Resources', enabled: true, path: '/operations/resources' }
65
- ]
65
+ systems: {
66
+ dashboard: { id: 'dashboard', order: 10, label: 'Dashboard', lifecycle: 'active' },
67
+ sales: { id: 'sales', order: 20, label: 'Sales', lifecycle: 'active' },
68
+ clients: { id: 'clients', order: 30, label: 'Clients', lifecycle: 'active' },
69
+ projects: { id: 'projects', order: 40, label: 'Projects', lifecycle: 'active' }
70
+ }
66
71
  ```
67
72
 
68
- Containers omit `path`; leaves provide `path`. `uiPosition`, `requiresAdmin`, and `devOnly` inherit from ancestors.
69
- Development-only features remain in the contract with `enabled: true` and `devOnly: true`; shell consumers hide them and guard their paths outside development mode.
73
+ Systems describe semantic ownership, hierarchy, lifecycle, access, and governance. Sidebar presentation is authored separately under `navigation.sidebar`; UI-backed Systems may still provide `ui.path` for route matching during the migration, but they do not author composed surface lists. Lifecycle values such as `active`, `beta`, `deprecated`, and `archived` replace the old feature enabled/dev-only split.
74
+
75
+ ## Sidebar Navigation
76
+
77
+ Shell navigation is authored as a recursive sidebar tree:
78
+
79
+ ```ts
80
+ navigation: {
81
+ sidebar: {
82
+ primary: {
83
+ dashboard: {
84
+ type: 'surface',
85
+ order: 10,
86
+ label: 'Dashboard',
87
+ path: '/',
88
+ surfaceType: 'dashboard',
89
+ targets: { systems: ['dashboard'] }
90
+ },
91
+ business: {
92
+ type: 'group',
93
+ order: 20,
94
+ label: 'Business',
95
+ children: {
96
+ clients: {
97
+ type: 'surface',
98
+ order: 20,
99
+ label: 'Clients',
100
+ path: '/clients',
101
+ surfaceType: 'list',
102
+ targets: { systems: ['clients'] }
103
+ }
104
+ }
105
+ }
106
+ },
107
+ bottom: {}
108
+ }
109
+ }
110
+ ```
111
+
112
+ Routeable sidebar leaves are projected into flat semantic surface DTOs by `projectOrganizationSurfaces(model)`. Do not author top-level `surfaces` or `navigationGroups` fields on `OrganizationModel`.
70
113
 
71
114
  ## Graph IDs
72
115
 
73
116
  Cross-collection links use kind-prefixed IDs:
74
117
 
75
- - `feature:sales.crm`
118
+ - `system:sales.crm`
76
119
  - `integration:instantly`
77
120
  - `resource:lead-import`
78
- - `capability:operations.queue.review`
121
+ - `action:operations.queue.review`
122
+
123
+ ## Resource Descriptors
124
+
125
+ The OM Resources domain is governance-only. Descriptors declare canonical `id`, required `systemPath`, governance `status`, and optional role ownership. `DeploymentSpec` remains the runtime/deploy assembly around those descriptors, not a second resource identity catalog.
79
126
 
80
127
  ## Resolution Semantics
81
128
 
@@ -83,20 +130,20 @@ Cross-collection links use kind-prefixed IDs:
83
130
  - `resolveOrganizationModel()` deep-merges a partial override into the default model, then validates it.
84
131
  - `createFoundationOrganizationModel()` resolves the canonical model and returns UI-facing helper outputs.
85
132
  - Plain objects merge recursively.
133
+ - Id-keyed domain maps merge additively by key.
86
134
  - Arrays replace the default value.
87
135
  - Missing fields fall back to `DEFAULT_ORGANIZATION_MODEL`.
88
136
 
89
137
  ## Referential Integrity
90
138
 
91
- - Feature IDs must be unique.
92
- - Child feature IDs require ancestor feature nodes.
93
- - Container features omit `path`.
94
- - Leaf features provide `path`.
95
- - Resource links are validated against graph node IDs during registry registration.
139
+ - System IDs must be unique.
140
+ - Child System IDs require valid ancestors or `parentSystemId` links.
141
+ - Systems with UI provide `ui.path`.
142
+ - Systems, resources, roles, knowledge nodes, and goals must resolve their declared cross-references.
96
143
 
97
144
  ## Practical Guidance
98
145
 
99
146
  - Use `resolveOrganizationModel()` when you need a runtime-safe model.
100
147
  - Use `defineOrganizationModel()` when authoring static overrides.
101
- - Keep feature IDs stable because shell routing, gating, breadcrumbs, and docs depend on them.
102
- - Put semantic resource relationships on resource metadata, not on feature nodes.
148
+ - Keep System IDs stable because shell routing, gating, breadcrumbs, and docs depend on them.
149
+ - Put resource identity and governance in `resources`; attach executable behavior in operations.
@@ -0,0 +1,210 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ CONTENT_KIND_REGISTRY,
4
+ defineContentType,
5
+ lookupContentType,
6
+ pipelineKind,
7
+ stageKind,
8
+ templateKind,
9
+ templateStepKind,
10
+ statusFlowKind,
11
+ statusKind,
12
+ configKvKind
13
+ } from '../content-kinds/index'
14
+ import type { ContentTypeDefinition, ContentTypeKey } from '../content-kinds/types'
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // CONTENT_KIND_REGISTRY shape (D8: static object literal)
18
+ // ---------------------------------------------------------------------------
19
+
20
+ describe('CONTENT_KIND_REGISTRY', () => {
21
+ it('contains all 7 expected keys', () => {
22
+ const expectedKeys: ContentTypeKey[] = [
23
+ 'schema:pipeline',
24
+ 'schema:stage',
25
+ 'schema:template',
26
+ 'schema:template-step',
27
+ 'schema:status-flow',
28
+ 'schema:status',
29
+ 'config:kv'
30
+ ]
31
+ for (const key of expectedKeys) {
32
+ expect(CONTENT_KIND_REGISTRY).toHaveProperty(key)
33
+ }
34
+ })
35
+
36
+ it('maps schema:pipeline to the pipelineKind definition', () => {
37
+ expect(CONTENT_KIND_REGISTRY['schema:pipeline']).toBe(pipelineKind)
38
+ })
39
+
40
+ it('maps schema:stage to the stageKind definition', () => {
41
+ expect(CONTENT_KIND_REGISTRY['schema:stage']).toBe(stageKind)
42
+ })
43
+
44
+ it('maps schema:template to the templateKind definition', () => {
45
+ expect(CONTENT_KIND_REGISTRY['schema:template']).toBe(templateKind)
46
+ })
47
+
48
+ it('maps schema:template-step to the templateStepKind definition', () => {
49
+ expect(CONTENT_KIND_REGISTRY['schema:template-step']).toBe(templateStepKind)
50
+ })
51
+
52
+ it('maps schema:status-flow to the statusFlowKind definition', () => {
53
+ expect(CONTENT_KIND_REGISTRY['schema:status-flow']).toBe(statusFlowKind)
54
+ })
55
+
56
+ it('maps schema:status to the statusKind definition', () => {
57
+ expect(CONTENT_KIND_REGISTRY['schema:status']).toBe(statusKind)
58
+ })
59
+
60
+ it('maps config:kv to the configKvKind definition', () => {
61
+ expect(CONTENT_KIND_REGISTRY['config:kv']).toBe(configKvKind)
62
+ })
63
+
64
+ it('has exactly 7 keys (no extra unintended entries)', () => {
65
+ expect(Object.keys(CONTENT_KIND_REGISTRY)).toHaveLength(7)
66
+ })
67
+
68
+ it('every entry has kind, type, payloadSchema, label, and description fields', () => {
69
+ for (const [key, def] of Object.entries(CONTENT_KIND_REGISTRY)) {
70
+ expect(def).toHaveProperty('kind')
71
+ expect(def).toHaveProperty('type')
72
+ expect(def).toHaveProperty('payloadSchema')
73
+ expect(typeof def.kind).toBe('string')
74
+ expect(typeof def.type).toBe('string')
75
+ expect(def.label).toBeTruthy()
76
+ expect(def.description).toBeTruthy()
77
+ // key must equal kind:type
78
+ expect(key).toBe(`${def.kind}:${def.type}`)
79
+ }
80
+ })
81
+ })
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // parentTypes constraint
85
+ // ---------------------------------------------------------------------------
86
+
87
+ describe('parentTypes constraints', () => {
88
+ it('schema:pipeline has empty parentTypes (top-level, no parent required)', () => {
89
+ expect(pipelineKind.parentTypes).toEqual([])
90
+ })
91
+
92
+ it('schema:stage parentTypes is ["schema:pipeline"]', () => {
93
+ expect(stageKind.parentTypes).toEqual(['schema:pipeline'])
94
+ })
95
+
96
+ it('schema:template has empty parentTypes', () => {
97
+ expect(templateKind.parentTypes).toEqual([])
98
+ })
99
+
100
+ it('schema:template-step parentTypes is ["schema:template"]', () => {
101
+ expect(templateStepKind.parentTypes).toEqual(['schema:template'])
102
+ })
103
+
104
+ it('schema:status-flow has empty parentTypes', () => {
105
+ expect(statusFlowKind.parentTypes).toEqual([])
106
+ })
107
+
108
+ it('schema:status parentTypes is ["schema:status-flow"]', () => {
109
+ expect(statusKind.parentTypes).toEqual(['schema:status-flow'])
110
+ })
111
+
112
+ it('config:kv has empty parentTypes', () => {
113
+ expect(configKvKind.parentTypes).toEqual([])
114
+ })
115
+ })
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // lookupContentType
119
+ // ---------------------------------------------------------------------------
120
+
121
+ describe('lookupContentType', () => {
122
+ it('returns the correct definition for schema:pipeline', () => {
123
+ const def = lookupContentType('schema', 'pipeline')
124
+ expect(def).toBe(pipelineKind)
125
+ })
126
+
127
+ it('returns the correct definition for schema:stage', () => {
128
+ const def = lookupContentType('schema', 'stage')
129
+ expect(def).toBe(stageKind)
130
+ })
131
+
132
+ it('returns the correct definition for config:kv', () => {
133
+ const def = lookupContentType('config', 'kv')
134
+ expect(def).toBe(configKvKind)
135
+ })
136
+
137
+ it('returns undefined for an unregistered kind (D2: not an error)', () => {
138
+ const def = lookupContentType('tenant', 'custom-thing')
139
+ expect(def).toBeUndefined()
140
+ })
141
+
142
+ it('returns undefined for unknown kind with valid type', () => {
143
+ const def = lookupContentType('unknown', 'pipeline')
144
+ expect(def).toBeUndefined()
145
+ })
146
+
147
+ it('returns undefined for valid kind with unknown type', () => {
148
+ const def = lookupContentType('schema', 'not-a-real-type')
149
+ expect(def).toBeUndefined()
150
+ })
151
+
152
+ it('returns undefined for empty kind string', () => {
153
+ const def = lookupContentType('', 'pipeline')
154
+ expect(def).toBeUndefined()
155
+ })
156
+
157
+ it('returns undefined for empty type string', () => {
158
+ const def = lookupContentType('schema', '')
159
+ expect(def).toBeUndefined()
160
+ })
161
+ })
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // defineContentType (identity factory, L16)
165
+ // ---------------------------------------------------------------------------
166
+
167
+ describe('defineContentType', () => {
168
+ it('is an identity function — returns the same object reference', () => {
169
+ const def: ContentTypeDefinition<{ name: string }> = {
170
+ kind: 'tenant',
171
+ type: 'custom',
172
+ payloadSchema: { safeParse: () => ({ success: true, data: { name: 'x' } }) } as unknown as ContentTypeDefinition<{
173
+ name: string
174
+ }>['payloadSchema'],
175
+ label: 'Custom',
176
+ description: 'A tenant-defined type.',
177
+ parentTypes: []
178
+ }
179
+ const result = defineContentType(def)
180
+ expect(result).toBe(def)
181
+ })
182
+
183
+ it('does NOT mutate the input definition', () => {
184
+ const def: ContentTypeDefinition<unknown> = {
185
+ kind: 'schema',
186
+ type: 'pipeline',
187
+ payloadSchema: {
188
+ safeParse: () => ({ success: true, data: {} })
189
+ } as unknown as ContentTypeDefinition<unknown>['payloadSchema'],
190
+ parentTypes: []
191
+ }
192
+ const original = { ...def }
193
+ defineContentType(def)
194
+ expect(def).toEqual(original)
195
+ })
196
+
197
+ it('does NOT register the definition globally (no side-effects on CONTENT_KIND_REGISTRY)', () => {
198
+ const tenantKey = 'tenant:my-new-type'
199
+ defineContentType({
200
+ kind: 'tenant',
201
+ type: 'my-new-type',
202
+ payloadSchema: {
203
+ safeParse: () => ({ success: true, data: {} })
204
+ } as unknown as ContentTypeDefinition<unknown>['payloadSchema'],
205
+ parentTypes: []
206
+ })
207
+ // After calling defineContentType, the registry must remain unchanged (D8: static)
208
+ expect(CONTENT_KIND_REGISTRY).not.toHaveProperty(tenantKey)
209
+ })
210
+ })