@elevasis/core 0.15.1 → 0.17.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 (72) hide show
  1. package/dist/index.d.ts +1662 -23
  2. package/dist/index.js +171 -24
  3. package/dist/knowledge/index.d.ts +1340 -0
  4. package/dist/knowledge/index.js +138 -0
  5. package/dist/organization-model/index.d.ts +1662 -23
  6. package/dist/organization-model/index.js +171 -24
  7. package/dist/test-utils/index.d.ts +711 -10
  8. package/dist/test-utils/index.js +159 -16
  9. package/package.json +7 -3
  10. package/src/__tests__/publish.test.ts +14 -13
  11. package/src/__tests__/template-core-compatibility.test.ts +4 -4
  12. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +1265 -1154
  13. package/src/auth/multi-tenancy/index.ts +3 -0
  14. package/src/auth/multi-tenancy/theme-presets.ts +45 -0
  15. package/src/auth/multi-tenancy/types.ts +57 -83
  16. package/src/auth/multi-tenancy/users/api-schemas.ts +165 -194
  17. package/src/business/acquisition/activity-events.ts +1 -1
  18. package/src/business/acquisition/api-schemas.ts +1196 -1177
  19. package/src/business/acquisition/crm-state-actions.test.ts +139 -139
  20. package/src/business/acquisition/types.ts +381 -390
  21. package/src/business/crm/api-schemas.ts +40 -0
  22. package/src/business/crm/index.ts +1 -0
  23. package/src/business/deals/api-schemas.ts +79 -0
  24. package/src/business/deals/index.ts +1 -0
  25. package/src/business/projects/types.ts +124 -88
  26. package/src/execution/core/runner-types.ts +61 -80
  27. package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-tools.ts +105 -104
  28. package/src/execution/engine/tools/integration/server/adapters/instantly/instantly-tools.ts +1474 -1473
  29. package/src/execution/engine/tools/integration/server/adapters/millionverifier/millionverifier-tools.ts +103 -102
  30. package/src/execution/engine/tools/integration/server/adapters/signature-api/signature-api-tools.ts +182 -179
  31. package/src/execution/engine/tools/integration/server/adapters/stripe/stripe-tools.ts +310 -309
  32. package/src/execution/engine/tools/integration/tool.ts +255 -253
  33. package/src/execution/engine/tools/lead-service-types.ts +895 -894
  34. package/src/execution/engine/tools/messages.ts +43 -0
  35. package/src/execution/engine/tools/platform/acquisition/types.ts +2 -1
  36. package/src/execution/engine/tools/platform/email/types.ts +97 -96
  37. package/src/execution/engine/tools/types.ts +234 -233
  38. package/src/execution/engine/workflow/types.ts +195 -193
  39. package/src/execution/external/api-schemas.ts +40 -0
  40. package/src/execution/external/index.ts +1 -0
  41. package/src/knowledge/README.md +32 -0
  42. package/src/knowledge/__tests__/queries.test.ts +504 -0
  43. package/src/knowledge/format.ts +99 -0
  44. package/src/knowledge/index.ts +5 -0
  45. package/src/knowledge/published.ts +5 -0
  46. package/src/knowledge/queries.ts +256 -0
  47. package/src/organization-model/__tests__/defaults.test.ts +172 -172
  48. package/src/organization-model/__tests__/foundation.test.ts +7 -7
  49. package/src/organization-model/__tests__/icons.test.ts +27 -0
  50. package/src/organization-model/__tests__/knowledge.test.ts +214 -0
  51. package/src/organization-model/contracts.ts +17 -15
  52. package/src/organization-model/defaults.ts +74 -19
  53. package/src/organization-model/domains/knowledge.ts +53 -0
  54. package/src/organization-model/domains/navigation.ts +416 -399
  55. package/src/organization-model/domains/shared.ts +6 -5
  56. package/src/organization-model/foundation.ts +10 -6
  57. package/src/organization-model/graph/build.ts +209 -182
  58. package/src/organization-model/graph/schema.ts +37 -34
  59. package/src/organization-model/graph/types.ts +47 -31
  60. package/src/organization-model/icons.ts +81 -0
  61. package/src/organization-model/index.ts +8 -3
  62. package/src/organization-model/organization-model.mdx +1 -1
  63. package/src/organization-model/published.ts +103 -86
  64. package/src/organization-model/schema.ts +90 -85
  65. package/src/organization-model/types.ts +40 -33
  66. package/src/platform/index.ts +23 -27
  67. package/src/platform/registry/index.ts +0 -4
  68. package/src/platform/registry/resource-registry.ts +0 -77
  69. package/src/platform/registry/serialized-types.ts +148 -219
  70. package/src/platform/registry/stats-types.ts +60 -60
  71. package/src/reference/_generated/contracts.md +1265 -1154
  72. package/src/platform/registry/__tests__/resource-registry.list-executable.test.ts +0 -393
@@ -0,0 +1,256 @@
1
+ import type { OrganizationGraph } from '../organization-model/graph/types'
2
+ import type { OrgKnowledgeKind, OrgKnowledgeNode } from '../organization-model/domains/knowledge'
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Internal helpers
6
+ // ---------------------------------------------------------------------------
7
+
8
+ /**
9
+ * Graph node ID prefix for knowledge nodes.
10
+ * Graph node IDs follow the convention: `knowledge:<om-node-id>`
11
+ * e.g. `knowledge:knowledge.outreach-playbook`
12
+ */
13
+ function toGraphNodeId(omNodeId: string): string {
14
+ return `knowledge:${omNodeId}`
15
+ }
16
+
17
+ /**
18
+ * Resolve the sourceId of every knowledge node in the graph, keyed by graph
19
+ * node ID. The `sourceId` on a knowledge graph node is the OM node id
20
+ * (e.g. `knowledge.outreach-playbook`).
21
+ */
22
+ function buildKnowledgeSourceIdMap(graph: OrganizationGraph): Map<string, string> {
23
+ const map = new Map<string, string>()
24
+ for (const node of graph.nodes) {
25
+ if (node.kind === 'knowledge' && node.sourceId) {
26
+ map.set(node.id, node.sourceId)
27
+ }
28
+ }
29
+ return map
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Query functions
34
+ // ---------------------------------------------------------------------------
35
+
36
+ /**
37
+ * Returns all knowledge nodes whose `governs` edges point to the given
38
+ * featureId (graph node ID: `feature:<featureId>`).
39
+ *
40
+ * @param graph - The built OrganizationGraph.
41
+ * @param featureId - The dotted feature id (e.g. `sales.crm`).
42
+ */
43
+ export function byFeature(
44
+ graph: OrganizationGraph,
45
+ featureId: string,
46
+ knowledgeNodes: OrgKnowledgeNode[]
47
+ ): OrgKnowledgeNode[] {
48
+ const targetGraphNodeId = `feature:${featureId}`
49
+
50
+ // Find all knowledge graph node IDs that have a `governs` edge to the target
51
+ const governingKnowledgeNodeIds = new Set<string>()
52
+ for (const edge of graph.edges) {
53
+ if (edge.kind === 'governs' && edge.targetId === targetGraphNodeId && edge.sourceId.startsWith('knowledge:')) {
54
+ governingKnowledgeNodeIds.add(edge.sourceId)
55
+ }
56
+ }
57
+
58
+ // Build a lookup from graph-node-id -> OM sourceId
59
+ const sourceIdMap = buildKnowledgeSourceIdMap(graph)
60
+
61
+ // Collect the OM node ids that correspond to those graph nodes
62
+ const matchingOmIds = new Set<string>()
63
+ for (const graphNodeId of governingKnowledgeNodeIds) {
64
+ const omId = sourceIdMap.get(graphNodeId)
65
+ if (omId) matchingOmIds.add(omId)
66
+ }
67
+
68
+ return knowledgeNodes.filter((n) => matchingOmIds.has(n.id))
69
+ }
70
+
71
+ /**
72
+ * Returns all knowledge nodes whose `kind` matches the given kind.
73
+ *
74
+ * @param graph - The built OrganizationGraph (unused structurally; kept for API symmetry).
75
+ * @param kind - The knowledge kind (`'playbook' | 'strategy' | 'reference'`).
76
+ * @param knowledgeNodes - Flat array of OrgKnowledgeNode from the OrganizationModel.
77
+ */
78
+ export function byKind(
79
+ _graph: OrganizationGraph,
80
+ kind: OrgKnowledgeKind,
81
+ knowledgeNodes: OrgKnowledgeNode[]
82
+ ): OrgKnowledgeNode[] {
83
+ return knowledgeNodes.filter((n) => n.kind === kind)
84
+ }
85
+
86
+ /**
87
+ * Returns all knowledge nodes where `ownerIds` includes the given `ownerId`.
88
+ *
89
+ * @param _graph - The built OrganizationGraph (unused; kept for API symmetry).
90
+ * @param ownerId - The owner id string (matches values in `node.ownerIds`).
91
+ * @param knowledgeNodes - Flat array of OrgKnowledgeNode from the OrganizationModel.
92
+ */
93
+ export function byOwner(
94
+ _graph: OrganizationGraph,
95
+ ownerId: string,
96
+ knowledgeNodes: OrgKnowledgeNode[]
97
+ ): OrgKnowledgeNode[] {
98
+ return knowledgeNodes.filter((n) => n.ownerIds.includes(ownerId))
99
+ }
100
+
101
+ /**
102
+ * Returns the IDs of the graph nodes that the given knowledge node governs
103
+ * (outgoing `governs` edges from the knowledge graph node).
104
+ *
105
+ * The graph node ID for a knowledge OM node with id `X` is `knowledge:X`.
106
+ *
107
+ * @param graph - The built OrganizationGraph.
108
+ * @param nodeId - The OM knowledge node id (e.g. `knowledge.outreach-playbook`).
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', ...]`).
111
+ */
112
+ export function governs(graph: OrganizationGraph, nodeId: string): string[] {
113
+ const graphNodeId = nodeId.startsWith('knowledge:') ? nodeId : toGraphNodeId(nodeId)
114
+
115
+ const results: string[] = []
116
+ for (const edge of graph.edges) {
117
+ if (edge.kind === 'governs' && edge.sourceId === graphNodeId) {
118
+ results.push(edge.targetId)
119
+ }
120
+ }
121
+ return results
122
+ }
123
+
124
+ /**
125
+ * Returns the IDs of the knowledge graph nodes that govern the given node
126
+ * (incoming `governs` edges pointing to `nodeId`).
127
+ *
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:`).
131
+ * @returns Array of source graph node IDs (e.g. `['knowledge:knowledge.outreach-playbook', ...]`).
132
+ */
133
+ export function governedBy(graph: OrganizationGraph, nodeId: string): string[] {
134
+ // Accept both `feature:sales.crm` and bare `sales.crm`
135
+ const targetId =
136
+ nodeId.startsWith('feature:') || nodeId.startsWith('knowledge:') || nodeId.startsWith('resource:')
137
+ ? nodeId
138
+ : `feature:${nodeId}`
139
+
140
+ const results: string[] = []
141
+ for (const edge of graph.edges) {
142
+ if (edge.kind === 'governs' && edge.targetId === targetId) {
143
+ results.push(edge.sourceId)
144
+ }
145
+ }
146
+ return results
147
+ }
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // Path parser
151
+ // ---------------------------------------------------------------------------
152
+
153
+ /** The recognized mount axes for Knowledge Map paths. */
154
+ export type KnowledgeMount = 'by-feature' | 'by-kind' | 'by-owner' | 'graph' | 'node'
155
+
156
+ /**
157
+ * The result of parsing a Knowledge Map path string.
158
+ *
159
+ * Shape: `{ mount: KnowledgeMount, args: string[] }`
160
+ *
161
+ * Per-mount arg arrays:
162
+ * - `by-feature`: `[featureId]` (e.g. `['sales.crm']`)
163
+ * - `by-kind`: `[kind]` (e.g. `['playbook']`)
164
+ * - `by-owner`: `[ownerId]` (e.g. `['role.ops-lead']`)
165
+ * - `graph`: `[nodeId, verb]` where verb is `'governs'` or `'governed-by'`
166
+ * - `node`: `[nodeId]` (single node lookup, no sub-path)
167
+ */
168
+ export interface ParsedKnowledgePath {
169
+ mount: KnowledgeMount
170
+ args: string[]
171
+ }
172
+
173
+ /**
174
+ * Parses a Knowledge Map path string into a `{ mount, args }` descriptor.
175
+ *
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>'] }`
183
+ *
184
+ * The path MUST start with `/`. Trailing slashes are stripped before parsing.
185
+ *
186
+ * @throws {Error} If the path is empty, missing a leading slash, or does not
187
+ * match any supported mount pattern.
188
+ */
189
+ export function parsePath(pathString: string): ParsedKnowledgePath {
190
+ if (!pathString || typeof pathString !== 'string') {
191
+ throw new Error('parsePath: path must be a non-empty string')
192
+ }
193
+
194
+ if (!pathString.startsWith('/')) {
195
+ throw new Error(`parsePath: path must start with "/", got: "${pathString}"`)
196
+ }
197
+
198
+ // Strip trailing slashes then split
199
+ const normalized = pathString.replace(/\/+$/, '')
200
+ const segments = normalized.split('/').filter((s) => s.length > 0)
201
+
202
+ if (segments.length === 0) {
203
+ throw new Error(`parsePath: path resolves to root with no mount: "${pathString}"`)
204
+ }
205
+
206
+ const [first, ...rest] = segments
207
+
208
+ // /by-feature/<featureId>
209
+ if (first === 'by-feature') {
210
+ if (rest.length === 0) {
211
+ throw new Error(`parsePath: /by-feature requires a featureId argument, got: "${pathString}"`)
212
+ }
213
+ return { mount: 'by-feature', args: [rest.join('/')] }
214
+ }
215
+
216
+ // /by-kind/<kind>
217
+ if (first === 'by-kind') {
218
+ if (rest.length === 0) {
219
+ throw new Error(`parsePath: /by-kind requires a kind argument, got: "${pathString}"`)
220
+ }
221
+ return { mount: 'by-kind', args: [rest[0]] }
222
+ }
223
+
224
+ // /by-owner/<ownerId>
225
+ if (first === 'by-owner') {
226
+ if (rest.length === 0) {
227
+ throw new Error(`parsePath: /by-owner requires an ownerId argument, got: "${pathString}"`)
228
+ }
229
+ return { mount: 'by-owner', args: [rest.join('/')] }
230
+ }
231
+
232
+ // /graph/<nodeId>/governs or /graph/<nodeId>/governed-by
233
+ if (first === 'graph') {
234
+ if (rest.length < 2) {
235
+ throw new Error(`parsePath: /graph requires <nodeId>/<verb> (governs|governed-by), got: "${pathString}"`)
236
+ }
237
+ const graphNodeId = rest.slice(0, -1).join('/')
238
+ const verb = rest[rest.length - 1]
239
+ if (verb !== 'governs' && verb !== 'governed-by') {
240
+ throw new Error(
241
+ `parsePath: /graph/<nodeId> verb must be "governs" or "governed-by", got: "${verb}" in "${pathString}"`
242
+ )
243
+ }
244
+ return { mount: 'graph', args: [graphNodeId, verb] }
245
+ }
246
+
247
+ // /<nodeId> (single node)
248
+ // first must not be a recognized mount prefix
249
+ if (segments.length === 1) {
250
+ return { mount: 'node', args: [first] }
251
+ }
252
+
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>`
255
+ )
256
+ }
@@ -1,85 +1,85 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { DEFAULT_ORGANIZATION_MODEL } from '../defaults'
3
- import { DEFAULT_ORGANIZATION_MODEL_BRANDING } from '../domains/branding'
4
- import { OrganizationModelBrandingSchema } from '../domains/branding'
5
- import { DEFAULT_ORGANIZATION_MODEL_SALES } from '../domains/sales'
6
- import { OrganizationModelSalesSchema } from '../domains/sales'
7
- import { DEFAULT_ORGANIZATION_MODEL_PROJECTS } from '../domains/projects'
8
- import { OrganizationModelProjectsSchema } from '../domains/projects'
9
- import { DEFAULT_ORGANIZATION_MODEL_PROSPECTING } from '../domains/prospecting'
10
- import { OrganizationModelProspectingSchema } from '../domains/prospecting'
11
- import { DEFAULT_ORGANIZATION_MODEL_NAVIGATION } from '../domains/navigation'
12
- import { OrganizationModelNavigationSchema } from '../domains/navigation'
13
- import { DEFAULT_ORGANIZATION_MODEL_OPERATIONS } from '../domains/operations'
14
- import { OperationsDomainSchema } from '../domains/operations'
15
- import { DEFAULT_ORGANIZATION_MODEL_STATUSES } from '../domains/statuses'
16
- import { StatusesDomainSchema } from '../domains/statuses'
17
- import { resolveOrganizationModel } from '../resolve'
18
- import { OrganizationModelSchema } from '../schema'
19
-
20
- // All DEFAULT_ORGANIZATION_MODEL* constants and the schemas used to validate them.
21
- // The composite constant lives in defaults.ts; domain sub-constants live in their
22
- // respective domain files. All are covered here for roundtrip integrity.
23
- const domainCases = [
24
- {
25
- name: 'DEFAULT_ORGANIZATION_MODEL_BRANDING',
26
- constant: DEFAULT_ORGANIZATION_MODEL_BRANDING,
27
- schema: OrganizationModelBrandingSchema
28
- },
29
- {
30
- name: 'DEFAULT_ORGANIZATION_MODEL_SALES',
31
- constant: DEFAULT_ORGANIZATION_MODEL_SALES,
32
- schema: OrganizationModelSalesSchema
33
- },
34
- {
35
- name: 'DEFAULT_ORGANIZATION_MODEL_PROJECTS',
36
- constant: DEFAULT_ORGANIZATION_MODEL_PROJECTS,
37
- schema: OrganizationModelProjectsSchema
38
- },
39
- {
40
- name: 'DEFAULT_ORGANIZATION_MODEL_PROSPECTING',
41
- constant: DEFAULT_ORGANIZATION_MODEL_PROSPECTING,
42
- schema: OrganizationModelProspectingSchema
43
- },
44
- {
45
- name: 'DEFAULT_ORGANIZATION_MODEL_NAVIGATION',
46
- constant: DEFAULT_ORGANIZATION_MODEL_NAVIGATION,
47
- schema: OrganizationModelNavigationSchema
48
- },
49
- {
50
- name: 'DEFAULT_ORGANIZATION_MODEL_STATUSES',
51
- constant: DEFAULT_ORGANIZATION_MODEL_STATUSES,
52
- schema: StatusesDomainSchema
53
- },
54
- {
55
- name: 'DEFAULT_ORGANIZATION_MODEL_OPERATIONS',
56
- constant: DEFAULT_ORGANIZATION_MODEL_OPERATIONS,
57
- schema: OperationsDomainSchema
58
- }
59
- ] as const
60
-
61
- describe('organization-model defaults', () => {
62
- describe('DEFAULT_ORGANIZATION_MODEL (composite)', () => {
63
- it('passes OrganizationModelSchema.parse without throwing', () => {
64
- expect(() => OrganizationModelSchema.parse(DEFAULT_ORGANIZATION_MODEL)).not.toThrow()
65
- })
66
-
67
- it('resolveOrganizationModel(DEFAULT_ORGANIZATION_MODEL) returns a schema-valid model', () => {
68
- const result = resolveOrganizationModel(DEFAULT_ORGANIZATION_MODEL)
69
- expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
70
- })
71
-
1
+ import { describe, expect, it } from 'vitest'
2
+ import { DEFAULT_ORGANIZATION_MODEL } from '../defaults'
3
+ import { DEFAULT_ORGANIZATION_MODEL_BRANDING } from '../domains/branding'
4
+ import { OrganizationModelBrandingSchema } from '../domains/branding'
5
+ import { DEFAULT_ORGANIZATION_MODEL_SALES } from '../domains/sales'
6
+ import { OrganizationModelSalesSchema } from '../domains/sales'
7
+ import { DEFAULT_ORGANIZATION_MODEL_PROJECTS } from '../domains/projects'
8
+ import { OrganizationModelProjectsSchema } from '../domains/projects'
9
+ import { DEFAULT_ORGANIZATION_MODEL_PROSPECTING } from '../domains/prospecting'
10
+ import { OrganizationModelProspectingSchema } from '../domains/prospecting'
11
+ import { DEFAULT_ORGANIZATION_MODEL_NAVIGATION } from '../domains/navigation'
12
+ import { OrganizationModelNavigationSchema } from '../domains/navigation'
13
+ import { DEFAULT_ORGANIZATION_MODEL_OPERATIONS } from '../domains/operations'
14
+ import { OperationsDomainSchema } from '../domains/operations'
15
+ import { DEFAULT_ORGANIZATION_MODEL_STATUSES } from '../domains/statuses'
16
+ import { StatusesDomainSchema } from '../domains/statuses'
17
+ import { resolveOrganizationModel } from '../resolve'
18
+ import { OrganizationModelSchema } from '../schema'
19
+
20
+ // All DEFAULT_ORGANIZATION_MODEL* constants and the schemas used to validate them.
21
+ // The composite constant lives in defaults.ts; domain sub-constants live in their
22
+ // respective domain files. All are covered here for roundtrip integrity.
23
+ const domainCases = [
24
+ {
25
+ name: 'DEFAULT_ORGANIZATION_MODEL_BRANDING',
26
+ constant: DEFAULT_ORGANIZATION_MODEL_BRANDING,
27
+ schema: OrganizationModelBrandingSchema
28
+ },
29
+ {
30
+ name: 'DEFAULT_ORGANIZATION_MODEL_SALES',
31
+ constant: DEFAULT_ORGANIZATION_MODEL_SALES,
32
+ schema: OrganizationModelSalesSchema
33
+ },
34
+ {
35
+ name: 'DEFAULT_ORGANIZATION_MODEL_PROJECTS',
36
+ constant: DEFAULT_ORGANIZATION_MODEL_PROJECTS,
37
+ schema: OrganizationModelProjectsSchema
38
+ },
39
+ {
40
+ name: 'DEFAULT_ORGANIZATION_MODEL_PROSPECTING',
41
+ constant: DEFAULT_ORGANIZATION_MODEL_PROSPECTING,
42
+ schema: OrganizationModelProspectingSchema
43
+ },
44
+ {
45
+ name: 'DEFAULT_ORGANIZATION_MODEL_NAVIGATION',
46
+ constant: DEFAULT_ORGANIZATION_MODEL_NAVIGATION,
47
+ schema: OrganizationModelNavigationSchema
48
+ },
49
+ {
50
+ name: 'DEFAULT_ORGANIZATION_MODEL_STATUSES',
51
+ constant: DEFAULT_ORGANIZATION_MODEL_STATUSES,
52
+ schema: StatusesDomainSchema
53
+ },
54
+ {
55
+ name: 'DEFAULT_ORGANIZATION_MODEL_OPERATIONS',
56
+ constant: DEFAULT_ORGANIZATION_MODEL_OPERATIONS,
57
+ schema: OperationsDomainSchema
58
+ }
59
+ ] as const
60
+
61
+ describe('organization-model defaults', () => {
62
+ describe('DEFAULT_ORGANIZATION_MODEL (composite)', () => {
63
+ it('passes OrganizationModelSchema.parse without throwing', () => {
64
+ expect(() => OrganizationModelSchema.parse(DEFAULT_ORGANIZATION_MODEL)).not.toThrow()
65
+ })
66
+
67
+ it('resolveOrganizationModel(DEFAULT_ORGANIZATION_MODEL) returns a schema-valid model', () => {
68
+ const result = resolveOrganizationModel(DEFAULT_ORGANIZATION_MODEL)
69
+ expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
70
+ })
71
+
72
72
  it('resolveOrganizationModel with no override equals resolveOrganizationModel with DEFAULT_ORGANIZATION_MODEL', () => {
73
73
  const fromUndefined = resolveOrganizationModel(undefined)
74
74
  const fromDefault = resolveOrganizationModel(DEFAULT_ORGANIZATION_MODEL)
75
75
  expect(fromDefault).toEqual(fromUndefined)
76
76
  })
77
77
 
78
- it('keeps Command View enabled but development-only in the default Operations navigation', () => {
78
+ it('keeps Command View enabled but development-only in the default Knowledge navigation', () => {
79
79
  const result = resolveOrganizationModel(undefined)
80
- const commandViewFeature = result.features.find((feature) => feature.id === 'operations.command-view')
80
+ const commandViewFeature = result.features.find((feature) => feature.id === 'knowledge.command-view')
81
81
  const commandViewSurface = DEFAULT_ORGANIZATION_MODEL_NAVIGATION.surfaces.find(
82
- (surface) => surface.id === 'operations.command-view'
82
+ (surface) => surface.id === 'knowledge.command-view'
83
83
  )
84
84
 
85
85
  expect(commandViewFeature?.enabled).toBe(true)
@@ -88,101 +88,101 @@ describe('organization-model defaults', () => {
88
88
  expect(commandViewSurface?.devOnly).toBe(true)
89
89
  })
90
90
  })
91
-
92
- describe.each(domainCases)('$name (domain sub-constant)', ({ name: _name, constant, schema }) => {
93
- it('passes its domain schema parse without throwing', () => {
94
- expect(() => schema.parse(constant)).not.toThrow()
95
- })
96
- })
97
-
98
- describe('DEFAULT_ORGANIZATION_MODEL_BRANDING via resolveOrganizationModel', () => {
99
- it('resolving with branding override produces a schema-valid model', () => {
100
- const result = resolveOrganizationModel({ branding: DEFAULT_ORGANIZATION_MODEL_BRANDING })
101
- expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
102
- })
103
- })
104
-
105
- describe('DEFAULT_ORGANIZATION_MODEL_SALES via resolveOrganizationModel', () => {
106
- it('resolving with sales override produces a schema-valid model', () => {
107
- const result = resolveOrganizationModel({ sales: DEFAULT_ORGANIZATION_MODEL_SALES })
108
- expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
109
- })
110
- })
111
-
112
- describe('DEFAULT_ORGANIZATION_MODEL_PROJECTS via resolveOrganizationModel', () => {
113
- it('resolving with projects override produces a schema-valid model', () => {
114
- const result = resolveOrganizationModel({ projects: DEFAULT_ORGANIZATION_MODEL_PROJECTS })
115
- expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
116
- })
117
- })
118
-
119
- describe('DEFAULT_ORGANIZATION_MODEL_PROSPECTING via resolveOrganizationModel', () => {
120
- it('resolving with prospecting override produces a schema-valid model', () => {
121
- const result = resolveOrganizationModel({ prospecting: DEFAULT_ORGANIZATION_MODEL_PROSPECTING })
122
- expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
123
- })
124
- })
125
-
126
- describe('DEFAULT_ORGANIZATION_MODEL_NAVIGATION via resolveOrganizationModel', () => {
127
- it('resolving with navigation override produces a schema-valid model', () => {
128
- const result = resolveOrganizationModel({ navigation: DEFAULT_ORGANIZATION_MODEL_NAVIGATION })
129
- expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
130
- })
131
- })
132
-
133
- describe('DEFAULT_ORGANIZATION_MODEL_OPERATIONS via resolveOrganizationModel', () => {
134
- it('resolving with operations override produces a schema-valid model', () => {
135
- const result = resolveOrganizationModel({ operations: DEFAULT_ORGANIZATION_MODEL_OPERATIONS })
136
- expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
137
- })
138
-
139
- it('default operations seed covers all 5 entity categories', () => {
140
- expect(DEFAULT_ORGANIZATION_MODEL_OPERATIONS.entries).toHaveLength(5)
141
- })
142
-
143
- it('every entry has a non-empty id, label, and semanticClass', () => {
144
- for (const entry of DEFAULT_ORGANIZATION_MODEL_OPERATIONS.entries) {
145
- expect(entry.id.length).toBeGreaterThan(0)
146
- expect(entry.label.length).toBeGreaterThan(0)
147
- expect(entry.semanticClass.length).toBeGreaterThan(0)
148
- }
149
- })
150
-
151
- it('all entry ids are unique', () => {
152
- const ids = DEFAULT_ORGANIZATION_MODEL_OPERATIONS.entries.map((e) => e.id)
153
- const uniqueIds = new Set(ids)
154
- expect(uniqueIds.size).toBe(ids.length)
155
- })
156
- })
157
-
158
- describe('DEFAULT_ORGANIZATION_MODEL_STATUSES via resolveOrganizationModel', () => {
159
- it('resolving with statuses override produces a schema-valid model', () => {
160
- const result = resolveOrganizationModel({ statuses: DEFAULT_ORGANIZATION_MODEL_STATUSES })
161
- expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
162
- })
163
-
164
- it('default statuses seed covers all 9 delivery.task values', () => {
165
- const taskEntries = DEFAULT_ORGANIZATION_MODEL_STATUSES.entries.filter((e) => e.semanticClass === 'delivery.task')
166
- expect(taskEntries).toHaveLength(9)
167
- })
168
-
169
- it('default statuses seed covers all 5 queue values', () => {
170
- const queueEntries = DEFAULT_ORGANIZATION_MODEL_STATUSES.entries.filter((e) => e.semanticClass === 'queue')
171
- expect(queueEntries).toHaveLength(5)
172
- })
173
-
174
- it('every entry has a non-empty id, label, and semanticClass', () => {
175
- for (const entry of DEFAULT_ORGANIZATION_MODEL_STATUSES.entries) {
176
- expect(entry.id.length).toBeGreaterThan(0)
177
- expect(entry.label.length).toBeGreaterThan(0)
178
- expect(entry.semanticClass.length).toBeGreaterThan(0)
179
- }
180
- })
181
-
182
- it('all entry ids are unique', () => {
183
- const ids = DEFAULT_ORGANIZATION_MODEL_STATUSES.entries.map((e) => e.id)
184
- const uniqueIds = new Set(ids)
185
- expect(uniqueIds.size).toBe(ids.length)
186
- })
187
- })
188
- })
91
+
92
+ describe.each(domainCases)('$name (domain sub-constant)', ({ name: _name, constant, schema }) => {
93
+ it('passes its domain schema parse without throwing', () => {
94
+ expect(() => schema.parse(constant)).not.toThrow()
95
+ })
96
+ })
97
+
98
+ describe('DEFAULT_ORGANIZATION_MODEL_BRANDING via resolveOrganizationModel', () => {
99
+ it('resolving with branding override produces a schema-valid model', () => {
100
+ const result = resolveOrganizationModel({ branding: DEFAULT_ORGANIZATION_MODEL_BRANDING })
101
+ expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
102
+ })
103
+ })
104
+
105
+ describe('DEFAULT_ORGANIZATION_MODEL_SALES via resolveOrganizationModel', () => {
106
+ it('resolving with sales override produces a schema-valid model', () => {
107
+ const result = resolveOrganizationModel({ sales: DEFAULT_ORGANIZATION_MODEL_SALES })
108
+ expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
109
+ })
110
+ })
111
+
112
+ describe('DEFAULT_ORGANIZATION_MODEL_PROJECTS via resolveOrganizationModel', () => {
113
+ it('resolving with projects override produces a schema-valid model', () => {
114
+ const result = resolveOrganizationModel({ projects: DEFAULT_ORGANIZATION_MODEL_PROJECTS })
115
+ expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
116
+ })
117
+ })
118
+
119
+ describe('DEFAULT_ORGANIZATION_MODEL_PROSPECTING via resolveOrganizationModel', () => {
120
+ it('resolving with prospecting override produces a schema-valid model', () => {
121
+ const result = resolveOrganizationModel({ prospecting: DEFAULT_ORGANIZATION_MODEL_PROSPECTING })
122
+ expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
123
+ })
124
+ })
125
+
126
+ describe('DEFAULT_ORGANIZATION_MODEL_NAVIGATION via resolveOrganizationModel', () => {
127
+ it('resolving with navigation override produces a schema-valid model', () => {
128
+ const result = resolveOrganizationModel({ navigation: DEFAULT_ORGANIZATION_MODEL_NAVIGATION })
129
+ expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
130
+ })
131
+ })
132
+
133
+ describe('DEFAULT_ORGANIZATION_MODEL_OPERATIONS via resolveOrganizationModel', () => {
134
+ it('resolving with operations override produces a schema-valid model', () => {
135
+ const result = resolveOrganizationModel({ operations: DEFAULT_ORGANIZATION_MODEL_OPERATIONS })
136
+ expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
137
+ })
138
+
139
+ it('default operations seed covers all 5 entity categories', () => {
140
+ expect(DEFAULT_ORGANIZATION_MODEL_OPERATIONS.entries).toHaveLength(5)
141
+ })
142
+
143
+ it('every entry has a non-empty id, label, and semanticClass', () => {
144
+ for (const entry of DEFAULT_ORGANIZATION_MODEL_OPERATIONS.entries) {
145
+ expect(entry.id.length).toBeGreaterThan(0)
146
+ expect(entry.label.length).toBeGreaterThan(0)
147
+ expect(entry.semanticClass.length).toBeGreaterThan(0)
148
+ }
149
+ })
150
+
151
+ it('all entry ids are unique', () => {
152
+ const ids = DEFAULT_ORGANIZATION_MODEL_OPERATIONS.entries.map((e) => e.id)
153
+ const uniqueIds = new Set(ids)
154
+ expect(uniqueIds.size).toBe(ids.length)
155
+ })
156
+ })
157
+
158
+ describe('DEFAULT_ORGANIZATION_MODEL_STATUSES via resolveOrganizationModel', () => {
159
+ it('resolving with statuses override produces a schema-valid model', () => {
160
+ const result = resolveOrganizationModel({ statuses: DEFAULT_ORGANIZATION_MODEL_STATUSES })
161
+ expect(() => OrganizationModelSchema.parse(result)).not.toThrow()
162
+ })
163
+
164
+ it('default statuses seed covers all 9 delivery.task values', () => {
165
+ const taskEntries = DEFAULT_ORGANIZATION_MODEL_STATUSES.entries.filter((e) => e.semanticClass === 'delivery.task')
166
+ expect(taskEntries).toHaveLength(9)
167
+ })
168
+
169
+ it('default statuses seed covers all 5 queue values', () => {
170
+ const queueEntries = DEFAULT_ORGANIZATION_MODEL_STATUSES.entries.filter((e) => e.semanticClass === 'queue')
171
+ expect(queueEntries).toHaveLength(5)
172
+ })
173
+
174
+ it('every entry has a non-empty id, label, and semanticClass', () => {
175
+ for (const entry of DEFAULT_ORGANIZATION_MODEL_STATUSES.entries) {
176
+ expect(entry.id.length).toBeGreaterThan(0)
177
+ expect(entry.label.length).toBeGreaterThan(0)
178
+ expect(entry.semanticClass.length).toBeGreaterThan(0)
179
+ }
180
+ })
181
+
182
+ it('all entry ids are unique', () => {
183
+ const ids = DEFAULT_ORGANIZATION_MODEL_STATUSES.entries.map((e) => e.id)
184
+ const uniqueIds = new Set(ids)
185
+ expect(uniqueIds.size).toBe(ids.length)
186
+ })
187
+ })
188
+ })
@@ -59,13 +59,13 @@ describe('createFoundationOrganizationModel', () => {
59
59
  branding: { organizationName: 'Acme', productName: 'Acme OS', shortName: 'Acme' }
60
60
  })
61
61
 
62
- const crmSurface = result.getOrganizationSurface('crm')
63
- expect(crmSurface).toBeDefined()
64
- expect(crmSurface?.id).toBe('crm')
65
- expect(crmSurface?.icon).toBe('crm')
66
-
67
- expect(result.getOrganizationSurface('nonexistent')).toBeUndefined()
68
- })
62
+ const crmSurface = result.getOrganizationSurface('crm')
63
+ expect(crmSurface).toBeDefined()
64
+ expect(crmSurface?.id).toBe('crm')
65
+ expect(crmSurface?.icon).toBe('feature.crm')
66
+
67
+ expect(result.getOrganizationSurface('nonexistent')).toBeUndefined()
68
+ })
69
69
 
70
70
  it('throws when a required core surface is missing', () => {
71
71
  const override = {
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ ORGANIZATION_MODEL_ICON_TOKENS,
4
+ OrganizationModelBuiltinIconTokenSchema,
5
+ OrganizationModelIconTokenSchema
6
+ } from '../icons'
7
+ import { IconNameSchema } from '../domains/shared'
8
+
9
+ describe('OrganizationModelIconTokenSchema', () => {
10
+ it('accepts built-in semantic icon tokens', () => {
11
+ for (const token of ORGANIZATION_MODEL_ICON_TOKENS) {
12
+ expect(OrganizationModelBuiltinIconTokenSchema.parse(token)).toBe(token)
13
+ expect(OrganizationModelIconTokenSchema.parse(token)).toBe(token)
14
+ }
15
+ })
16
+
17
+ it('accepts custom extension icon tokens', () => {
18
+ expect(OrganizationModelIconTokenSchema.parse('custom.partner-portal')).toBe('custom.partner-portal')
19
+ expect(IconNameSchema.parse('custom.acme.ops_queue')).toBe('custom.acme.ops_queue')
20
+ })
21
+
22
+ it('rejects random strings outside the built-in and custom namespaces', () => {
23
+ for (const token of ['crm', 'lead-gen', 'settings', 'random.icon', 'custom.', 'custom.Bad']) {
24
+ expect(OrganizationModelIconTokenSchema.safeParse(token).success).toBe(false)
25
+ }
26
+ })
27
+ })