@elevasis/core 0.18.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.
Files changed (54) hide show
  1. package/dist/index.d.ts +82 -1
  2. package/dist/index.js +353 -171
  3. package/dist/knowledge/index.d.ts +44 -1
  4. package/dist/organization-model/index.d.ts +82 -1
  5. package/dist/organization-model/index.js +353 -171
  6. package/dist/test-utils/index.d.ts +41 -12
  7. package/dist/test-utils/index.js +352 -171
  8. package/package.json +4 -3
  9. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +89 -69
  10. package/src/auth/multi-tenancy/organizations/__tests__/api-schemas.test.ts +194 -0
  11. package/src/auth/multi-tenancy/organizations/api-schemas.ts +136 -128
  12. package/src/business/acquisition/api-schemas.test.ts +199 -15
  13. package/src/business/acquisition/api-schemas.ts +116 -51
  14. package/src/business/acquisition/build-templates.test.ts +212 -0
  15. package/src/business/acquisition/derive-actions.test.ts +1 -1
  16. package/src/business/acquisition/types.ts +21 -38
  17. package/src/business/deals/api-schemas.ts +2 -2
  18. package/src/execution/engine/index.ts +436 -434
  19. package/src/execution/engine/tools/integration/server/adapters/google-calendar/google-calendar-adapter.ts +428 -0
  20. package/src/execution/engine/tools/integration/server/adapters/google-calendar/index.ts +2 -0
  21. package/src/execution/engine/tools/lead-service-types.ts +51 -9
  22. package/src/execution/engine/tools/platform/acquisition/company-tools.ts +7 -6
  23. package/src/execution/engine/tools/platform/acquisition/contact-tools.ts +6 -5
  24. package/src/execution/engine/tools/platform/acquisition/types.ts +20 -9
  25. package/src/execution/engine/tools/registry.ts +700 -698
  26. package/src/execution/engine/tools/tool-maps.ts +10 -0
  27. package/src/execution/external/__tests__/api-schemas.test.ts +127 -0
  28. package/src/integrations/oauth/__tests__/provider-registry.test.ts +7 -6
  29. package/src/integrations/oauth/provider-registry.ts +74 -61
  30. package/src/integrations/oauth/server/credentials.ts +43 -39
  31. package/src/knowledge/__tests__/queries.test.ts +89 -0
  32. package/src/organization-model/__tests__/graph.test.ts +108 -2
  33. package/src/organization-model/__tests__/icons.test.ts +61 -0
  34. package/src/organization-model/__tests__/knowledge.test.ts +118 -1
  35. package/src/organization-model/__tests__/prospecting-ssot.test.ts +91 -0
  36. package/src/organization-model/__tests__/schema.test.ts +122 -0
  37. package/src/organization-model/__tests__/surface-projection.test.ts +174 -0
  38. package/src/organization-model/defaults.ts +8 -0
  39. package/src/organization-model/domains/knowledge.ts +9 -0
  40. package/src/organization-model/domains/prospecting.ts +347 -226
  41. package/src/organization-model/domains/sales.ts +40 -30
  42. package/src/organization-model/graph/build.ts +74 -0
  43. package/src/organization-model/graph/schema.ts +1 -0
  44. package/src/organization-model/graph/types.ts +1 -0
  45. package/src/organization-model/icons.ts +3 -0
  46. package/src/organization-model/schema.ts +63 -0
  47. package/src/organization-model/surface-projection.ts +218 -0
  48. package/src/organization-model/types.ts +9 -1
  49. package/src/platform/constants/versions.ts +1 -1
  50. package/src/platform/utils/__tests__/validation.test.ts +1084 -1083
  51. package/src/platform/utils/validation.ts +425 -425
  52. package/src/reference/_generated/contracts.md +89 -69
  53. package/src/server.ts +6 -0
  54. package/src/supabase/database.types.ts +6 -12
@@ -135,6 +135,8 @@ export interface StatefulStageDefinition {
135
135
  /** Matches stage_key values written by workflow steps. */
136
136
  stageKey: string
137
137
  label: string
138
+ /** UI color token. Consumers may map this to their design system. */
139
+ color?: string
138
140
  states: StatefulStateDefinition[]
139
141
  }
140
142
 
@@ -251,6 +253,7 @@ export const CRM_PIPELINE_DEFINITION: StatefulPipelineDefinition = {
251
253
  {
252
254
  stageKey: 'interested',
253
255
  label: 'Interested',
256
+ color: 'blue',
254
257
  states: [
255
258
  CRM_DISCOVERY_REPLIED_STATE,
256
259
  CRM_DISCOVERY_LINK_SENT_STATE,
@@ -262,11 +265,11 @@ export const CRM_PIPELINE_DEFINITION: StatefulPipelineDefinition = {
262
265
  CRM_FOLLOWUP_3_SENT_STATE
263
266
  ]
264
267
  },
265
- { stageKey: 'proposal', label: 'Proposal', states: [] },
266
- { stageKey: 'closing', label: 'Closing', states: [] },
267
- { stageKey: 'closed_won', label: 'Closed Won', states: [] },
268
- { stageKey: 'closed_lost', label: 'Closed Lost', states: [] },
269
- { stageKey: 'nurturing', label: 'Nurturing', states: [] }
268
+ { stageKey: 'proposal', label: 'Proposal', color: 'yellow', states: [] },
269
+ { stageKey: 'closing', label: 'Closing', color: 'orange', states: [] },
270
+ { stageKey: 'closed_won', label: 'Closed Won', color: 'green', states: [] },
271
+ { stageKey: 'closed_lost', label: 'Closed Lost', color: 'red', states: [] },
272
+ { stageKey: 'nurturing', label: 'Nurturing', color: 'grape', states: [] }
270
273
  ]
271
274
  }
272
275
 
@@ -440,21 +443,21 @@ export const LEAD_GEN_PIPELINE_DEFINITIONS: Record<string, StatefulPipelineDefin
440
443
  }
441
444
 
442
445
  // ============================================================================
443
- // Lead-Gen Stage Catalog (status-based processing model)
446
+ // Lead-Gen Stage Catalog (OM Spine processing-state model)
447
+ //
448
+ // Canonical set of processing stage keys for acq_companies.processing_state and
449
+ // acq_contacts.processing_state. These keys coordinate build templates, workflow
450
+ // factory validation, API filters, and UI progress projections.
444
451
  //
445
- // Canonical set of processing stage keys for the status-map model that replaces
446
- // the stateful trait on acq_list_members and acq_list_companies. Each key maps
447
- // to a terminal status string in the processing_state jsonb column.
452
+ // State is sparse: absent keys mean "not attempted"; present keys hold terminal
453
+ // status entries such as success, no_result, skipped, or error.
448
454
  //
449
- // Sources:
455
+ // Historical sources:
450
456
  // ACQ_LIST_MEMBERS_LEAD_GEN_PIPELINE → personalized, uploaded, interested,
451
457
  // discovered, verified
452
458
  // ACQ_LIST_COMPANIES_LEAD_GEN_PIPELINE → populated, extracted, qualified
453
459
  // Design plan hint (lead-gen-domain-cleanup.mdx §4) → scraped, enriched
454
460
  //
455
- // Wave 2 will validate pipeline_config.stages[].key against this catalog at
456
- // list-create/update time. Each lead-gen workflow in operations will declare
457
- // stageImplemented: '<key>' in its WorkflowDefinition (Slice D-2).
458
461
  // ============================================================================
459
462
 
460
463
  /** One entry in the lead-gen stage catalog. */
@@ -488,15 +491,15 @@ export const LEAD_GEN_STAGE_CATALOG: Record<string, LeadGenStageCatalogEntry> =
488
491
  },
489
492
  populated: {
490
493
  key: 'populated',
491
- label: 'Populated',
492
- description: 'Company record populated with structured data from scrape results.',
494
+ label: 'Companies found',
495
+ description: 'Companies have been found and added to the lead-gen list.',
493
496
  order: 2,
494
497
  entity: 'company'
495
498
  },
496
499
  extracted: {
497
500
  key: 'extracted',
498
- label: 'Extracted',
499
- description: 'Website content extracted and parsed for company intelligence.',
501
+ label: 'Websites analyzed',
502
+ description: 'Company websites have been analyzed for business signals.',
500
503
  order: 3,
501
504
  entity: 'company'
502
505
  },
@@ -507,29 +510,36 @@ export const LEAD_GEN_STAGE_CATALOG: Record<string, LeadGenStageCatalogEntry> =
507
510
  order: 4,
508
511
  entity: 'company'
509
512
  },
513
+ 'decision-makers-enriched': {
514
+ key: 'decision-makers-enriched',
515
+ label: 'Decision-makers found',
516
+ description: 'Decision-maker contacts discovered and attached to a qualified company.',
517
+ order: 6,
518
+ entity: 'company'
519
+ },
510
520
 
511
521
  // Prospecting — contact discovery
512
522
  discovered: {
513
523
  key: 'discovered',
514
- label: 'Discovered',
515
- description: 'Contact email address discovered via an email-discovery workflow.',
524
+ label: 'Decision-makers found',
525
+ description: 'Decision-maker contact details have been found.',
516
526
  order: 5,
517
527
  entity: 'contact'
518
528
  },
519
529
  verified: {
520
530
  key: 'verified',
521
- label: 'Verified',
522
- description: 'Contact email address verified as deliverable (email verification workflow).',
523
- order: 6,
531
+ label: 'Emails verified',
532
+ description: 'Contact email addresses have been checked for deliverability.',
533
+ order: 7,
524
534
  entity: 'contact'
525
535
  },
526
536
 
527
537
  // Qualification
528
538
  qualified: {
529
539
  key: 'qualified',
530
- label: 'Qualified',
531
- description: 'Company passed the ICP qualification rubric (company-qualification workflow).',
532
- order: 7,
540
+ label: 'Companies qualified',
541
+ description: 'Companies have been scored against the qualification criteria.',
542
+ order: 8,
533
543
  entity: 'company'
534
544
  },
535
545
 
@@ -538,21 +548,21 @@ export const LEAD_GEN_STAGE_CATALOG: Record<string, LeadGenStageCatalogEntry> =
538
548
  key: 'personalized',
539
549
  label: 'Personalized',
540
550
  description: 'Outreach message personalized for the contact (Instantly personalization workflow).',
541
- order: 8,
551
+ order: 9,
542
552
  entity: 'contact'
543
553
  },
544
554
  uploaded: {
545
555
  key: 'uploaded',
546
- label: 'Uploaded',
547
- description: 'Contact uploaded to an Instantly campaign for outreach.',
548
- order: 9,
556
+ label: 'Reviewed and exported',
557
+ description: 'Approved records have been reviewed and exported for handoff.',
558
+ order: 10,
549
559
  entity: 'contact'
550
560
  },
551
561
  interested: {
552
562
  key: 'interested',
553
563
  label: 'Interested',
554
564
  description: 'Contact replied with a positive signal (Instantly reply-handler transition).',
555
- order: 10,
565
+ order: 11,
556
566
  entity: 'contact'
557
567
  }
558
568
  }
@@ -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
 
@@ -2,6 +2,7 @@ import { z } from 'zod'
2
2
 
3
3
  export const ORGANIZATION_MODEL_ICON_TOKENS = [
4
4
  'nav.dashboard',
5
+ 'nav.calendar',
5
6
  'nav.sales',
6
7
  'nav.crm',
7
8
  'nav.lead-gen',
@@ -16,6 +17,7 @@ export const ORGANIZATION_MODEL_ICON_TOKENS = [
16
17
  'knowledge.strategy',
17
18
  'knowledge.reference',
18
19
  'feature.dashboard',
20
+ 'feature.calendar',
19
21
  'feature.sales',
20
22
  'feature.crm',
21
23
  'feature.finance',
@@ -39,6 +41,7 @@ export const ORGANIZATION_MODEL_ICON_TOKENS = [
39
41
  'integration.google-sheets',
40
42
  'integration.attio',
41
43
  'surface.dashboard',
44
+ 'surface.calendar',
42
45
  'surface.overview',
43
46
  'surface.command-view',
44
47
  'surface.command-queue',
@@ -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
+ }
@@ -12,7 +12,13 @@ import { CustomersDomainSchema, CustomerSegmentSchema, FirmographicsSchema } fro
12
12
  import { OfferingsDomainSchema, ProductSchema, PricingModelSchema } from './domains/offerings'
13
13
  import { RolesDomainSchema, RoleSchema } from './domains/roles'
14
14
  import { GoalsDomainSchema, ObjectiveSchema, KeyResultSchema } from './domains/goals'
15
- import { KnowledgeDomainSchema, OrgKnowledgeNodeSchema, OrgKnowledgeKindSchema } from './domains/knowledge'
15
+ import {
16
+ KnowledgeDomainBindingSchema,
17
+ KnowledgeDomainSchema,
18
+ KnowledgeSkillBindingSchema,
19
+ OrgKnowledgeKindSchema,
20
+ OrgKnowledgeNodeSchema
21
+ } from './domains/knowledge'
16
22
  import { OrganizationModelIconTokenSchema, OrganizationModelBuiltinIconTokenSchema } from './icons'
17
23
  import { OrganizationModelSchema } from './schema'
18
24
 
@@ -47,6 +53,8 @@ export type OrganizationModelKeyResult = z.infer<typeof KeyResultSchema>
47
53
  export type OrganizationModelKnowledge = z.infer<typeof KnowledgeDomainSchema>
48
54
  export type OrgKnowledgeNode = z.infer<typeof OrgKnowledgeNodeSchema>
49
55
  export type OrgKnowledgeKind = z.infer<typeof OrgKnowledgeKindSchema>
56
+ export type KnowledgeSkillBinding = z.infer<typeof KnowledgeSkillBindingSchema>
57
+ export type KnowledgeDomainBinding = z.infer<typeof KnowledgeDomainBindingSchema>
50
58
  export type OrganizationModelIconToken = z.infer<typeof OrganizationModelIconTokenSchema>
51
59
  export type OrganizationModelBuiltinIconToken = z.infer<typeof OrganizationModelBuiltinIconTokenSchema>
52
60
 
@@ -1,3 +1,3 @@
1
1
  export const VERSION = {
2
- CURRENT: '1.7.5'
2
+ CURRENT: '1.8.6'
3
3
  }