@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.
- package/dist/index.d.ts +82 -1
- package/dist/index.js +353 -171
- package/dist/knowledge/index.d.ts +44 -1
- package/dist/organization-model/index.d.ts +82 -1
- package/dist/organization-model/index.js +353 -171
- package/dist/test-utils/index.d.ts +41 -12
- package/dist/test-utils/index.js +352 -171
- package/package.json +4 -3
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +89 -69
- package/src/auth/multi-tenancy/organizations/__tests__/api-schemas.test.ts +194 -0
- package/src/auth/multi-tenancy/organizations/api-schemas.ts +136 -128
- package/src/business/acquisition/api-schemas.test.ts +199 -15
- package/src/business/acquisition/api-schemas.ts +116 -51
- package/src/business/acquisition/build-templates.test.ts +212 -0
- package/src/business/acquisition/derive-actions.test.ts +1 -1
- package/src/business/acquisition/types.ts +21 -38
- package/src/business/deals/api-schemas.ts +2 -2
- package/src/execution/engine/index.ts +436 -434
- package/src/execution/engine/tools/integration/server/adapters/google-calendar/google-calendar-adapter.ts +428 -0
- package/src/execution/engine/tools/integration/server/adapters/google-calendar/index.ts +2 -0
- package/src/execution/engine/tools/lead-service-types.ts +51 -9
- package/src/execution/engine/tools/platform/acquisition/company-tools.ts +7 -6
- package/src/execution/engine/tools/platform/acquisition/contact-tools.ts +6 -5
- package/src/execution/engine/tools/platform/acquisition/types.ts +20 -9
- package/src/execution/engine/tools/registry.ts +700 -698
- package/src/execution/engine/tools/tool-maps.ts +10 -0
- package/src/execution/external/__tests__/api-schemas.test.ts +127 -0
- package/src/integrations/oauth/__tests__/provider-registry.test.ts +7 -6
- package/src/integrations/oauth/provider-registry.ts +74 -61
- package/src/integrations/oauth/server/credentials.ts +43 -39
- package/src/knowledge/__tests__/queries.test.ts +89 -0
- package/src/organization-model/__tests__/graph.test.ts +108 -2
- package/src/organization-model/__tests__/icons.test.ts +61 -0
- package/src/organization-model/__tests__/knowledge.test.ts +118 -1
- package/src/organization-model/__tests__/prospecting-ssot.test.ts +91 -0
- package/src/organization-model/__tests__/schema.test.ts +122 -0
- package/src/organization-model/__tests__/surface-projection.test.ts +174 -0
- package/src/organization-model/defaults.ts +8 -0
- package/src/organization-model/domains/knowledge.ts +9 -0
- package/src/organization-model/domains/prospecting.ts +347 -226
- package/src/organization-model/domains/sales.ts +40 -30
- package/src/organization-model/graph/build.ts +74 -0
- package/src/organization-model/graph/schema.ts +1 -0
- package/src/organization-model/graph/types.ts +1 -0
- package/src/organization-model/icons.ts +3 -0
- package/src/organization-model/schema.ts +63 -0
- package/src/organization-model/surface-projection.ts +218 -0
- package/src/organization-model/types.ts +9 -1
- package/src/platform/constants/versions.ts +1 -1
- package/src/platform/utils/__tests__/validation.test.ts +1084 -1083
- package/src/platform/utils/validation.ts +425 -425
- package/src/reference/_generated/contracts.md +89 -69
- package/src/server.ts +6 -0
- 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 (
|
|
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
|
-
//
|
|
446
|
-
//
|
|
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
|
-
//
|
|
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: '
|
|
492
|
-
description: '
|
|
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: '
|
|
499
|
-
description: '
|
|
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: '
|
|
515
|
-
description: '
|
|
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: '
|
|
522
|
-
description: 'Contact email
|
|
523
|
-
order:
|
|
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: '
|
|
531
|
-
description: '
|
|
532
|
-
order:
|
|
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:
|
|
551
|
+
order: 9,
|
|
542
552
|
entity: 'contact'
|
|
543
553
|
},
|
|
544
554
|
uploaded: {
|
|
545
555
|
key: 'uploaded',
|
|
546
|
-
label: '
|
|
547
|
-
description: '
|
|
548
|
-
order:
|
|
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:
|
|
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)
|
|
@@ -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 {
|
|
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
|
|