@elevasis/core 0.24.0 → 0.25.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 (50) hide show
  1. package/dist/index.d.ts +3117 -2166
  2. package/dist/index.js +574 -16
  3. package/dist/knowledge/index.d.ts +122 -7
  4. package/dist/organization-model/index.d.ts +3117 -2166
  5. package/dist/organization-model/index.js +574 -16
  6. package/dist/test-utils/index.d.ts +135 -45
  7. package/dist/test-utils/index.js +122 -14
  8. package/package.json +3 -3
  9. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +139 -101
  10. package/src/execution/engine/llm/adapters/__tests__/openrouter.integration.test.ts +10 -10
  11. package/src/execution/engine/workflow/types.ts +5 -7
  12. package/src/knowledge/__tests__/queries.test.ts +960 -546
  13. package/src/knowledge/format.ts +322 -100
  14. package/src/knowledge/index.ts +18 -5
  15. package/src/knowledge/queries.ts +1004 -239
  16. package/src/organization-model/__tests__/deprecate-helpers.test.ts +71 -0
  17. package/src/organization-model/__tests__/domains/resources.test.ts +19 -8
  18. package/src/organization-model/__tests__/domains/topology.test.ts +188 -0
  19. package/src/organization-model/__tests__/graph.test.ts +98 -7
  20. package/src/organization-model/__tests__/resolve.test.ts +9 -7
  21. package/src/organization-model/__tests__/scaffolders.test.ts +93 -0
  22. package/src/organization-model/__tests__/schema.test.ts +14 -4
  23. package/src/organization-model/defaults.ts +5 -3
  24. package/src/organization-model/domains/resources.ts +63 -20
  25. package/src/organization-model/domains/topology.ts +261 -0
  26. package/src/organization-model/graph/build.ts +63 -15
  27. package/src/organization-model/graph/schema.ts +4 -3
  28. package/src/organization-model/graph/types.ts +5 -4
  29. package/src/organization-model/helpers.ts +76 -9
  30. package/src/organization-model/icons.ts +1 -0
  31. package/src/organization-model/index.ts +7 -5
  32. package/src/organization-model/ontology.ts +2 -5
  33. package/src/organization-model/organization-model.mdx +16 -11
  34. package/src/organization-model/published.ts +51 -15
  35. package/src/organization-model/scaffolders/helpers.ts +84 -0
  36. package/src/organization-model/scaffolders/index.ts +19 -0
  37. package/src/organization-model/scaffolders/scaffoldKnowledgeNode.ts +48 -0
  38. package/src/organization-model/scaffolders/scaffoldOntologyRecord.ts +38 -0
  39. package/src/organization-model/scaffolders/scaffoldResource.ts +59 -0
  40. package/src/organization-model/scaffolders/scaffoldSystem.ts +110 -0
  41. package/src/organization-model/scaffolders/types.ts +81 -0
  42. package/src/organization-model/schema.ts +51 -11
  43. package/src/organization-model/types.ts +25 -11
  44. package/src/platform/constants/versions.ts +1 -1
  45. package/src/platform/registry/__tests__/validation.test.ts +199 -14
  46. package/src/platform/registry/resource-registry.ts +11 -11
  47. package/src/platform/registry/validation.ts +226 -34
  48. package/src/reference/_generated/contracts.md +139 -101
  49. package/src/reference/glossary.md +74 -72
  50. package/src/supabase/database.types.ts +3156 -3153
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod'
2
- import { ModelIdSchema } from './shared'
2
+ import { DescriptionSchema, LabelSchema, ModelIdSchema } from './shared'
3
3
  import { SystemPathSchema, SystemLifecycleSchema } from './systems'
4
4
  import { ActionInvocationSchema } from './actions'
5
5
  import { OntologyIdSchema } from '../ontology'
@@ -57,13 +57,35 @@ export const EventDescriptorSchema = EventEmissionDescriptorSchema.extend({
57
57
  ownerKind: z.enum(['resource', 'entity']).meta({ label: 'Owner kind' })
58
58
  })
59
59
 
60
- export const ResourceOntologyBindingSchema = z.object({
61
- implements: z.array(OntologyIdSchema).optional(),
62
- reads: z.array(OntologyIdSchema).optional(),
63
- writes: z.array(OntologyIdSchema).optional(),
64
- usesCatalogs: z.array(OntologyIdSchema).optional(),
65
- emits: z.array(OntologyIdSchema).optional()
66
- })
60
+ export const ResourceOntologyBindingSchema = z
61
+ .object({
62
+ actions: z.array(OntologyIdSchema).optional(),
63
+ primaryAction: OntologyIdSchema.optional(),
64
+ reads: z.array(OntologyIdSchema).optional(),
65
+ writes: z.array(OntologyIdSchema).optional(),
66
+ usesCatalogs: z.array(OntologyIdSchema).optional(),
67
+ emits: z.array(OntologyIdSchema).optional()
68
+ })
69
+ .superRefine((binding, ctx) => {
70
+ if (binding.primaryAction === undefined) return
71
+ if (binding.actions?.includes(binding.primaryAction)) return
72
+
73
+ ctx.addIssue({
74
+ code: z.ZodIssueCode.custom,
75
+ path: ['primaryAction'],
76
+ message: 'Resource ontology primaryAction must be included in actions'
77
+ })
78
+ })
79
+
80
+ type OntologyRefInput = string | { id: string }
81
+ type ResourceOntologyBindingInput = {
82
+ actions?: OntologyRefInput[]
83
+ primaryAction?: OntologyRefInput
84
+ reads?: OntologyRefInput[]
85
+ writes?: OntologyRefInput[]
86
+ usesCatalogs?: OntologyRefInput[]
87
+ emits?: OntologyRefInput[]
88
+ }
67
89
 
68
90
  export const CodeReferenceSchema = z.object({
69
91
  path: z
@@ -83,7 +105,11 @@ const ResourceEntryBaseSchema = z.object({
83
105
  /** Domain-map iteration order. Convention: multiples of 10 (10, 20, 30, ...) to allow easy insertion. */
84
106
  order: z.number().default(0),
85
107
  /** Required single System membership — value is a dot-separated system path (e.g. "sales.lead-gen"). */
86
- systemPath: SystemPathSchema.meta({ ref: 'system' }),
108
+ systemPath: SystemPathSchema.meta({ ref: 'system' }),
109
+ /** Executable display title owned by the OM Resource descriptor. */
110
+ title: LabelSchema.optional(),
111
+ /** Executable display description owned by the OM Resource descriptor. */
112
+ description: DescriptionSchema.optional(),
87
113
  /** Optional role responsible for maintaining this resource. */
88
114
  ownerRoleId: ModelIdSchema.meta({ ref: 'role' }).optional(),
89
115
  status: ResourceGovernanceStatusSchema,
@@ -97,12 +123,10 @@ const ResourceEntryBaseSchema = z.object({
97
123
  codeRefs: z.array(CodeReferenceSchema).default([])
98
124
  })
99
125
 
100
- export const WorkflowResourceEntrySchema = ResourceEntryBaseSchema.extend({
101
- kind: z.literal('workflow'),
102
- /** Mirrors WorkflowConfig.actionKey when the runtime workflow has one. */
103
- actionKey: z.string().trim().min(1).max(255).optional(),
104
- emits: z.array(EventEmissionDescriptorSchema).optional()
105
- })
126
+ export const WorkflowResourceEntrySchema = ResourceEntryBaseSchema.extend({
127
+ kind: z.literal('workflow'),
128
+ emits: z.array(EventEmissionDescriptorSchema).optional()
129
+ })
106
130
 
107
131
  export const AgentResourceEntrySchema = ResourceEntryBaseSchema.extend({
108
132
  kind: z.literal('agent'),
@@ -155,13 +179,32 @@ export function defineResource<const TResource extends ResourceEntry>(resource:
155
179
  return ResourceEntrySchema.parse(resource) as TResource
156
180
  }
157
181
 
158
- export function defineResources<const TResources extends Record<string, ResourceEntry>>(
159
- resources: TResources
160
- ): TResources {
182
+ export function defineResources<const TResources extends Record<string, ResourceEntry>>(
183
+ resources: TResources
184
+ ): TResources {
161
185
  return Object.fromEntries(
162
186
  Object.entries(resources).map(([key, resource]) => [key, ResourceEntrySchema.parse(resource)])
163
- ) as TResources
164
- }
187
+ ) as TResources
188
+ }
189
+
190
+ function ontologyIdFrom(input: OntologyRefInput): string {
191
+ return typeof input === 'string' ? input : input.id
192
+ }
193
+
194
+ function ontologyIdArrayFrom(input: OntologyRefInput[] | undefined): string[] | undefined {
195
+ return input?.map(ontologyIdFrom)
196
+ }
197
+
198
+ export function defineResourceOntology(input: ResourceOntologyBindingInput): ResourceOntologyBinding {
199
+ return ResourceOntologyBindingSchema.parse({
200
+ actions: ontologyIdArrayFrom(input.actions),
201
+ primaryAction: input.primaryAction === undefined ? undefined : ontologyIdFrom(input.primaryAction),
202
+ reads: ontologyIdArrayFrom(input.reads),
203
+ writes: ontologyIdArrayFrom(input.writes),
204
+ usesCatalogs: ontologyIdArrayFrom(input.usesCatalogs),
205
+ emits: ontologyIdArrayFrom(input.emits)
206
+ })
207
+ }
165
208
 
166
209
  export type ResourceId = z.infer<typeof ResourceIdSchema>
167
210
  export type ResourceKind = z.infer<typeof ResourceKindSchema>
@@ -0,0 +1,261 @@
1
+ import { z } from 'zod'
2
+ import { OntologyIdSchema, parseOntologyId } from '../ontology'
3
+ import { JsonValueSchema, SystemPathSchema } from './systems'
4
+ import { ModelIdSchema } from './shared'
5
+ import { ResourceIdSchema, type ResourceEntry } from './resources'
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Topology domain
9
+ // ---------------------------------------------------------------------------
10
+ //
11
+ // Topology captures durable operational wiring. It stays separate from
12
+ // ontology, which describes semantic meaning.
13
+
14
+ const SecretLikeMetadataKeySchema = /(?:secret|password|passwd|token|api[-_]?key|credential|private[-_]?key)/i
15
+ const SecretLikeMetadataValueSchema =
16
+ /(?:sk-[A-Za-z0-9_-]{12,}|pk_live_[A-Za-z0-9_-]{12,}|eyJ[A-Za-z0-9_-]{20,}|-----BEGIN (?:RSA |OPENSSH |EC )?PRIVATE KEY-----)/
17
+
18
+ export const OmTopologyNodeKindSchema = z.enum([
19
+ 'system',
20
+ 'resource',
21
+ 'ontology',
22
+ 'policy',
23
+ 'role',
24
+ 'trigger',
25
+ 'humanCheckpoint',
26
+ 'externalResource'
27
+ ])
28
+
29
+ export const OmTopologyRelationshipKindSchema = z.enum(['triggers', 'uses', 'approval'])
30
+
31
+ export const OmTopologyNodeRefSchema = z.discriminatedUnion('kind', [
32
+ z.object({ kind: z.literal('system'), id: ModelIdSchema }),
33
+ z.object({ kind: z.literal('resource'), id: ResourceIdSchema }),
34
+ z.object({ kind: z.literal('ontology'), id: OntologyIdSchema }),
35
+ z.object({ kind: z.literal('policy'), id: ModelIdSchema }),
36
+ z.object({ kind: z.literal('role'), id: ModelIdSchema }),
37
+ z.object({ kind: z.literal('trigger'), id: ResourceIdSchema }),
38
+ z.object({ kind: z.literal('humanCheckpoint'), id: ResourceIdSchema }),
39
+ z.object({ kind: z.literal('externalResource'), id: ResourceIdSchema })
40
+ ])
41
+
42
+ export const OmTopologyMetadataSchema = z
43
+ .record(z.string().trim().min(1).max(120), JsonValueSchema)
44
+ .superRefine((metadata, ctx) => {
45
+ function visit(value: unknown, path: Array<string | number>): void {
46
+ if (typeof value === 'string' && SecretLikeMetadataValueSchema.test(value)) {
47
+ ctx.addIssue({
48
+ code: z.ZodIssueCode.custom,
49
+ path,
50
+ message: 'Topology metadata must not contain secret-like values'
51
+ })
52
+ return
53
+ }
54
+
55
+ if (Array.isArray(value)) {
56
+ value.forEach((entry, index) => visit(entry, [...path, index]))
57
+ return
58
+ }
59
+
60
+ if (typeof value !== 'object' || value === null) return
61
+
62
+ Object.entries(value).forEach(([key, entry]) => {
63
+ if (SecretLikeMetadataKeySchema.test(key)) {
64
+ ctx.addIssue({
65
+ code: z.ZodIssueCode.custom,
66
+ path: [...path, key],
67
+ message: `Topology metadata key "${key}" looks secret-like`
68
+ })
69
+ }
70
+ visit(entry, [...path, key])
71
+ })
72
+ }
73
+
74
+ visit(metadata, [])
75
+ })
76
+
77
+ export const OmTopologyRelationshipSchema = z.object({
78
+ from: OmTopologyNodeRefSchema,
79
+ kind: OmTopologyRelationshipKindSchema,
80
+ to: OmTopologyNodeRefSchema,
81
+ systemPath: SystemPathSchema.optional(),
82
+ required: z.boolean().optional(),
83
+ metadata: OmTopologyMetadataSchema.optional()
84
+ })
85
+
86
+ export const OmTopologyDomainSchema = z
87
+ .object({
88
+ version: z.literal(1).default(1),
89
+ relationships: z.record(z.string().trim().min(1).max(255), OmTopologyRelationshipSchema).default({})
90
+ })
91
+ .default({ version: 1, relationships: {} })
92
+
93
+ export const DEFAULT_ORGANIZATION_MODEL_TOPOLOGY: z.infer<typeof OmTopologyDomainSchema> = {
94
+ version: 1,
95
+ relationships: {}
96
+ }
97
+
98
+ export type OmTopologyNodeKind = z.infer<typeof OmTopologyNodeKindSchema>
99
+ export type OmTopologyRelationshipKind = z.infer<typeof OmTopologyRelationshipKindSchema>
100
+ export type OmTopologyNodeRef = z.infer<typeof OmTopologyNodeRefSchema>
101
+ export type OmTopologyMetadata = z.infer<typeof OmTopologyMetadataSchema>
102
+ export type OmTopologyRelationship = z.infer<typeof OmTopologyRelationshipSchema>
103
+ export type OmTopologyDomain = z.infer<typeof OmTopologyDomainSchema>
104
+
105
+ type TypedIdObject = { id: string }
106
+ type TopologyRefFactoryInput = string | TypedIdObject
107
+ type TopologyRelationshipInput = Omit<OmTopologyRelationship, 'from' | 'to'> & {
108
+ from: OmTopologyNodeInput
109
+ to: OmTopologyNodeInput
110
+ }
111
+ type TopologyRelationshipOptions = Omit<Partial<OmTopologyRelationship>, 'from' | 'kind' | 'to'>
112
+ export type OmTopologyNodeInput = OmTopologyNodeRef | ResourceEntry
113
+
114
+ function idFrom(input: TopologyRefFactoryInput): string {
115
+ return typeof input === 'string' ? input : input.id
116
+ }
117
+
118
+ function parseRef(kind: OmTopologyNodeKind, id: string): OmTopologyNodeRef {
119
+ return OmTopologyNodeRefSchema.parse({ kind, id })
120
+ }
121
+
122
+ function isNodeRef(input: unknown): input is OmTopologyNodeRef {
123
+ return OmTopologyNodeRefSchema.safeParse(input).success
124
+ }
125
+
126
+ function isResourceEntry(input: unknown): input is ResourceEntry {
127
+ if (typeof input !== 'object' || input === null) return false
128
+ const candidate = input as Partial<ResourceEntry>
129
+ return (
130
+ typeof candidate.id === 'string' &&
131
+ typeof candidate.systemPath === 'string' &&
132
+ typeof candidate.status === 'string' &&
133
+ ['workflow', 'agent', 'integration', 'script'].includes(String(candidate.kind))
134
+ )
135
+ }
136
+
137
+ export const topologyRef = {
138
+ system: (system: TopologyRefFactoryInput) => parseRef('system', idFrom(system)),
139
+ resource: (resource: TopologyRefFactoryInput) => parseRef('resource', idFrom(resource)),
140
+ ontology: (record: TopologyRefFactoryInput) => parseRef('ontology', idFrom(record)),
141
+ policy: (policy: TopologyRefFactoryInput) => parseRef('policy', idFrom(policy)),
142
+ role: (role: TopologyRefFactoryInput) => parseRef('role', idFrom(role)),
143
+ trigger: (trigger: TopologyRefFactoryInput) => parseRef('trigger', idFrom(trigger)),
144
+ humanCheckpoint: (checkpoint: TopologyRefFactoryInput) => parseRef('humanCheckpoint', idFrom(checkpoint)),
145
+ externalResource: (externalResource: TopologyRefFactoryInput) => parseRef('externalResource', idFrom(externalResource))
146
+ } as const
147
+
148
+ export const topologyRelationship = {
149
+ triggers: (from: OmTopologyNodeInput, to: OmTopologyNodeInput, options: TopologyRelationshipOptions = {}) =>
150
+ defineTopologyRelationship({
151
+ ...options,
152
+ from,
153
+ kind: 'triggers',
154
+ to
155
+ }),
156
+ uses: (from: OmTopologyNodeInput, to: OmTopologyNodeInput, options: TopologyRelationshipOptions = {}) =>
157
+ defineTopologyRelationship({
158
+ ...options,
159
+ from,
160
+ kind: 'uses',
161
+ to
162
+ }),
163
+ approval: (from: OmTopologyNodeInput, to: OmTopologyNodeInput, options: TopologyRelationshipOptions = {}) =>
164
+ defineTopologyRelationship({
165
+ ...options,
166
+ from,
167
+ kind: 'approval',
168
+ to
169
+ }),
170
+ usesIntegration: (
171
+ from: OmTopologyNodeInput,
172
+ integration: OmTopologyNodeInput,
173
+ options: TopologyRelationshipOptions = {}
174
+ ) =>
175
+ defineTopologyRelationship({
176
+ required: true,
177
+ ...options,
178
+ from,
179
+ kind: 'uses',
180
+ to: integration
181
+ }),
182
+ requestsApproval: (
183
+ from: OmTopologyNodeInput,
184
+ checkpoint: TopologyRefFactoryInput,
185
+ options: TopologyRelationshipOptions = {}
186
+ ) =>
187
+ defineTopologyRelationship({
188
+ required: true,
189
+ ...options,
190
+ from,
191
+ kind: 'approval',
192
+ to: topologyRef.humanCheckpoint(checkpoint)
193
+ }),
194
+ checkpointRoutesTo: (
195
+ checkpoint: TopologyRefFactoryInput,
196
+ to: OmTopologyNodeInput,
197
+ options: TopologyRelationshipOptions = {}
198
+ ) =>
199
+ defineTopologyRelationship({
200
+ required: true,
201
+ ...options,
202
+ from: topologyRef.humanCheckpoint(checkpoint),
203
+ kind: 'triggers',
204
+ to
205
+ })
206
+ } as const
207
+
208
+ export function compileTopologyNodeRef(input: OmTopologyNodeInput): OmTopologyNodeRef {
209
+ if (isNodeRef(input)) return input
210
+ if (isResourceEntry(input)) return topologyRef.resource(input)
211
+
212
+ throw new Error('Topology node refs must be typed node objects or serializable { kind, id } refs')
213
+ }
214
+
215
+ export function parseTopologyNodeRef(input: string | OmTopologyNodeRef): OmTopologyNodeRef {
216
+ if (typeof input !== 'string') return OmTopologyNodeRefSchema.parse(input)
217
+
218
+ const separatorIndex = input.indexOf(':')
219
+ if (separatorIndex === -1) {
220
+ throw new Error(`Topology node ref "${input}" must use <kind>:<id>`)
221
+ }
222
+
223
+ const kind = input.slice(0, separatorIndex)
224
+ const id = input.slice(separatorIndex + 1)
225
+ if (!OmTopologyNodeKindSchema.safeParse(kind).success) {
226
+ throw new Error(`Topology node ref "${input}" has unsupported kind "${kind}"`)
227
+ }
228
+
229
+ return OmTopologyNodeRefSchema.parse({ kind, id })
230
+ }
231
+
232
+ export function defineTopologyRelationship(input: TopologyRelationshipInput): OmTopologyRelationship {
233
+ return OmTopologyRelationshipSchema.parse({
234
+ ...input,
235
+ from: compileTopologyNodeRef(input.from),
236
+ to: compileTopologyNodeRef(input.to)
237
+ })
238
+ }
239
+
240
+ export function defineTopology(
241
+ relationships: Record<string, TopologyRelationshipInput> | TopologyRelationshipInput[]
242
+ ): OmTopologyDomain {
243
+ const entries = Array.isArray(relationships)
244
+ ? relationships.map((relationship, index) => [`relationship-${index + 1}`, relationship] as const)
245
+ : Object.entries(relationships)
246
+
247
+ return OmTopologyDomainSchema.parse({
248
+ version: 1,
249
+ relationships: Object.fromEntries(entries.map(([key, relationship]) => [key, defineTopologyRelationship(relationship)]))
250
+ })
251
+ }
252
+
253
+ export function isOntologyTopologyRef(value: unknown): value is Extract<OmTopologyNodeRef, { kind: 'ontology' }> {
254
+ if (!isNodeRef(value) || value.kind !== 'ontology') return false
255
+ try {
256
+ parseOntologyId(value.id)
257
+ return true
258
+ } catch {
259
+ return false
260
+ }
261
+ }
@@ -8,10 +8,11 @@ import type {
8
8
  OrganizationGraphNode,
9
9
  OrganizationGraphNodeKind
10
10
  } from './types'
11
- import type { ActionInvocation } from '../domains/actions'
12
- import type { Entity } from '../domains/entities'
13
- import type { ResourceEntry } from '../domains/resources'
14
- import type { EventDescriptor, OrganizationModelSidebarNode } from '../types'
11
+ import type { ActionInvocation } from '../domains/actions'
12
+ import type { Entity } from '../domains/entities'
13
+ import type { ResourceEntry } from '../domains/resources'
14
+ import type { OmTopologyNodeRef } from '../domains/topology'
15
+ import type { EventDescriptor, OrganizationModelSidebarNode } from '../types'
15
16
  import {
16
17
  getAllPipelines,
17
18
  getAllBuildTemplates,
@@ -286,8 +287,40 @@ export function buildOrganizationGraph(input: BuildOrganizationGraphInput): Orga
286
287
  systemPathByRef.set(path, path)
287
288
  systemPathByRef.set(system.id, path)
288
289
  }
289
- const validSystemRefs = new Set(systemPathByRef.keys())
290
- const systemNodeId = (systemRef: string) => nodeId('system', systemPathByRef.get(systemRef) ?? systemRef)
290
+ const validSystemRefs = new Set(systemPathByRef.keys())
291
+ const systemNodeId = (systemRef: string) => nodeId('system', systemPathByRef.get(systemRef) ?? systemRef)
292
+
293
+ function topologyNodeId(ref: OmTopologyNodeRef): string {
294
+ if (ref.kind === 'system') return systemNodeId(ref.id)
295
+ if (ref.kind === 'resource') return nodeId('resource', ref.id)
296
+ if (ref.kind === 'ontology') return ontologyGraphNodeId(ref.id)
297
+ if (ref.kind === 'policy') return nodeId('policy', ref.id)
298
+ if (ref.kind === 'role') return nodeId('role', ref.id)
299
+ return nodeId('resource', ref.id)
300
+ }
301
+
302
+ function ensureTopologyNode(ref: OmTopologyNodeRef): string {
303
+ const id = topologyNodeId(ref)
304
+ if (nodeIds.has(id)) return id
305
+
306
+ if (ref.kind === 'resource') {
307
+ ensureResourceNode(nodes, nodeIds, resourceNodesById, ref.id)
308
+ return id
309
+ }
310
+
311
+ if (ref.kind === 'trigger' || ref.kind === 'humanCheckpoint' || ref.kind === 'externalResource') {
312
+ pushUniqueNode(nodes, nodeIds, {
313
+ id,
314
+ kind: 'resource',
315
+ label: ref.id,
316
+ sourceId: ref.id,
317
+ resourceType:
318
+ ref.kind === 'trigger' ? 'trigger' : ref.kind === 'humanCheckpoint' ? 'human_checkpoint' : 'external'
319
+ })
320
+ }
321
+
322
+ return id
323
+ }
291
324
 
292
325
  for (const { path, system } of systemsWithPaths.sort((a, b) => a.path.localeCompare(b.path))) {
293
326
  const id = nodeId('system', path)
@@ -649,7 +682,7 @@ export function buildOrganizationGraph(input: BuildOrganizationGraphInput): Orga
649
682
  })
650
683
  }
651
684
 
652
- pushOntologyBindingEdges(edges, edgeIds, resourceNode.id, 'implements', resource.ontology?.implements)
685
+ pushOntologyBindingEdges(edges, edgeIds, resourceNode.id, 'actions', resource.ontology?.actions)
653
686
  pushOntologyBindingEdges(edges, edgeIds, resourceNode.id, 'reads', resource.ontology?.reads)
654
687
  pushOntologyBindingEdges(edges, edgeIds, resourceNode.id, 'writes', resource.ontology?.writes)
655
688
  pushOntologyBindingEdges(edges, edgeIds, resourceNode.id, 'uses_catalog', resource.ontology?.usesCatalogs)
@@ -698,9 +731,9 @@ export function buildOrganizationGraph(input: BuildOrganizationGraphInput): Orga
698
731
  }
699
732
  }
700
733
 
701
- for (const policy of Object.values(organizationModel.policies).sort(
702
- (a, b) => a.order - b.order || a.id.localeCompare(b.id)
703
- )) {
734
+ for (const policy of Object.values(organizationModel.policies).sort(
735
+ (a, b) => a.order - b.order || a.id.localeCompare(b.id)
736
+ )) {
704
737
  const id = nodeId('policy', policy.id)
705
738
  pushUniqueNode(nodes, nodeIds, {
706
739
  id,
@@ -787,11 +820,26 @@ export function buildOrganizationGraph(input: BuildOrganizationGraphInput): Orga
787
820
  targetId: nodeId('role', effect.roleId),
788
821
  label: effect.kind
789
822
  })
790
- }
791
- }
792
- }
793
-
794
- for (const segment of Object.values(organizationModel.customers).sort(
823
+ }
824
+ }
825
+ }
826
+
827
+ for (const [relationshipId, relationship] of Object.entries(organizationModel.topology.relationships).sort(([a], [b]) =>
828
+ a.localeCompare(b)
829
+ )) {
830
+ const sourceId = ensureTopologyNode(relationship.from)
831
+ const targetId = ensureTopologyNode(relationship.to)
832
+
833
+ pushUniqueEdge(edges, edgeIds, {
834
+ id: edgeId(relationship.kind, sourceId, targetId, `topology-${relationshipId}`),
835
+ kind: relationship.kind,
836
+ sourceId,
837
+ targetId,
838
+ relationshipType: relationship.kind
839
+ })
840
+ }
841
+
842
+ for (const segment of Object.values(organizationModel.customers).sort(
795
843
  (a, b) => a.order - b.order || a.id.localeCompare(b.id)
796
844
  )) {
797
845
  const id = nodeId('customer-segment', segment.id)
@@ -33,10 +33,11 @@ export const OrganizationGraphEdgeKindSchema = z.enum([
33
33
  'affects',
34
34
  'emits',
35
35
  'originates_from',
36
- 'triggers',
37
- 'applies_to',
36
+ 'triggers',
37
+ 'approval',
38
+ 'applies_to',
38
39
  'effects',
39
- 'implements',
40
+ 'actions',
40
41
  'reads',
41
42
  'writes',
42
43
  'uses_catalog'
@@ -32,11 +32,12 @@ export type OrganizationGraphEdgeKind =
32
32
  | 'links'
33
33
  | 'affects'
34
34
  | 'emits'
35
- | 'originates_from'
36
- | 'triggers'
37
- | 'applies_to'
35
+ | 'originates_from'
36
+ | 'triggers'
37
+ | 'approval'
38
+ | 'applies_to'
38
39
  | 'effects'
39
- | 'implements'
40
+ | 'actions'
40
41
  | 'reads'
41
42
  | 'writes'
42
43
  | 'uses_catalog'
@@ -2,6 +2,7 @@ import type { OrganizationModel, OrganizationModelSystemEntry } from './types'
2
2
  import type { ContentNode } from './content-kinds/types'
3
3
  import type { JsonValue } from './domains/systems'
4
4
  import type { ResourceEntry } from './domains/resources'
5
+ import type { OmTopologyRelationship } from './domains/topology'
5
6
 
6
7
  // W1A has landed: ContentNode and SystemEntry (with content + subsystems) are now
7
8
  // defined in their canonical locations.
@@ -314,14 +315,80 @@ export function resolveSystemConfig(model: OrganizationModel, path: string): Rec
314
315
  * getResourcesForSystem(model, 'sales', { includeDescendants: true })
315
316
  * // → resources where systemPath === 'sales' OR systemPath starts with 'sales.'
316
317
  */
317
- export function getResourcesForSystem(
318
- model: OrganizationModel,
319
- systemPath: string,
320
- options: { includeDescendants?: boolean } = {}
321
- ): ResourceEntry[] {
318
+ export function getResourcesForSystem(
319
+ model: OrganizationModel,
320
+ systemPath: string,
321
+ options: { includeDescendants?: boolean } = {}
322
+ ): ResourceEntry[] {
322
323
  const { includeDescendants = false } = options
323
324
  const prefix = systemPath + '.'
324
- return Object.values(model.resources ?? {}).filter(
325
- (r) => r.systemPath === systemPath || (includeDescendants && r.systemPath.startsWith(prefix))
326
- )
327
- }
325
+ return Object.values(model.resources ?? {}).filter(
326
+ (r) => r.systemPath === systemPath || (includeDescendants && r.systemPath.startsWith(prefix))
327
+ )
328
+ }
329
+
330
+ export interface SystemTopologyEdge {
331
+ id: string
332
+ relationship: OmTopologyRelationship
333
+ }
334
+
335
+ export interface SystemDeprecationDependents {
336
+ resources: ResourceEntry[]
337
+ topologyEdges: SystemTopologyEdge[]
338
+ }
339
+
340
+ function systemPathIsInScope(candidate: string, systemPath: string, includeDescendants: boolean): boolean {
341
+ return candidate === systemPath || (includeDescendants && candidate.startsWith(`${systemPath}.`))
342
+ }
343
+
344
+ function topologyRelationshipTouchesSystem(
345
+ relationship: OmTopologyRelationship,
346
+ systemPath: string,
347
+ resourceIds: Set<string>,
348
+ includeDescendants: boolean
349
+ ): boolean {
350
+ if (
351
+ relationship.systemPath !== undefined &&
352
+ systemPathIsInScope(relationship.systemPath, systemPath, includeDescendants)
353
+ ) {
354
+ return true
355
+ }
356
+
357
+ for (const ref of [relationship.from, relationship.to]) {
358
+ if (ref.kind === 'system' && systemPathIsInScope(ref.id, systemPath, includeDescendants)) return true
359
+ if (ref.kind === 'resource' && resourceIds.has(ref.id)) return true
360
+ }
361
+
362
+ return false
363
+ }
364
+
365
+ export function getTopologyEdgesForSystem(
366
+ model: OrganizationModel,
367
+ systemPath: string,
368
+ options: { includeDescendants?: boolean; resourceIds?: Iterable<string> } = {}
369
+ ): SystemTopologyEdge[] {
370
+ const { includeDescendants = false } = options
371
+ const resourceIds = new Set(
372
+ options.resourceIds ?? getResourcesForSystem(model, systemPath, { includeDescendants }).map((r) => r.id)
373
+ )
374
+
375
+ return Object.entries(model.topology?.relationships ?? {})
376
+ .filter(([, relationship]) =>
377
+ topologyRelationshipTouchesSystem(relationship, systemPath, resourceIds, includeDescendants)
378
+ )
379
+ .map(([id, relationship]) => ({ id, relationship }))
380
+ }
381
+
382
+ export function getSystemDeprecationDependents(
383
+ model: OrganizationModel,
384
+ systemPath: string,
385
+ options: { includeDescendants?: boolean } = {}
386
+ ): SystemDeprecationDependents {
387
+ const resources = getResourcesForSystem(model, systemPath, options).filter((resource) => resource.status === 'active')
388
+ const resourceIds = new Set(resources.map((resource) => resource.id))
389
+
390
+ return {
391
+ resources,
392
+ topologyEdges: getTopologyEdgesForSystem(model, systemPath, { ...options, resourceIds })
393
+ }
394
+ }
@@ -8,6 +8,7 @@ export const ORGANIZATION_MODEL_ICON_TOKENS = [
8
8
  'crm',
9
9
  'lead-gen',
10
10
  'projects',
11
+ 'clients',
11
12
  'operations',
12
13
  'monitoring',
13
14
  'knowledge',
@@ -13,8 +13,9 @@ export * from './resolve'
13
13
  export * from './foundation'
14
14
  export * from './surface-projection'
15
15
  export * from './helpers'
16
- export * from './migration-helpers'
17
- export * from './graph'
16
+ export * from './migration-helpers'
17
+ export * from './scaffolders'
18
+ export * from './graph'
18
19
  export * from './catalogs/lead-gen'
19
20
  export * from './domains/branding'
20
21
  // Phase 4: OrganizationModelSalesSchema, OrganizationModelProspectingSchema,
@@ -73,9 +74,10 @@ export type {
73
74
  ActionRegistry
74
75
  } from './domains/prospecting'
75
76
  export { ProjectsDomainStateSchema } from './domains/projects'
76
- export * from './domains/systems'
77
- export * from './domains/resources'
78
- export {
77
+ export * from './domains/systems'
78
+ export * from './domains/resources'
79
+ export * from './domains/topology'
80
+ export {
79
81
  DEFAULT_ORGANIZATION_MODEL_NAVIGATION,
80
82
  getSortedSidebarEntries,
81
83
  NavigationGroupSchema,
@@ -496,7 +496,6 @@ function addLegacyEntityProjections(
496
496
  }
497
497
  }
498
498
  : {}),
499
- legacyEntityId: entity.id,
500
499
  ...(entity.rowSchema !== undefined ? { rowSchema: entity.rowSchema } : {}),
501
500
  ...(entity.stateCatalogId !== undefined ? { stateCatalogId: entity.stateCatalogId } : {})
502
501
  }
@@ -524,8 +523,7 @@ function addLegacyEntityProjections(
524
523
  from: legacyObjectId(entity),
525
524
  to: legacyObjectId(targetEntity),
526
525
  cardinality: link.kind,
527
- ...(link.via !== undefined ? { via: link.via } : {}),
528
- legacyEntityId: entity.id
526
+ ...(link.via !== undefined ? { via: link.via } : {})
529
527
  }
530
528
 
531
529
  addRecord(index, diagnostics, sourcesById, 'linkTypes', linkType, {
@@ -606,8 +604,7 @@ function addSystemContentProjections(
606
604
  ? { appliesTo: formatOntologyId({ scope: systemPath, kind: 'object', localId: node.data['entityId'] }) }
607
605
  : {}),
608
606
  ...(Object.keys(entries).length > 0 ? { entries } : {}),
609
- ...(node.data !== undefined ? { data: node.data } : {}),
610
- legacyContentId: `${systemPath}:${localId}`
607
+ ...(node.data !== undefined ? { data: node.data } : {})
611
608
  }
612
609
 
613
610
  addRecord(index, diagnostics, sourcesById, 'catalogTypes', catalogType, {