@elevasis/core 0.19.0 → 0.20.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.
@@ -7,6 +7,7 @@ import type {
7
7
  OrganizationGraphNode,
8
8
  OrganizationGraphNodeKind
9
9
  } from './types'
10
+ import { CAPABILITY_REGISTRY } from '../domains/prospecting'
10
11
 
11
12
  function nodeId(kind: OrganizationGraphNodeKind, sourceId?: string): string {
12
13
  return kind === 'organization' ? 'organization-model' : `${kind}:${sourceId ?? ''}`
@@ -187,6 +188,79 @@ export function buildOrganizationGraph(input: BuildOrganizationGraphInput): Orga
187
188
  }
188
189
  }
189
190
 
191
+ const allStages = [
192
+ ...organizationModel.prospecting.companyStages,
193
+ ...organizationModel.prospecting.contactStages
194
+ ].sort((a, b) => a.order - b.order || a.id.localeCompare(b.id))
195
+
196
+ for (const stage of allStages) {
197
+ const id = nodeId('stage', stage.id)
198
+ pushUniqueNode(nodes, nodeIds, {
199
+ id,
200
+ kind: 'stage',
201
+ label: stage.label,
202
+ sourceId: stage.id,
203
+ ...(stage.description ? { description: stage.description } : {}),
204
+ ...(stage.icon ? { icon: stage.icon } : {})
205
+ })
206
+ pushUniqueEdge(edges, edgeIds, {
207
+ id: edgeId('contains', organizationNode.id, id),
208
+ kind: 'contains',
209
+ sourceId: organizationNode.id,
210
+ targetId: id
211
+ })
212
+ }
213
+
214
+ for (const cap of [...CAPABILITY_REGISTRY].sort((a, b) => a.id.localeCompare(b.id))) {
215
+ const id = nodeId('capability', cap.id)
216
+ pushUniqueNode(nodes, nodeIds, {
217
+ id,
218
+ kind: 'capability',
219
+ label: cap.label,
220
+ sourceId: cap.id,
221
+ description: cap.description
222
+ })
223
+ pushUniqueEdge(edges, edgeIds, {
224
+ id: edgeId('contains', organizationNode.id, id),
225
+ kind: 'contains',
226
+ sourceId: organizationNode.id,
227
+ targetId: id
228
+ })
229
+ const resourceNode = ensureResourceNode(nodes, nodeIds, resourceNodesById, cap.resourceId)
230
+ pushUniqueEdge(edges, edgeIds, {
231
+ id: edgeId('maps_to', id, resourceNode.id),
232
+ kind: 'maps_to',
233
+ sourceId: id,
234
+ targetId: resourceNode.id
235
+ })
236
+ }
237
+
238
+ for (const template of [...organizationModel.prospecting.buildTemplates].sort((a, b) => a.id.localeCompare(b.id))) {
239
+ const stepById = new Map(template.steps.map((s) => [s.id, s]))
240
+ for (const step of [...template.steps].sort((a, b) => a.id.localeCompare(b.id))) {
241
+ const stageNodeId = nodeId('stage', step.stageKey)
242
+ const capNodeId = nodeId('capability', step.capabilityKey)
243
+ pushUniqueEdge(edges, edgeIds, {
244
+ id: edgeId('uses', stageNodeId, capNodeId, step.id),
245
+ kind: 'uses',
246
+ sourceId: stageNodeId,
247
+ targetId: capNodeId
248
+ })
249
+ for (const depId of step.dependsOn ?? []) {
250
+ const depStep = stepById.get(depId)
251
+ if (depStep) {
252
+ const depStageNodeId = nodeId('stage', depStep.stageKey)
253
+ pushUniqueEdge(edges, edgeIds, {
254
+ id: edgeId('references', stageNodeId, depStageNodeId, step.id),
255
+ kind: 'references',
256
+ sourceId: stageNodeId,
257
+ targetId: depStageNodeId
258
+ })
259
+ }
260
+ }
261
+ }
262
+ }
263
+
190
264
  if (commandViewData) {
191
265
  const commandViewResources = collectCommandViewResources(commandViewData).sort((a, b) =>
192
266
  a.resourceId.localeCompare(b.resourceId)
@@ -8,6 +8,7 @@ export const OrganizationGraphNodeKindSchema = z.enum([
8
8
  'surface',
9
9
  'entity',
10
10
  'capability',
11
+ 'stage',
11
12
  'resource',
12
13
  'knowledge'
13
14
  ])
@@ -9,6 +9,7 @@ export type OrganizationGraphNodeKind =
9
9
  | 'surface'
10
10
  | 'entity'
11
11
  | 'capability'
12
+ | 'stage'
12
13
  | 'resource'
13
14
  | 'knowledge'
14
15
 
@@ -70,8 +70,14 @@ function hasFeature(featuresById: Map<string, unknown>, featureId: string): bool
70
70
  return featuresById.has(featureId) || featuresById.has(LEGACY_FEATURE_ALIASES.get(featureId) ?? '')
71
71
  }
72
72
 
73
+ function defaultFeaturePathFor(id: string): string {
74
+ return `/${id.replaceAll('.', '/')}`
75
+ }
76
+
73
77
  export const OrganizationModelSchema = OrganizationModelSchemaBase.superRefine((model, ctx) => {
74
78
  const featuresById = collectIds(model.features, ctx, ['features'], 'Feature')
79
+ const featureIdsByEffectivePath = new Map<string, string>()
80
+
75
81
  model.features.forEach((feature, featureIndex) => {
76
82
  const segments = feature.id.split('.')
77
83
  if (segments.length > 1) {
@@ -88,6 +94,21 @@ export const OrganizationModelSchema = OrganizationModelSchemaBase.superRefine((
88
94
  const hasChildren = model.features.some(
89
95
  (candidate) => candidate.id.startsWith(`${feature.id}.`) && candidate.id !== feature.id
90
96
  )
97
+ const contributesRoutePath = feature.path !== undefined || !hasChildren
98
+ if (contributesRoutePath) {
99
+ const effectivePath = feature.path ?? defaultFeaturePathFor(feature.id)
100
+ const existingFeatureId = featureIdsByEffectivePath.get(effectivePath)
101
+ if (existingFeatureId !== undefined) {
102
+ addIssue(
103
+ ctx,
104
+ ['features', featureIndex, feature.path === undefined ? 'id' : 'path'],
105
+ `Feature "${feature.id}" effective path "${effectivePath}" duplicates feature "${existingFeatureId}"`
106
+ )
107
+ } else {
108
+ featureIdsByEffectivePath.set(effectivePath, feature.id)
109
+ }
110
+ }
111
+
91
112
  if (hasChildren && feature.enabled) {
92
113
  const hasEnabledDescendant = model.features.some(
93
114
  (candidate) => candidate.id.startsWith(`${feature.id}.`) && candidate.enabled
@@ -102,6 +123,48 @@ export const OrganizationModelSchema = OrganizationModelSchemaBase.superRefine((
102
123
  }
103
124
  })
104
125
 
126
+ const surfacesById = collectIds(model.navigation.surfaces, ctx, ['navigation', 'surfaces'], 'Navigation surface')
127
+
128
+ if (model.navigation.defaultSurfaceId !== undefined && !surfacesById.has(model.navigation.defaultSurfaceId)) {
129
+ addIssue(
130
+ ctx,
131
+ ['navigation', 'defaultSurfaceId'],
132
+ `Navigation defaultSurfaceId references unknown surface "${model.navigation.defaultSurfaceId}"`
133
+ )
134
+ }
135
+
136
+ model.navigation.groups.forEach((group, groupIndex) => {
137
+ group.surfaceIds.forEach((surfaceId, surfaceIndex) => {
138
+ if (!surfacesById.has(surfaceId)) {
139
+ addIssue(
140
+ ctx,
141
+ ['navigation', 'groups', groupIndex, 'surfaceIds', surfaceIndex],
142
+ `Navigation group "${group.id}" references unknown surface "${surfaceId}"`
143
+ )
144
+ }
145
+ })
146
+ })
147
+
148
+ model.navigation.surfaces.forEach((surface, surfaceIndex) => {
149
+ if (surface.featureId !== undefined && !hasFeature(featuresById, surface.featureId)) {
150
+ addIssue(
151
+ ctx,
152
+ ['navigation', 'surfaces', surfaceIndex, 'featureId'],
153
+ `Navigation surface "${surface.id}" references unknown feature "${surface.featureId}"`
154
+ )
155
+ }
156
+
157
+ surface.featureIds.forEach((featureId, featureIndex) => {
158
+ if (!hasFeature(featuresById, featureId)) {
159
+ addIssue(
160
+ ctx,
161
+ ['navigation', 'surfaces', surfaceIndex, 'featureIds', featureIndex],
162
+ `Navigation surface "${surface.id}" references unknown feature "${featureId}"`
163
+ )
164
+ }
165
+ })
166
+ })
167
+
105
168
  // Offerings -> CustomerSegment cross-ref: targetSegmentIds must resolve
106
169
  const segmentsById = new Map(model.customers.segments.map((seg) => [seg.id, seg]))
107
170
  model.offerings.products.forEach((product, productIndex) => {
@@ -0,0 +1,218 @@
1
+ import type { OrganizationModel, OrganizationModelFeature, OrganizationModelSurface } from './types'
2
+
3
+ export type OrganizationSurfaceProjectionIssueCode =
4
+ | 'duplicate-surface-id'
5
+ | 'duplicate-surface-path'
6
+ | 'unknown-default-surface'
7
+ | 'unknown-group-surface'
8
+ | 'unknown-surface-feature'
9
+ | 'unknown-surface-feature-reference'
10
+
11
+ export interface OrganizationSurfaceProjection {
12
+ id: string
13
+ label: string
14
+ path: string
15
+ surfaceType: OrganizationModelSurface['surfaceType']
16
+ featureId?: string
17
+ featureIds: string[]
18
+ entityIds: string[]
19
+ resourceIds: string[]
20
+ capabilityIds: string[]
21
+ enabled: boolean
22
+ devOnly?: boolean
23
+ requiresAdmin?: boolean
24
+ }
25
+
26
+ export interface OrganizationSurfaceProjectionIssue {
27
+ code: OrganizationSurfaceProjectionIssueCode
28
+ message: string
29
+ path: Array<string | number>
30
+ surfaceId?: string
31
+ featureId?: string
32
+ groupId?: string
33
+ value?: string
34
+ }
35
+
36
+ const LEGACY_FEATURE_ALIASES = new Map<string, string>([
37
+ ['crm', 'sales.crm'],
38
+ ['lead-gen', 'sales.lead-gen'],
39
+ ['submitted-requests', 'monitoring.submitted-requests']
40
+ ])
41
+
42
+ function normalizePath(path: string): string {
43
+ return path.length > 1 ? path.replace(/\/+$/, '') : path
44
+ }
45
+
46
+ function collectFeaturesById(model: OrganizationModel): Map<string, OrganizationModelFeature> {
47
+ return new Map(model.features.map((feature) => [feature.id, feature]))
48
+ }
49
+
50
+ function resolveFeatureId(featuresById: Map<string, OrganizationModelFeature>, featureId: string): string | undefined {
51
+ if (featuresById.has(featureId)) {
52
+ return featureId
53
+ }
54
+
55
+ const aliasTarget = LEGACY_FEATURE_ALIASES.get(featureId)
56
+ return aliasTarget !== undefined && featuresById.has(aliasTarget) ? aliasTarget : undefined
57
+ }
58
+
59
+ function getFeatureWithAncestors(
60
+ featuresById: Map<string, OrganizationModelFeature>,
61
+ featureId: string
62
+ ): OrganizationModelFeature[] {
63
+ const features: OrganizationModelFeature[] = []
64
+ const segments = featureId.split('.')
65
+
66
+ for (let index = 1; index <= segments.length; index += 1) {
67
+ const candidate = featuresById.get(segments.slice(0, index).join('.'))
68
+ if (candidate) {
69
+ features.push(candidate)
70
+ }
71
+ }
72
+
73
+ return features
74
+ }
75
+
76
+ function hasInheritedFlag(
77
+ featuresById: Map<string, OrganizationModelFeature>,
78
+ featureIds: string[],
79
+ flag: 'devOnly' | 'requiresAdmin'
80
+ ): boolean {
81
+ return featureIds.some((featureId) =>
82
+ getFeatureWithAncestors(featuresById, featureId).some((feature) => feature[flag] === true)
83
+ )
84
+ }
85
+
86
+ function isFeatureEnabled(featuresById: Map<string, OrganizationModelFeature>, featureId: string): boolean {
87
+ const featureLineage = getFeatureWithAncestors(featuresById, featureId)
88
+ return featureLineage.length > 0 && featureLineage.every((feature) => feature.enabled)
89
+ }
90
+
91
+ function unique(values: string[]): string[] {
92
+ return [...new Set(values)]
93
+ }
94
+
95
+ export function projectOrganizationSurfaces(model: OrganizationModel): OrganizationSurfaceProjection[] {
96
+ const featuresById = collectFeaturesById(model)
97
+
98
+ return model.navigation.surfaces.map((surface) => {
99
+ const featureId = surface.featureId ? resolveFeatureId(featuresById, surface.featureId) : undefined
100
+ const featureIds = unique(
101
+ [featureId, ...surface.featureIds.map((candidate) => resolveFeatureId(featuresById, candidate))].filter(
102
+ (candidate): candidate is string => candidate !== undefined
103
+ )
104
+ )
105
+ const enabled = surface.enabled && featureIds.every((candidate) => isFeatureEnabled(featuresById, candidate))
106
+ const devOnly = surface.devOnly === true || hasInheritedFlag(featuresById, featureIds, 'devOnly')
107
+ const requiresAdmin = hasInheritedFlag(featuresById, featureId ? [featureId] : featureIds, 'requiresAdmin')
108
+
109
+ return {
110
+ id: surface.id,
111
+ label: surface.label,
112
+ path: surface.path,
113
+ surfaceType: surface.surfaceType,
114
+ featureId,
115
+ featureIds,
116
+ entityIds: [...surface.entityIds],
117
+ resourceIds: [...surface.resourceIds],
118
+ capabilityIds: [...surface.capabilityIds],
119
+ enabled,
120
+ ...(devOnly ? { devOnly } : {}),
121
+ ...(requiresAdmin ? { requiresAdmin } : {})
122
+ }
123
+ })
124
+ }
125
+
126
+ export function validateOrganizationSurfaceProjection(model: OrganizationModel): OrganizationSurfaceProjectionIssue[] {
127
+ const issues: OrganizationSurfaceProjectionIssue[] = []
128
+ const featuresById = collectFeaturesById(model)
129
+ const surfaceIds = new Set<string>()
130
+ const surfacePaths = new Map<string, string>()
131
+
132
+ function addIssue(issue: OrganizationSurfaceProjectionIssue): void {
133
+ issues.push(issue)
134
+ }
135
+
136
+ model.navigation.surfaces.forEach((surface, surfaceIndex) => {
137
+ if (surfaceIds.has(surface.id)) {
138
+ addIssue({
139
+ code: 'duplicate-surface-id',
140
+ message: `Surface id "${surface.id}" must be unique`,
141
+ path: ['navigation', 'surfaces', surfaceIndex, 'id'],
142
+ surfaceId: surface.id,
143
+ value: surface.id
144
+ })
145
+ } else {
146
+ surfaceIds.add(surface.id)
147
+ }
148
+
149
+ const normalizedPath = normalizePath(surface.path)
150
+ const existingSurfaceId = surfacePaths.get(normalizedPath)
151
+ if (existingSurfaceId !== undefined) {
152
+ addIssue({
153
+ code: 'duplicate-surface-path',
154
+ message: `Surface path "${surface.path}" is already used by surface "${existingSurfaceId}"`,
155
+ path: ['navigation', 'surfaces', surfaceIndex, 'path'],
156
+ surfaceId: surface.id,
157
+ value: surface.path
158
+ })
159
+ } else {
160
+ surfacePaths.set(normalizedPath, surface.id)
161
+ }
162
+
163
+ if (surface.featureId !== undefined && resolveFeatureId(featuresById, surface.featureId) === undefined) {
164
+ addIssue({
165
+ code: 'unknown-surface-feature',
166
+ message: `Surface "${surface.id}" references unknown feature "${surface.featureId}"`,
167
+ path: ['navigation', 'surfaces', surfaceIndex, 'featureId'],
168
+ surfaceId: surface.id,
169
+ featureId: surface.featureId,
170
+ value: surface.featureId
171
+ })
172
+ }
173
+
174
+ surface.featureIds.forEach((featureId, featureIndex) => {
175
+ if (resolveFeatureId(featuresById, featureId) !== undefined) {
176
+ return
177
+ }
178
+
179
+ addIssue({
180
+ code: 'unknown-surface-feature-reference',
181
+ message: `Surface "${surface.id}" references unknown feature "${featureId}"`,
182
+ path: ['navigation', 'surfaces', surfaceIndex, 'featureIds', featureIndex],
183
+ surfaceId: surface.id,
184
+ featureId,
185
+ value: featureId
186
+ })
187
+ })
188
+ })
189
+
190
+ if (model.navigation.defaultSurfaceId !== undefined && !surfaceIds.has(model.navigation.defaultSurfaceId)) {
191
+ addIssue({
192
+ code: 'unknown-default-surface',
193
+ message: `Default surface "${model.navigation.defaultSurfaceId}" is not declared in navigation.surfaces`,
194
+ path: ['navigation', 'defaultSurfaceId'],
195
+ surfaceId: model.navigation.defaultSurfaceId,
196
+ value: model.navigation.defaultSurfaceId
197
+ })
198
+ }
199
+
200
+ model.navigation.groups.forEach((group, groupIndex) => {
201
+ group.surfaceIds.forEach((surfaceId, surfaceIndex) => {
202
+ if (surfaceIds.has(surfaceId)) {
203
+ return
204
+ }
205
+
206
+ addIssue({
207
+ code: 'unknown-group-surface',
208
+ message: `Navigation group "${group.id}" references unknown surface "${surfaceId}"`,
209
+ path: ['navigation', 'groups', groupIndex, 'surfaceIds', surfaceIndex],
210
+ surfaceId,
211
+ groupId: group.id,
212
+ value: surfaceId
213
+ })
214
+ })
215
+ })
216
+
217
+ return issues
218
+ }
@@ -1,3 +1,3 @@
1
1
  export const VERSION = {
2
- CURRENT: '1.8.1'
2
+ CURRENT: '1.8.6'
3
3
  }
@@ -1123,7 +1123,7 @@ export const DEFAULT_CRM_PRIORITY_RULE_CONFIG: CrmPriorityRuleConfig = {
1123
1123
  ### `DealStageSchema`
1124
1124
 
1125
1125
  ```typescript
1126
- export const DealStageSchema = z.enum(['interested', 'proposal', 'closing', 'closed_won', 'closed_lost', 'nurturing'])
1126
+ export const DealStageSchema = CrmStageKeySchema
1127
1127
  ```
1128
1128
 
1129
1129
  ### `AcqDealTaskKindSchema`
@@ -1215,7 +1215,7 @@ export const TransitionItemRequestSchema = z
1215
1215
  .object({
1216
1216
  pipelineKey: z.string().min(1),
1217
1217
  stageKey: z.string().min(1),
1218
- stateKey: z.string().nullable().optional(),
1218
+ stateKey: z.string().min(1).nullable().optional(),
1219
1219
  reason: z.string().optional(),
1220
1220
  expectedUpdatedAt: z.string().datetime().optional()
1221
1221
  })
@@ -1409,6 +1409,11 @@ export const DealTaskListResponseSchema = z.array(DealTaskResponseSchema)
1409
1409
 
1410
1410
  ```typescript
1411
1411
  export const DealSchemas = {
1412
+ // Primitives
1413
+ CrmStageKey: CrmStageKeySchema,
1414
+ CrmStateKey: CrmStateKeySchema,
1415
+ DealStage: DealStageSchema,
1416
+
1412
1417
  // Params
1413
1418
  DealIdParams: DealIdParamsSchema,
1414
1419
  DealTaskIdParams: DealTaskIdParamsSchema,
@@ -1421,7 +1426,7 @@ export const DealSchemas = {
1421
1426
  // Request bodies
1422
1427
  CreateDealNoteRequest: CreateDealNoteRequestSchema,
1423
1428
  CreateDealTaskRequest: CreateDealTaskRequestSchema,
1424
- TransitionItemRequest: TransitionItemRequestSchema,
1429
+ TransitionItemRequest: CrmTransitionItemRequestSchema,
1425
1430
  TransitionDealStateRequest: TransitionDealStateRequestSchema,
1426
1431
  ExecuteActionParams: ExecuteActionParamsSchema,
1427
1432
  ExecuteActionRequest: ExecuteActionRequestSchema,
@@ -2552,7 +2557,7 @@ export const AcqSubstrateSchemas = {
2552
2557
  ListCompanyIdParams: ListCompanyIdParamsSchema,
2553
2558
  AcqListCompanyResponse: AcqListCompanyResponseSchema,
2554
2559
 
2555
- // Transition (shared with deals — TransitionItemRequestSchema)
2560
+ // Transition (generic stateful substrate)
2556
2561
  TransitionItemRequest: TransitionItemRequestSchema
2557
2562
  }
2558
2563
  ```
@@ -2620,6 +2625,8 @@ export interface StatefulStageDefinition {
2620
2625
  /** Matches stage_key values written by workflow steps. */
2621
2626
  stageKey: string
2622
2627
  label: string
2628
+ /** UI color token. Consumers may map this to their design system. */
2629
+ color?: string
2623
2630
  states: StatefulStateDefinition[]
2624
2631
  }
2625
2632
  ```