@elevasis/core 0.34.2 → 0.35.1
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/auth/index.d.ts +74 -2
- package/dist/auth/index.js +67 -30
- package/dist/index.d.ts +60 -2
- package/dist/index.js +54 -1
- package/dist/knowledge/index.d.ts +12 -0
- package/dist/organization-model/index.d.ts +60 -2
- package/dist/organization-model/index.js +54 -1
- package/dist/test-utils/index.d.ts +12 -0
- package/dist/test-utils/index.js +51 -0
- package/package.json +1 -1
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +69 -30
- package/src/auth/multi-tenancy/index.ts +29 -26
- package/src/auth/multi-tenancy/org-id.test.ts +139 -0
- package/src/auth/multi-tenancy/org-id.ts +112 -0
- package/src/business/acquisition/api-schemas.test.ts +456 -28
- package/src/business/acquisition/ontology-validation.ts +715 -23
- package/src/execution/engine/tools/platform/storage/__tests__/storage.test.ts +997 -998
- package/src/organization-model/__tests__/domains/systems.test.ts +61 -15
- package/src/organization-model/__tests__/domains/topology.test.ts +23 -0
- package/src/organization-model/__tests__/lookup-helpers.test.ts +34 -0
- package/src/organization-model/__tests__/schema.test.ts +112 -0
- package/src/organization-model/domains/systems.ts +44 -0
- package/src/organization-model/domains/topology.ts +18 -1
- package/src/organization-model/helpers.ts +18 -0
- package/src/organization-model/published.ts +19 -1
- package/src/organization-model/schema-refinements.ts +23 -0
- package/src/organization-model/types.ts +17 -1
- package/src/platform/constants/versions.ts +3 -3
- package/src/platform/registry/__tests__/resource-registry.test.ts +73 -4
- package/src/platform/registry/__tests__/validation.test.ts +218 -4
- package/src/platform/registry/index.ts +28 -15
- package/src/platform/registry/resource-registry.ts +2 -0
- package/src/platform/registry/validation.ts +172 -2
- package/src/reference/_generated/contracts.md +44 -0
- package/src/supabase/__tests__/helpers.test.ts +92 -51
- package/src/supabase/helpers.ts +40 -20
- package/src/supabase/index.ts +52 -52
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
compileOrganizationOntology,
|
|
3
3
|
formatOntologyId,
|
|
4
|
+
parseOntologyId,
|
|
4
5
|
type OntologyActionType,
|
|
5
6
|
type OntologyCatalogType,
|
|
6
7
|
type OntologyId,
|
|
@@ -8,12 +9,73 @@ import {
|
|
|
8
9
|
type ResolvedOntologyIndex
|
|
9
10
|
} from '../../organization-model/ontology'
|
|
10
11
|
import type { OrganizationModel } from '../../organization-model/types'
|
|
12
|
+
import type { ResourceEntry } from '../../organization-model/domains/resources'
|
|
13
|
+
import type { OmTopologyRelationship } from '../../organization-model/domains/topology'
|
|
11
14
|
import {
|
|
12
15
|
type LeadGenStageCatalogEntry,
|
|
13
16
|
type StatefulStateDefinition
|
|
14
17
|
} from '../../organization-model/domains/sales'
|
|
18
|
+
import { getSystem } from '../../organization-model/helpers'
|
|
15
19
|
import { getLeadGenStageCatalog } from '../../organization-model/migration-helpers'
|
|
16
20
|
|
|
21
|
+
export const LEAD_GEN_API_INTERFACE = {
|
|
22
|
+
systemPath: 'sales.lead-gen',
|
|
23
|
+
interfaceKey: 'api',
|
|
24
|
+
readinessProfile: 'sales.lead-gen.api'
|
|
25
|
+
} as const
|
|
26
|
+
|
|
27
|
+
export const CRM_API_INTERFACE = {
|
|
28
|
+
systemPath: 'sales.crm',
|
|
29
|
+
interfaceKey: 'api',
|
|
30
|
+
readinessProfile: 'sales.crm.api'
|
|
31
|
+
} as const
|
|
32
|
+
|
|
33
|
+
export const LEAD_GEN_CRM_HANDOFF_INTERFACE = {
|
|
34
|
+
systemPath: 'sales.lead-gen',
|
|
35
|
+
interfaceKey: 'crm-handoff',
|
|
36
|
+
readinessProfile: 'sales.lead-gen.crm-handoff'
|
|
37
|
+
} as const
|
|
38
|
+
|
|
39
|
+
const LEAD_GEN_API_READINESS_LABEL = LEAD_GEN_API_INTERFACE.readinessProfile
|
|
40
|
+
const CRM_API_READINESS_LABEL = CRM_API_INTERFACE.readinessProfile
|
|
41
|
+
const LEAD_GEN_CRM_HANDOFF_READINESS_LABEL = LEAD_GEN_CRM_HANDOFF_INTERFACE.readinessProfile
|
|
42
|
+
|
|
43
|
+
export const LEAD_GEN_LIST_OBJECT_ONTOLOGY_ID = formatOntologyId({
|
|
44
|
+
scope: 'sales.lead-gen',
|
|
45
|
+
kind: 'object',
|
|
46
|
+
localId: 'list'
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
export const LEAD_GEN_COMPANY_OBJECT_ONTOLOGY_ID = formatOntologyId({
|
|
50
|
+
scope: 'sales.lead-gen',
|
|
51
|
+
kind: 'object',
|
|
52
|
+
localId: 'company'
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
export const LEAD_GEN_CONTACT_OBJECT_ONTOLOGY_ID = formatOntologyId({
|
|
56
|
+
scope: 'sales.lead-gen',
|
|
57
|
+
kind: 'object',
|
|
58
|
+
localId: 'contact'
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
export const LEAD_GEN_BUILD_TEMPLATE_CATALOG_ONTOLOGY_ID = formatOntologyId({
|
|
62
|
+
scope: 'sales.lead-gen',
|
|
63
|
+
kind: 'catalog',
|
|
64
|
+
localId: 'build-template'
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
export const LEAD_GEN_COMPANY_STAGE_CATALOG_ONTOLOGY_ID = formatOntologyId({
|
|
68
|
+
scope: 'sales.lead-gen',
|
|
69
|
+
kind: 'catalog',
|
|
70
|
+
localId: 'company-stage'
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
export const LEAD_GEN_CONTACT_STAGE_CATALOG_ONTOLOGY_ID = formatOntologyId({
|
|
74
|
+
scope: 'sales.lead-gen',
|
|
75
|
+
kind: 'catalog',
|
|
76
|
+
localId: 'contact-stage'
|
|
77
|
+
})
|
|
78
|
+
|
|
17
79
|
export const CRM_PIPELINE_CATALOG_ONTOLOGY_ID = formatOntologyId({
|
|
18
80
|
scope: 'sales.crm',
|
|
19
81
|
kind: 'catalog',
|
|
@@ -26,11 +88,74 @@ export const LEAD_GEN_STAGE_CATALOG_ONTOLOGY_ID = formatOntologyId({
|
|
|
26
88
|
localId: 'lead-gen.stage-catalog'
|
|
27
89
|
})
|
|
28
90
|
|
|
29
|
-
|
|
91
|
+
type BusinessOntologyValidationIndexBase = {
|
|
30
92
|
ontology: ResolvedOntologyIndex
|
|
93
|
+
actionTypesByLegacyId: Record<string, OntologyActionType>
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type LeadGenApiOntologyValidationIndex = BusinessOntologyValidationIndexBase & {
|
|
97
|
+
leadGenStageCatalog: OntologyCatalogType
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type CrmApiOntologyValidationIndex = BusinessOntologyValidationIndexBase & {
|
|
31
101
|
crmPipelineCatalog: OntologyCatalogType
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type LeadGenCrmHandoffOntologyValidationIndex = BusinessOntologyValidationIndexBase & {
|
|
32
105
|
leadGenStageCatalog: OntologyCatalogType
|
|
33
|
-
|
|
106
|
+
crmPipelineCatalog: OntologyCatalogType
|
|
107
|
+
bridgeGrant?: OmTopologyRelationship
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type BusinessOntologyValidationIndex = LeadGenApiOntologyValidationIndex & CrmApiOntologyValidationIndex
|
|
111
|
+
|
|
112
|
+
export type SystemInterfaceReadinessIssueFamily =
|
|
113
|
+
| 'SYSTEM_INTERFACE_MISSING'
|
|
114
|
+
| 'SYSTEM_INTERFACE_DISABLED'
|
|
115
|
+
| 'SYSTEM_INTERFACE_INVALID'
|
|
116
|
+
| 'SYSTEM_INTERFACE_NOT_READY'
|
|
117
|
+
| 'SYSTEM_BRIDGE_NOT_READY'
|
|
118
|
+
|
|
119
|
+
export interface SystemInterfaceReadinessIssue {
|
|
120
|
+
family: SystemInterfaceReadinessIssueFamily
|
|
121
|
+
code: string
|
|
122
|
+
path?: string
|
|
123
|
+
ref?: string
|
|
124
|
+
message: string
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface SystemInterfaceReadinessRequest {
|
|
128
|
+
systemPath: string
|
|
129
|
+
interfaceKey: string
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface SystemInterfaceReadinessResult {
|
|
133
|
+
ready: boolean
|
|
134
|
+
systemPath: string
|
|
135
|
+
interfaceKey: string
|
|
136
|
+
readinessProfile?: string
|
|
137
|
+
scopedResourceIds: string[]
|
|
138
|
+
issues: SystemInterfaceReadinessIssue[]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export class SystemInterfaceReadinessError extends Error {
|
|
142
|
+
public readonly code: SystemInterfaceReadinessIssueFamily
|
|
143
|
+
public readonly statusCode = 503
|
|
144
|
+
public readonly systemPath: string
|
|
145
|
+
public readonly interfaceKey: string
|
|
146
|
+
public readonly readinessProfile?: string
|
|
147
|
+
public readonly issues: SystemInterfaceReadinessIssue[]
|
|
148
|
+
|
|
149
|
+
constructor(result: SystemInterfaceReadinessResult) {
|
|
150
|
+
const firstIssue = result.issues[0]
|
|
151
|
+
super(formatInterfaceReadinessFailure(result))
|
|
152
|
+
this.name = 'SystemInterfaceReadinessError'
|
|
153
|
+
this.code = firstIssue?.family ?? 'SYSTEM_INTERFACE_NOT_READY'
|
|
154
|
+
this.systemPath = result.systemPath
|
|
155
|
+
this.interfaceKey = result.interfaceKey
|
|
156
|
+
this.readinessProfile = result.readinessProfile
|
|
157
|
+
this.issues = result.issues
|
|
158
|
+
}
|
|
34
159
|
}
|
|
35
160
|
|
|
36
161
|
function createLeadGenStageCatalog(model: OrganizationModel): OntologyCatalogType {
|
|
@@ -55,17 +180,344 @@ function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
|
55
180
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
56
181
|
}
|
|
57
182
|
|
|
58
|
-
function
|
|
183
|
+
function addReadinessIssue(
|
|
184
|
+
issues: SystemInterfaceReadinessIssue[],
|
|
185
|
+
family: SystemInterfaceReadinessIssueFamily,
|
|
186
|
+
code: string,
|
|
187
|
+
message: string,
|
|
188
|
+
details: Pick<SystemInterfaceReadinessIssue, 'path' | 'ref'> = {}
|
|
189
|
+
): void {
|
|
190
|
+
issues.push({
|
|
191
|
+
family,
|
|
192
|
+
code,
|
|
193
|
+
message,
|
|
194
|
+
...details
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function formatInterfaceIdentity(systemPath: string, interfaceKey: string): string {
|
|
199
|
+
return `${systemPath}/${interfaceKey}`
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function profileForInterface(systemPath: string, interfaceKey: string, readinessProfile?: string): string {
|
|
203
|
+
return readinessProfile ?? `${systemPath}.${interfaceKey}`
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function readinessMarkerPath(context: { systemPath: string; interfaceKey: string }): string {
|
|
207
|
+
return context.interfaceKey === 'api'
|
|
208
|
+
? `systems.${context.systemPath}.apiInterface`
|
|
209
|
+
: `systems.${context.systemPath}.derivedCrmHandoffReadiness`
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function formatInterfaceReadinessFailure(result: SystemInterfaceReadinessResult): string {
|
|
213
|
+
const identity = formatInterfaceIdentity(result.systemPath, result.interfaceKey)
|
|
214
|
+
const issueSummary = result.issues.map((issue) => `${issue.family}:${issue.code}: ${issue.message}`).join('; ')
|
|
215
|
+
return `${identity} readiness failed${result.readinessProfile ? ` (${result.readinessProfile})` : ''}: ${issueSummary}`
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function throwIfInterfaceNotReady(result: SystemInterfaceReadinessResult): void {
|
|
219
|
+
if (result.ready) return
|
|
220
|
+
throw new SystemInterfaceReadinessError(result)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function getActiveScopedResources(
|
|
224
|
+
model: OrganizationModel,
|
|
225
|
+
resourceIds: string[],
|
|
226
|
+
issues: SystemInterfaceReadinessIssue[],
|
|
227
|
+
context: { systemPath: string; interfaceKey: string }
|
|
228
|
+
): ResourceEntry[] {
|
|
229
|
+
const resources: ResourceEntry[] = []
|
|
230
|
+
|
|
231
|
+
for (const [index, resourceId] of resourceIds.entries()) {
|
|
232
|
+
const resource = model.resources?.[resourceId]
|
|
233
|
+
const path = `${readinessMarkerPath(context)}.resourceIds.${index}`
|
|
234
|
+
if (resource === undefined) {
|
|
235
|
+
addReadinessIssue(
|
|
236
|
+
issues,
|
|
237
|
+
'SYSTEM_INTERFACE_NOT_READY',
|
|
238
|
+
'missing-resource',
|
|
239
|
+
`System Interface "${formatInterfaceIdentity(context.systemPath, context.interfaceKey)}" scopes missing resource "${resourceId}".`,
|
|
240
|
+
{ path, ref: resourceId }
|
|
241
|
+
)
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (resource.systemPath !== context.systemPath) {
|
|
246
|
+
addReadinessIssue(
|
|
247
|
+
issues,
|
|
248
|
+
'SYSTEM_INTERFACE_INVALID',
|
|
249
|
+
'resource-system-mismatch',
|
|
250
|
+
`System Interface "${formatInterfaceIdentity(context.systemPath, context.interfaceKey)}" scopes resource "${resourceId}" from system "${resource.systemPath}".`,
|
|
251
|
+
{ path, ref: resourceId }
|
|
252
|
+
)
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (resource.status !== 'active') {
|
|
257
|
+
addReadinessIssue(
|
|
258
|
+
issues,
|
|
259
|
+
'SYSTEM_INTERFACE_NOT_READY',
|
|
260
|
+
'inactive-resource',
|
|
261
|
+
`System Interface "${formatInterfaceIdentity(context.systemPath, context.interfaceKey)}" scopes inactive resource "${resourceId}".`,
|
|
262
|
+
{ path, ref: resourceId }
|
|
263
|
+
)
|
|
264
|
+
continue
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
resources.push(resource)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return resources
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function resourceBindingIds(resources: ResourceEntry[], key: 'reads' | 'writes' | 'usesCatalogs' | 'actions' | 'emits'): Set<string> {
|
|
274
|
+
return new Set(resources.flatMap((resource) => resource.ontology?.[key] ?? []))
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function requireScopedBinding(
|
|
278
|
+
issues: SystemInterfaceReadinessIssue[],
|
|
279
|
+
boundIds: Set<string>,
|
|
280
|
+
bindingKey: 'reads' | 'writes' | 'usesCatalogs' | 'actions' | 'emits',
|
|
281
|
+
ontologyId: string,
|
|
282
|
+
context: { systemPath: string; interfaceKey: string }
|
|
283
|
+
): void {
|
|
284
|
+
if (boundIds.has(ontologyId)) return
|
|
285
|
+
|
|
286
|
+
addReadinessIssue(
|
|
287
|
+
issues,
|
|
288
|
+
'SYSTEM_INTERFACE_NOT_READY',
|
|
289
|
+
'missing-resource-binding',
|
|
290
|
+
`System Interface "${formatInterfaceIdentity(context.systemPath, context.interfaceKey)}" has no active scoped resource with ontology.${bindingKey} binding "${ontologyId}".`,
|
|
291
|
+
{ path: `${readinessMarkerPath(context)}.resourceIds`, ref: ontologyId }
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function ontologyOwnerMatches(ontologyId: string, expectedSystemPath: string, catalog: OntologyCatalogType | undefined): boolean {
|
|
296
|
+
if (catalog?.ownerSystemId !== undefined) return catalog.ownerSystemId === expectedSystemPath
|
|
297
|
+
return parseOntologyId(ontologyId).scope === expectedSystemPath
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function requireObjectReadiness(
|
|
301
|
+
issues: SystemInterfaceReadinessIssue[],
|
|
302
|
+
index: BusinessOntologyValidationIndexBase,
|
|
303
|
+
objectId: OntologyId,
|
|
304
|
+
context: { systemPath: string; interfaceKey: string }
|
|
305
|
+
): void {
|
|
306
|
+
const object = index.ontology.objectTypes[objectId]
|
|
307
|
+
if (object === undefined) {
|
|
308
|
+
addReadinessIssue(
|
|
309
|
+
issues,
|
|
310
|
+
'SYSTEM_INTERFACE_NOT_READY',
|
|
311
|
+
'missing-object',
|
|
312
|
+
`System Interface "${formatInterfaceIdentity(context.systemPath, context.interfaceKey)}" is missing required object type "${objectId}".`,
|
|
313
|
+
{ ref: objectId }
|
|
314
|
+
)
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const ownerSystemId = object.ownerSystemId ?? parseOntologyId(objectId).scope
|
|
319
|
+
if (ownerSystemId !== context.systemPath) {
|
|
320
|
+
addReadinessIssue(
|
|
321
|
+
issues,
|
|
322
|
+
'SYSTEM_INTERFACE_INVALID',
|
|
323
|
+
'foreign-object',
|
|
324
|
+
`System Interface "${formatInterfaceIdentity(context.systemPath, context.interfaceKey)}" requires object "${objectId}" owned by "${ownerSystemId}".`,
|
|
325
|
+
{ ref: objectId }
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function requireCatalogReadiness(
|
|
331
|
+
issues: SystemInterfaceReadinessIssue[],
|
|
332
|
+
index: BusinessOntologyValidationIndexBase,
|
|
333
|
+
catalogId: OntologyId,
|
|
334
|
+
context: { systemPath: string; interfaceKey: string; allowForeignOwner?: boolean }
|
|
335
|
+
): OntologyCatalogType | undefined {
|
|
336
|
+
const catalog = index.ontology.catalogTypes[catalogId]
|
|
337
|
+
if (catalog === undefined) {
|
|
338
|
+
addReadinessIssue(
|
|
339
|
+
issues,
|
|
340
|
+
'SYSTEM_INTERFACE_NOT_READY',
|
|
341
|
+
'missing-catalog',
|
|
342
|
+
`System Interface "${formatInterfaceIdentity(context.systemPath, context.interfaceKey)}" is missing required catalog "${catalogId}".`,
|
|
343
|
+
{ ref: catalogId }
|
|
344
|
+
)
|
|
345
|
+
return undefined
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (context.allowForeignOwner !== true && !ontologyOwnerMatches(catalogId, context.systemPath, catalog)) {
|
|
349
|
+
addReadinessIssue(
|
|
350
|
+
issues,
|
|
351
|
+
'SYSTEM_INTERFACE_INVALID',
|
|
352
|
+
'foreign-catalog',
|
|
353
|
+
`System Interface "${formatInterfaceIdentity(context.systemPath, context.interfaceKey)}" requires catalog "${catalogId}" owned by "${catalog.ownerSystemId ?? parseOntologyId(catalogId).scope}".`,
|
|
354
|
+
{ ref: catalogId }
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (Object.keys(getCatalogEntries(catalog)).length === 0) {
|
|
359
|
+
addReadinessIssue(
|
|
360
|
+
issues,
|
|
361
|
+
'SYSTEM_INTERFACE_NOT_READY',
|
|
362
|
+
'empty-catalog',
|
|
363
|
+
`System Interface "${formatInterfaceIdentity(context.systemPath, context.interfaceKey)}" requires catalog entries for "${catalogId}".`,
|
|
364
|
+
{ ref: catalogId }
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return catalog
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function requireLeadGenInterfaceReadiness(
|
|
372
|
+
issues: SystemInterfaceReadinessIssue[],
|
|
373
|
+
index: BusinessOntologyValidationIndexBase,
|
|
374
|
+
resources: ResourceEntry[],
|
|
375
|
+
context: { systemPath: string; interfaceKey: string }
|
|
376
|
+
): OntologyCatalogType | undefined {
|
|
377
|
+
const reads = resourceBindingIds(resources, 'reads')
|
|
378
|
+
const catalogs = resourceBindingIds(resources, 'usesCatalogs')
|
|
379
|
+
|
|
380
|
+
for (const objectId of [
|
|
381
|
+
LEAD_GEN_LIST_OBJECT_ONTOLOGY_ID,
|
|
382
|
+
LEAD_GEN_COMPANY_OBJECT_ONTOLOGY_ID,
|
|
383
|
+
LEAD_GEN_CONTACT_OBJECT_ONTOLOGY_ID
|
|
384
|
+
]) {
|
|
385
|
+
requireObjectReadiness(issues, index, objectId, context)
|
|
386
|
+
requireScopedBinding(issues, reads, 'reads', objectId, context)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
for (const catalogId of [
|
|
390
|
+
LEAD_GEN_BUILD_TEMPLATE_CATALOG_ONTOLOGY_ID,
|
|
391
|
+
LEAD_GEN_COMPANY_STAGE_CATALOG_ONTOLOGY_ID,
|
|
392
|
+
LEAD_GEN_CONTACT_STAGE_CATALOG_ONTOLOGY_ID
|
|
393
|
+
]) {
|
|
394
|
+
requireCatalogReadiness(issues, index, catalogId, context)
|
|
395
|
+
requireScopedBinding(issues, catalogs, 'usesCatalogs', catalogId, context)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const templateStepCatalog = findLeadGenTemplateStepCatalog(index)
|
|
399
|
+
if (templateStepCatalog === undefined) {
|
|
400
|
+
addReadinessIssue(
|
|
401
|
+
issues,
|
|
402
|
+
'SYSTEM_INTERFACE_NOT_READY',
|
|
403
|
+
'missing-template-step-catalog',
|
|
404
|
+
`System Interface "${formatInterfaceIdentity(context.systemPath, context.interfaceKey)}" is missing a lead-gen template-step catalog.`
|
|
405
|
+
)
|
|
406
|
+
} else {
|
|
407
|
+
requireCatalogReadiness(issues, index, templateStepCatalog.id, context)
|
|
408
|
+
requireScopedBinding(issues, catalogs, 'usesCatalogs', templateStepCatalog.id, context)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const leadGenStageCatalog = requireCatalogReadiness(issues, index, LEAD_GEN_STAGE_CATALOG_ONTOLOGY_ID, context)
|
|
412
|
+
return leadGenStageCatalog
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function requireCrmInterfaceReadiness(
|
|
416
|
+
issues: SystemInterfaceReadinessIssue[],
|
|
417
|
+
index: BusinessOntologyValidationIndexBase,
|
|
418
|
+
resources: ResourceEntry[],
|
|
419
|
+
context: { systemPath: string; interfaceKey: string; allowForeignOwner?: boolean }
|
|
420
|
+
): OntologyCatalogType | undefined {
|
|
421
|
+
const catalogs = resourceBindingIds(resources, 'usesCatalogs')
|
|
422
|
+
const crmPipelineCatalog = requireCatalogReadiness(issues, index, CRM_PIPELINE_CATALOG_ONTOLOGY_ID, context)
|
|
423
|
+
requireScopedBinding(issues, catalogs, 'usesCatalogs', CRM_PIPELINE_CATALOG_ONTOLOGY_ID, context)
|
|
424
|
+
return crmPipelineCatalog
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function findSystemInterfaceGrant(
|
|
428
|
+
model: OrganizationModel,
|
|
429
|
+
consumer: { systemPath: string; interfaceKey: string },
|
|
430
|
+
provider: { systemPath: string; interfaceKey: string }
|
|
431
|
+
): OmTopologyRelationship | undefined {
|
|
432
|
+
return Object.values(model.topology?.relationships ?? {}).find((relationship) => {
|
|
433
|
+
const grant = relationship.metadata?.['systemInterfaceGrant']
|
|
434
|
+
if (!isPlainRecord(grant) || !isPlainRecord(grant.consumer) || !isPlainRecord(grant.provider)) return false
|
|
435
|
+
return (
|
|
436
|
+
grant.consumer.systemPath === consumer.systemPath &&
|
|
437
|
+
grant.consumer.interfaceKey === consumer.interfaceKey &&
|
|
438
|
+
grant.provider.systemPath === provider.systemPath &&
|
|
439
|
+
grant.provider.interfaceKey === provider.interfaceKey
|
|
440
|
+
)
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function requireHandoffBridgeReadiness(
|
|
445
|
+
issues: SystemInterfaceReadinessIssue[],
|
|
446
|
+
model: OrganizationModel,
|
|
447
|
+
context: { systemPath: string; interfaceKey: string }
|
|
448
|
+
): OmTopologyRelationship | undefined {
|
|
449
|
+
const crmResult = computeInterfaceReadiness(model, CRM_API_INTERFACE)
|
|
450
|
+
if (!crmResult.ready) {
|
|
451
|
+
for (const issue of crmResult.issues) {
|
|
452
|
+
addReadinessIssue(
|
|
453
|
+
issues,
|
|
454
|
+
'SYSTEM_BRIDGE_NOT_READY',
|
|
455
|
+
issue.code,
|
|
456
|
+
`Provider interface ${formatInterfaceIdentity(CRM_API_INTERFACE.systemPath, CRM_API_INTERFACE.interfaceKey)} is not ready: ${issue.message}`,
|
|
457
|
+
{ path: issue.path, ref: issue.ref }
|
|
458
|
+
)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const bridgeGrant = findSystemInterfaceGrant(model, context, CRM_API_INTERFACE)
|
|
463
|
+
if (bridgeGrant === undefined) {
|
|
464
|
+
addReadinessIssue(
|
|
465
|
+
issues,
|
|
466
|
+
'SYSTEM_BRIDGE_NOT_READY',
|
|
467
|
+
'missing-topology-grant',
|
|
468
|
+
`System Interface "${formatInterfaceIdentity(context.systemPath, context.interfaceKey)}" requires a scoped topology grant to "${formatInterfaceIdentity(CRM_API_INTERFACE.systemPath, CRM_API_INTERFACE.interfaceKey)}".`
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return bridgeGrant
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function getLeadGenCrmHandoffResourceIds(model: OrganizationModel): string[] {
|
|
476
|
+
return Object.values(model.resources ?? {})
|
|
477
|
+
.filter(
|
|
478
|
+
(resource) =>
|
|
479
|
+
resource.systemPath === LEAD_GEN_CRM_HANDOFF_INTERFACE.systemPath &&
|
|
480
|
+
resource.ontology?.usesCatalogs?.includes(CRM_PIPELINE_CATALOG_ONTOLOGY_ID) === true
|
|
481
|
+
)
|
|
482
|
+
.map((resource) => resource.id)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function getSystemInterfaceReadinessMarker(
|
|
486
|
+
model: OrganizationModel,
|
|
487
|
+
request: SystemInterfaceReadinessRequest
|
|
488
|
+
): { lifecycle: string; readinessProfile?: string; resourceIds?: string[] } | undefined {
|
|
489
|
+
const system = getSystem(model, request.systemPath)
|
|
490
|
+
if (system === undefined) return undefined
|
|
491
|
+
|
|
492
|
+
if (request.interfaceKey === LEAD_GEN_API_INTERFACE.interfaceKey) {
|
|
493
|
+
return system.apiInterface
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (
|
|
497
|
+
request.systemPath === LEAD_GEN_CRM_HANDOFF_INTERFACE.systemPath &&
|
|
498
|
+
request.interfaceKey === LEAD_GEN_CRM_HANDOFF_INTERFACE.interfaceKey
|
|
499
|
+
) {
|
|
500
|
+
return {
|
|
501
|
+
lifecycle: 'active',
|
|
502
|
+
readinessProfile: LEAD_GEN_CRM_HANDOFF_INTERFACE.readinessProfile,
|
|
503
|
+
resourceIds: getLeadGenCrmHandoffResourceIds(model)
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return undefined
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function mergeLeadGenDerivedCatalogs(model: OrganizationModel): OrganizationModel {
|
|
59
511
|
const baseCatalogTypes = model.ontology?.catalogTypes ?? {}
|
|
60
|
-
const
|
|
512
|
+
const derivedCatalogTypes: Record<OntologyId, OntologyCatalogType> = {}
|
|
61
513
|
|
|
62
|
-
//
|
|
63
|
-
//
|
|
514
|
+
// Lead-gen validators consume a normalized stage catalog derived from the
|
|
515
|
+
// owning system's company/contact stage catalogs. CRM catalogs are never injected here.
|
|
64
516
|
if (baseCatalogTypes[LEAD_GEN_STAGE_CATALOG_ONTOLOGY_ID] === undefined) {
|
|
65
|
-
|
|
517
|
+
derivedCatalogTypes[LEAD_GEN_STAGE_CATALOG_ONTOLOGY_ID] = createLeadGenStageCatalog(model)
|
|
66
518
|
}
|
|
67
519
|
|
|
68
|
-
if (Object.keys(
|
|
520
|
+
if (Object.keys(derivedCatalogTypes).length === 0) return model
|
|
69
521
|
|
|
70
522
|
return {
|
|
71
523
|
...model,
|
|
@@ -73,33 +525,273 @@ function mergeBridgeCatalogs(model: OrganizationModel): OrganizationModel {
|
|
|
73
525
|
...(model.ontology ?? {}),
|
|
74
526
|
catalogTypes: {
|
|
75
527
|
...baseCatalogTypes,
|
|
76
|
-
...
|
|
528
|
+
...derivedCatalogTypes
|
|
77
529
|
}
|
|
78
530
|
} satisfies OntologyScope
|
|
79
531
|
}
|
|
80
532
|
}
|
|
81
533
|
|
|
82
|
-
|
|
83
|
-
const compilation = compileOrganizationOntology(
|
|
534
|
+
function compileBusinessOntology(model: OrganizationModel, contractId: string): BusinessOntologyValidationIndexBase {
|
|
535
|
+
const compilation = compileOrganizationOntology(mergeLeadGenDerivedCatalogs(model))
|
|
84
536
|
if (compilation.diagnostics.length > 0) {
|
|
85
537
|
const summary = compilation.diagnostics.map((diagnostic) => diagnostic.message).join('; ')
|
|
86
|
-
throw new Error(
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const crmPipelineCatalog = compilation.ontology.catalogTypes[CRM_PIPELINE_CATALOG_ONTOLOGY_ID]
|
|
90
|
-
const leadGenStageCatalog = compilation.ontology.catalogTypes[LEAD_GEN_STAGE_CATALOG_ONTOLOGY_ID]
|
|
91
|
-
if (crmPipelineCatalog === undefined || leadGenStageCatalog === undefined) {
|
|
92
|
-
throw new Error('Business ontology validation index is missing CRM or lead-gen catalog bridge records')
|
|
538
|
+
throw new Error(`${contractId} ontology validation index failed to compile: ${summary}`)
|
|
93
539
|
}
|
|
94
540
|
|
|
95
541
|
return {
|
|
96
542
|
ontology: compilation.ontology,
|
|
97
|
-
crmPipelineCatalog,
|
|
98
|
-
leadGenStageCatalog,
|
|
99
543
|
actionTypesByLegacyId: indexActionTypesByLegacyId(compilation.ontology.actionTypes)
|
|
100
544
|
}
|
|
101
545
|
}
|
|
102
546
|
|
|
547
|
+
function tryCompileBusinessOntology(
|
|
548
|
+
model: OrganizationModel,
|
|
549
|
+
readinessProfile: string,
|
|
550
|
+
issues: SystemInterfaceReadinessIssue[]
|
|
551
|
+
): BusinessOntologyValidationIndexBase | undefined {
|
|
552
|
+
try {
|
|
553
|
+
return compileBusinessOntology(model, readinessProfile)
|
|
554
|
+
} catch (error) {
|
|
555
|
+
addReadinessIssue(
|
|
556
|
+
issues,
|
|
557
|
+
'SYSTEM_INTERFACE_INVALID',
|
|
558
|
+
'ontology-compile-failed',
|
|
559
|
+
error instanceof Error ? error.message : String(error)
|
|
560
|
+
)
|
|
561
|
+
return undefined
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export function computeInterfaceReadiness(
|
|
566
|
+
model: OrganizationModel,
|
|
567
|
+
request: SystemInterfaceReadinessRequest
|
|
568
|
+
): SystemInterfaceReadinessResult {
|
|
569
|
+
const issues: SystemInterfaceReadinessIssue[] = []
|
|
570
|
+
const system = getSystem(model, request.systemPath)
|
|
571
|
+
const systemInterface = getSystemInterfaceReadinessMarker(model, request)
|
|
572
|
+
const readinessProfile =
|
|
573
|
+
systemInterface === undefined
|
|
574
|
+
? undefined
|
|
575
|
+
: profileForInterface(request.systemPath, request.interfaceKey, systemInterface.readinessProfile)
|
|
576
|
+
const scopedResourceIds = systemInterface?.resourceIds ?? []
|
|
577
|
+
|
|
578
|
+
if (system === undefined) {
|
|
579
|
+
addReadinessIssue(
|
|
580
|
+
issues,
|
|
581
|
+
'SYSTEM_INTERFACE_MISSING',
|
|
582
|
+
'missing-system',
|
|
583
|
+
`System "${request.systemPath}" is missing.`,
|
|
584
|
+
{ ref: request.systemPath }
|
|
585
|
+
)
|
|
586
|
+
return { ready: false, systemPath: request.systemPath, interfaceKey: request.interfaceKey, scopedResourceIds, issues }
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (systemInterface === undefined) {
|
|
590
|
+
addReadinessIssue(
|
|
591
|
+
issues,
|
|
592
|
+
'SYSTEM_INTERFACE_MISSING',
|
|
593
|
+
'missing-interface',
|
|
594
|
+
`System "${request.systemPath}" does not declare interface "${request.interfaceKey}".`,
|
|
595
|
+
{ path: readinessMarkerPath(request) }
|
|
596
|
+
)
|
|
597
|
+
return { ready: false, systemPath: request.systemPath, interfaceKey: request.interfaceKey, scopedResourceIds, issues }
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (systemInterface.lifecycle !== 'active') {
|
|
601
|
+
addReadinessIssue(
|
|
602
|
+
issues,
|
|
603
|
+
'SYSTEM_INTERFACE_DISABLED',
|
|
604
|
+
'inactive-interface',
|
|
605
|
+
`System Interface "${formatInterfaceIdentity(request.systemPath, request.interfaceKey)}" lifecycle is "${systemInterface.lifecycle}".`,
|
|
606
|
+
{ path: `${readinessMarkerPath(request)}.lifecycle` }
|
|
607
|
+
)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const supportedProfiles = [
|
|
611
|
+
LEAD_GEN_API_INTERFACE.readinessProfile,
|
|
612
|
+
CRM_API_INTERFACE.readinessProfile,
|
|
613
|
+
LEAD_GEN_CRM_HANDOFF_INTERFACE.readinessProfile
|
|
614
|
+
] as const
|
|
615
|
+
const supportedProfile =
|
|
616
|
+
readinessProfile !== undefined && supportedProfiles.some((profile) => profile === readinessProfile)
|
|
617
|
+
|
|
618
|
+
if (!supportedProfile) {
|
|
619
|
+
addReadinessIssue(
|
|
620
|
+
issues,
|
|
621
|
+
'SYSTEM_INTERFACE_INVALID',
|
|
622
|
+
'unknown-readiness-profile',
|
|
623
|
+
`System Interface "${formatInterfaceIdentity(request.systemPath, request.interfaceKey)}" references unknown readiness profile "${readinessProfile}".`,
|
|
624
|
+
{ path: `${readinessMarkerPath(request)}.readinessProfile`, ref: readinessProfile }
|
|
625
|
+
)
|
|
626
|
+
return {
|
|
627
|
+
ready: false,
|
|
628
|
+
systemPath: request.systemPath,
|
|
629
|
+
interfaceKey: request.interfaceKey,
|
|
630
|
+
readinessProfile,
|
|
631
|
+
scopedResourceIds,
|
|
632
|
+
issues
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (scopedResourceIds.length === 0) {
|
|
637
|
+
addReadinessIssue(
|
|
638
|
+
issues,
|
|
639
|
+
'SYSTEM_INTERFACE_NOT_READY',
|
|
640
|
+
'missing-scoped-resources',
|
|
641
|
+
`System Interface "${formatInterfaceIdentity(request.systemPath, request.interfaceKey)}" must scope at least one active resource.`,
|
|
642
|
+
{ path: `${readinessMarkerPath(request)}.resourceIds` }
|
|
643
|
+
)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const checkedReadinessProfile = readinessProfile
|
|
647
|
+
if (checkedReadinessProfile === undefined) {
|
|
648
|
+
throw new Error('Supported readiness profile unexpectedly resolved to undefined')
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const resources = getActiveScopedResources(model, scopedResourceIds, issues, request)
|
|
652
|
+
const index = tryCompileBusinessOntology(model, checkedReadinessProfile, issues)
|
|
653
|
+
|
|
654
|
+
if (index !== undefined) {
|
|
655
|
+
if (checkedReadinessProfile === LEAD_GEN_API_INTERFACE.readinessProfile) {
|
|
656
|
+
requireLeadGenInterfaceReadiness(issues, index, resources, request)
|
|
657
|
+
} else if (checkedReadinessProfile === CRM_API_INTERFACE.readinessProfile) {
|
|
658
|
+
requireCrmInterfaceReadiness(issues, index, resources, request)
|
|
659
|
+
} else if (checkedReadinessProfile === LEAD_GEN_CRM_HANDOFF_INTERFACE.readinessProfile) {
|
|
660
|
+
requireLeadGenInterfaceReadiness(issues, index, resources, request)
|
|
661
|
+
requireCrmInterfaceReadiness(issues, index, resources, { ...request, allowForeignOwner: true })
|
|
662
|
+
requireHandoffBridgeReadiness(issues, model, request)
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return {
|
|
667
|
+
ready: issues.length === 0,
|
|
668
|
+
systemPath: request.systemPath,
|
|
669
|
+
interfaceKey: request.interfaceKey,
|
|
670
|
+
readinessProfile,
|
|
671
|
+
scopedResourceIds,
|
|
672
|
+
issues
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function requireObjectType(index: BusinessOntologyValidationIndexBase, objectId: OntologyId, contractId: string): void {
|
|
677
|
+
if (index.ontology.objectTypes[objectId] === undefined) {
|
|
678
|
+
throw new Error(`${contractId} is missing required object type: ${objectId}`)
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function requireCatalog(
|
|
683
|
+
index: BusinessOntologyValidationIndexBase,
|
|
684
|
+
catalogId: OntologyId,
|
|
685
|
+
contractId: string
|
|
686
|
+
): OntologyCatalogType {
|
|
687
|
+
const catalog = index.ontology.catalogTypes[catalogId]
|
|
688
|
+
if (catalog === undefined) {
|
|
689
|
+
throw new Error(`${contractId} is missing required catalog: ${catalogId}`)
|
|
690
|
+
}
|
|
691
|
+
return catalog
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function requireCatalogEntries(catalog: OntologyCatalogType, contractId: string): void {
|
|
695
|
+
if (Object.keys(getCatalogEntries(catalog)).length === 0) {
|
|
696
|
+
throw new Error(`${contractId} requires catalog entries for ${catalog.id}`)
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function findLeadGenTemplateStepCatalog(index: BusinessOntologyValidationIndexBase): OntologyCatalogType | undefined {
|
|
701
|
+
return Object.values(index.ontology.catalogTypes).find(
|
|
702
|
+
(catalog) =>
|
|
703
|
+
catalog.ownerSystemId === 'sales.lead-gen' &&
|
|
704
|
+
catalog.kind === 'template-step' &&
|
|
705
|
+
catalog.appliesTo === LEAD_GEN_LIST_OBJECT_ONTOLOGY_ID
|
|
706
|
+
)
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function requireLeadGenApiReadiness(index: BusinessOntologyValidationIndexBase): OntologyCatalogType {
|
|
710
|
+
requireObjectType(index, LEAD_GEN_LIST_OBJECT_ONTOLOGY_ID, LEAD_GEN_API_READINESS_LABEL)
|
|
711
|
+
requireObjectType(index, LEAD_GEN_COMPANY_OBJECT_ONTOLOGY_ID, LEAD_GEN_API_READINESS_LABEL)
|
|
712
|
+
requireObjectType(index, LEAD_GEN_CONTACT_OBJECT_ONTOLOGY_ID, LEAD_GEN_API_READINESS_LABEL)
|
|
713
|
+
|
|
714
|
+
requireCatalogEntries(
|
|
715
|
+
requireCatalog(index, LEAD_GEN_BUILD_TEMPLATE_CATALOG_ONTOLOGY_ID, LEAD_GEN_API_READINESS_LABEL),
|
|
716
|
+
LEAD_GEN_API_READINESS_LABEL
|
|
717
|
+
)
|
|
718
|
+
requireCatalogEntries(
|
|
719
|
+
requireCatalog(index, LEAD_GEN_COMPANY_STAGE_CATALOG_ONTOLOGY_ID, LEAD_GEN_API_READINESS_LABEL),
|
|
720
|
+
LEAD_GEN_API_READINESS_LABEL
|
|
721
|
+
)
|
|
722
|
+
requireCatalogEntries(
|
|
723
|
+
requireCatalog(index, LEAD_GEN_CONTACT_STAGE_CATALOG_ONTOLOGY_ID, LEAD_GEN_API_READINESS_LABEL),
|
|
724
|
+
LEAD_GEN_API_READINESS_LABEL
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
const templateStepCatalog = findLeadGenTemplateStepCatalog(index)
|
|
728
|
+
if (templateStepCatalog === undefined) {
|
|
729
|
+
throw new Error(`${LEAD_GEN_API_READINESS_LABEL} is missing a lead-gen template-step catalog`)
|
|
730
|
+
}
|
|
731
|
+
requireCatalogEntries(templateStepCatalog, LEAD_GEN_API_READINESS_LABEL)
|
|
732
|
+
|
|
733
|
+
const leadGenStageCatalog = requireCatalog(index, LEAD_GEN_STAGE_CATALOG_ONTOLOGY_ID, LEAD_GEN_API_READINESS_LABEL)
|
|
734
|
+
requireCatalogEntries(leadGenStageCatalog, LEAD_GEN_API_READINESS_LABEL)
|
|
735
|
+
return leadGenStageCatalog
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function requireCrmApiReadiness(index: BusinessOntologyValidationIndexBase): OntologyCatalogType {
|
|
739
|
+
const crmPipelineCatalog = requireCatalog(index, CRM_PIPELINE_CATALOG_ONTOLOGY_ID, CRM_API_READINESS_LABEL)
|
|
740
|
+
requireCatalogEntries(crmPipelineCatalog, CRM_API_READINESS_LABEL)
|
|
741
|
+
return crmPipelineCatalog
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
export function compileLeadGenApiOntologyValidationIndex(model: OrganizationModel): LeadGenApiOntologyValidationIndex {
|
|
745
|
+
throwIfInterfaceNotReady(computeInterfaceReadiness(model, LEAD_GEN_API_INTERFACE))
|
|
746
|
+
const index = compileBusinessOntology(model, LEAD_GEN_API_READINESS_LABEL)
|
|
747
|
+
return {
|
|
748
|
+
...index,
|
|
749
|
+
leadGenStageCatalog: requireLeadGenApiReadiness(index)
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
export function compileCrmApiOntologyValidationIndex(model: OrganizationModel): CrmApiOntologyValidationIndex {
|
|
754
|
+
throwIfInterfaceNotReady(computeInterfaceReadiness(model, CRM_API_INTERFACE))
|
|
755
|
+
const index = compileBusinessOntology(model, CRM_API_READINESS_LABEL)
|
|
756
|
+
return {
|
|
757
|
+
...index,
|
|
758
|
+
crmPipelineCatalog: requireCrmApiReadiness(index)
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
export function compileLeadGenCrmHandoffOntologyValidationIndex(
|
|
763
|
+
model: OrganizationModel
|
|
764
|
+
): LeadGenCrmHandoffOntologyValidationIndex {
|
|
765
|
+
const readiness = computeInterfaceReadiness(model, LEAD_GEN_CRM_HANDOFF_INTERFACE)
|
|
766
|
+
throwIfInterfaceNotReady(readiness)
|
|
767
|
+
const index = compileBusinessOntology(model, LEAD_GEN_CRM_HANDOFF_READINESS_LABEL)
|
|
768
|
+
return {
|
|
769
|
+
...index,
|
|
770
|
+
leadGenStageCatalog: requireLeadGenApiReadiness(index),
|
|
771
|
+
crmPipelineCatalog: requireCrmApiReadiness(index),
|
|
772
|
+
bridgeGrant: findSystemInterfaceGrant(model, LEAD_GEN_CRM_HANDOFF_INTERFACE, CRM_API_INTERFACE)
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Legacy mixed acquisition index.
|
|
778
|
+
*
|
|
779
|
+
* Kept for existing API consumers until Wave 3 wires explicit route/service
|
|
780
|
+
* contract requirements. New base lead-gen paths should call
|
|
781
|
+
* `compileLeadGenApiOntologyValidationIndex()`; CRM-aware paths should call the
|
|
782
|
+
* CRM or bridge helpers above.
|
|
783
|
+
*/
|
|
784
|
+
export function compileBusinessOntologyValidationIndex(model: OrganizationModel): BusinessOntologyValidationIndex {
|
|
785
|
+
throwIfInterfaceNotReady(computeInterfaceReadiness(model, LEAD_GEN_API_INTERFACE))
|
|
786
|
+
throwIfInterfaceNotReady(computeInterfaceReadiness(model, CRM_API_INTERFACE))
|
|
787
|
+
const index = compileBusinessOntology(model, 'legacy acquisition business ontology')
|
|
788
|
+
return {
|
|
789
|
+
...index,
|
|
790
|
+
leadGenStageCatalog: requireLeadGenApiReadiness(index),
|
|
791
|
+
crmPipelineCatalog: requireCrmApiReadiness(index)
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
103
795
|
function indexActionTypesByLegacyId(
|
|
104
796
|
actionTypes: Record<OntologyId, OntologyActionType>
|
|
105
797
|
): Record<string, OntologyActionType> {
|
|
@@ -158,7 +850,7 @@ function asLeadGenStageEntry(key: string, value: unknown): LeadGenStageCatalogEn
|
|
|
158
850
|
* core does not materialize an Elevasis CRM pipeline singleton.
|
|
159
851
|
*/
|
|
160
852
|
export function getCrmStatesForStage(
|
|
161
|
-
index:
|
|
853
|
+
index: CrmApiOntologyValidationIndex,
|
|
162
854
|
stageKey: string
|
|
163
855
|
): StatefulStateDefinition[] {
|
|
164
856
|
const stage = getCatalogEntries(index.crmPipelineCatalog)[stageKey]
|
|
@@ -175,7 +867,7 @@ export function getCrmStatesForStage(
|
|
|
175
867
|
* The lead-gen stage catalog is tenant-model-dependent (it derives from the
|
|
176
868
|
* organization model's `sales.lead-gen` stage records). Hosts that own a
|
|
177
869
|
* populated tenant model (e.g. `apps/api` with the Elevasis canonical model)
|
|
178
|
-
* MUST build their own index via `
|
|
870
|
+
* MUST build their own index via `compileLeadGenApiOntologyValidationIndex(model)`
|
|
179
871
|
* and call `createLeadGenStageValidators(index)`. The core package stays generic;
|
|
180
872
|
* it must never import a tenant model.
|
|
181
873
|
*/
|
|
@@ -188,7 +880,7 @@ export type LeadGenStageValidators = {
|
|
|
188
880
|
resolveLeadGenRecordStageKey(stageKey: string, entity: 'company' | 'contact'): string
|
|
189
881
|
}
|
|
190
882
|
|
|
191
|
-
export function createLeadGenStageValidators(index:
|
|
883
|
+
export function createLeadGenStageValidators(index: LeadGenApiOntologyValidationIndex): LeadGenStageValidators {
|
|
192
884
|
const getCatalog = (): Record<string, LeadGenStageCatalogEntry> =>
|
|
193
885
|
Object.fromEntries(
|
|
194
886
|
Object.entries(getCatalogEntries(index.leadGenStageCatalog)).map(([key, value]) => [
|