@elevasis/core 0.26.0 → 0.28.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 (49) hide show
  1. package/dist/index.d.ts +162 -105
  2. package/dist/index.js +280 -174
  3. package/dist/knowledge/index.d.ts +43 -43
  4. package/dist/organization-model/index.d.ts +162 -105
  5. package/dist/organization-model/index.js +280 -174
  6. package/dist/test-utils/index.d.ts +20 -20
  7. package/dist/test-utils/index.js +184 -126
  8. package/package.json +3 -3
  9. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +976 -1063
  10. package/src/business/acquisition/api-schemas.test.ts +1962 -1841
  11. package/src/business/acquisition/api-schemas.ts +1461 -1464
  12. package/src/business/acquisition/crm-next-action.test.ts +45 -25
  13. package/src/business/acquisition/crm-next-action.ts +227 -220
  14. package/src/business/acquisition/crm-priority.test.ts +41 -8
  15. package/src/business/acquisition/crm-priority.ts +365 -349
  16. package/src/business/acquisition/crm-state-actions.test.ts +208 -153
  17. package/src/business/acquisition/derive-actions.test.ts +90 -13
  18. package/src/business/acquisition/derive-actions.ts +8 -139
  19. package/src/business/acquisition/ontology-validation.ts +72 -158
  20. package/src/business/pdf/sections/investment.ts +1 -1
  21. package/src/business/pdf/sections/summary-investment.ts +1 -1
  22. package/src/execution/engine/tools/tool-maps.ts +872 -831
  23. package/src/organization-model/__tests__/cross-ref.test.ts +167 -0
  24. package/src/organization-model/__tests__/define-domain-record.test.ts +289 -0
  25. package/src/organization-model/__tests__/om-spine-doc-contract.test.ts +56 -0
  26. package/src/organization-model/__tests__/published-zero-leak.test.ts +60 -1
  27. package/src/organization-model/__tests__/resolve.test.ts +1 -1
  28. package/src/organization-model/__tests__/schema-refinements.test.ts +72 -0
  29. package/src/organization-model/cross-ref.ts +175 -0
  30. package/src/organization-model/domains/actions.ts +13 -0
  31. package/src/organization-model/domains/branding.ts +6 -6
  32. package/src/organization-model/domains/customers.ts +95 -78
  33. package/src/organization-model/domains/entities.ts +157 -144
  34. package/src/organization-model/domains/goals.ts +100 -83
  35. package/src/organization-model/domains/knowledge.ts +106 -93
  36. package/src/organization-model/domains/offerings.ts +88 -71
  37. package/src/organization-model/domains/policies.ts +115 -102
  38. package/src/organization-model/domains/roles.ts +109 -96
  39. package/src/organization-model/domains/sales.test.ts +104 -218
  40. package/src/organization-model/domains/sales.ts +212 -375
  41. package/src/organization-model/domains/statuses.ts +351 -339
  42. package/src/organization-model/domains/systems.ts +176 -164
  43. package/src/organization-model/helpers.ts +331 -306
  44. package/src/organization-model/index.ts +43 -0
  45. package/src/organization-model/published.ts +27 -2
  46. package/src/organization-model/schema-refinements.ts +667 -0
  47. package/src/organization-model/schema.ts +8 -715
  48. package/src/platform/constants/versions.ts +1 -1
  49. package/src/reference/_generated/contracts.md +1000 -1087
@@ -1,306 +1,331 @@
1
- import type { OrganizationModel, OrganizationModelSystemEntry } from './types'
2
- import type { JsonValue } from './domains/systems'
3
- import type { ResourceEntry } from './domains/resources'
4
- import type { OmTopologyRelationship } from './domains/topology'
5
-
6
- // Phase 4: removed locally-scoped inferred types for deleted compound-domain schemas
7
- // (SalesPipeline, ProspectingBuildTemplate, etc.). migration-helpers.ts imports them directly.
8
-
9
- /**
10
- * SystemEntry extended with the recursive tree slots (`systems`, `subsystems`).
11
- * `OrganizationModelSystemEntry` is inferred from `SystemEntrySchema: ZodType<SystemEntry>`
12
- * so it already carries these fields; this alias re-exports it under the tree-walker name
13
- * for clarity at call sites.
14
- */
15
- export type SystemEntryWithTree = OrganizationModelSystemEntry
16
-
17
- function childSystemsOf(system: SystemEntryWithTree): Record<string, SystemEntryWithTree> {
18
- return system.systems ?? system.subsystems ?? {}
19
- }
20
-
21
- export function defaultPathFor(id: string): string {
22
- return `/${id.replaceAll('.', '/')}`
23
- }
24
-
25
- export function parentIdOf(id: string): string | undefined {
26
- const index = id.lastIndexOf('.')
27
- return index === -1 ? undefined : id.slice(0, index)
28
- }
29
-
30
- /**
31
- * Return all entries of an OM domain map sorted by their declared `order` field.
32
- * Use this whenever iteration order matters for UI or deterministic output.
33
- */
34
- export function listDomain<T extends { id: string; order: number }>(record: Record<string, T>): T[] {
35
- return Object.values(record).sort((a, b) => a.order - b.order)
36
- }
37
-
38
- export function findById(
39
- systems: Record<string, OrganizationModelSystemEntry>,
40
- id: string
41
- ): OrganizationModelSystemEntry | undefined {
42
- return systems[id]
43
- }
44
-
45
- export function findByPath(
46
- systems: Record<string, OrganizationModelSystemEntry>,
47
- path: string
48
- ): OrganizationModelSystemEntry | undefined {
49
- return Object.values(systems).find((system) => (system.path ?? system.ui?.path ?? defaultPathFor(system.id)) === path)
50
- }
51
-
52
- export function childrenOf(
53
- systems: Record<string, OrganizationModelSystemEntry>,
54
- id: string
55
- ): OrganizationModelSystemEntry[] {
56
- const prefix = `${id}.`
57
- return Object.values(systems).filter(
58
- (system) => system.id.startsWith(prefix) && !system.id.slice(prefix.length).includes('.')
59
- )
60
- }
61
-
62
- export function topLevel(systems: Record<string, OrganizationModelSystemEntry>): OrganizationModelSystemEntry[] {
63
- return Object.values(systems).filter((system) => !system.id.includes('.'))
64
- }
65
-
66
- export function ancestorsOf(
67
- systems: Record<string, OrganizationModelSystemEntry>,
68
- id: string
69
- ): OrganizationModelSystemEntry[] {
70
- const segments = id.split('.')
71
- const ids = segments.map((_, index) => segments.slice(0, index + 1).join('.'))
72
- return ids
73
- .map((ancestorId) => findById(systems, ancestorId))
74
- .filter((system): system is OrganizationModelSystemEntry => Boolean(system))
75
- }
76
-
77
- export function parentOf(
78
- systems: Record<string, OrganizationModelSystemEntry>,
79
- id: string
80
- ): OrganizationModelSystemEntry | undefined {
81
- const parentId = parentIdOf(id)
82
- return parentId ? findById(systems, parentId) : undefined
83
- }
84
-
85
- function inheritedValue<T>(
86
- systems: Record<string, OrganizationModelSystemEntry>,
87
- id: string,
88
- getValue: (system: OrganizationModelSystemEntry) => T | undefined
89
- ): T | undefined {
90
- return ancestorsOf(systems, id)
91
- .slice()
92
- .reverse()
93
- .map(getValue)
94
- .find((value): value is T => value !== undefined)
95
- }
96
-
97
- export function uiPositionFor(systems: Record<string, OrganizationModelSystemEntry>, id: string) {
98
- return inheritedValue(systems, id, (system) => system.uiPosition)
99
- }
100
-
101
- export function requiresAdminFor(systems: Record<string, OrganizationModelSystemEntry>, id: string): boolean {
102
- return inheritedValue(systems, id, (system) => system.requiresAdmin) ?? false
103
- }
104
-
105
- export function devOnlyFor(systems: Record<string, OrganizationModelSystemEntry>, id: string): boolean {
106
- return inheritedValue(systems, id, (system) => system.devOnly || system.lifecycle === 'beta' || undefined) ?? false
107
- }
108
-
109
- // ---------------------------------------------------------------------------
110
- // Recursive system-tree helpers (Wave 1B)
111
- // These operate on the NEW recursive System shape introduced by W1A, where
112
- // `model.systems` is `Record<LocalId, SystemEntry>` (nested via `subsystems`).
113
- // `SystemEntry` here is the extended interface defined at the top of this file.
114
- // ---------------------------------------------------------------------------
115
-
116
- /**
117
- * Resolve a system by its dot-separated path string.
118
- *
119
- * `getSystem(model, 'sales.crm.dispositions')` walks:
120
- * `model.systems['sales'] → .subsystems['crm'] → .subsystems['dispositions']`
121
- *
122
- * Returns `undefined` if any segment is missing.
123
- */
124
- export function getSystem(model: OrganizationModel, path: string): SystemEntryWithTree | undefined {
125
- const segments = path.split('.')
126
- // model.systems is typed as Record<string, OrganizationModelSystemEntry>; cast
127
- // to SystemEntryWithTree (which extends it with optional content + subsystems).
128
- let current: Record<string, SystemEntryWithTree> = model.systems as Record<string, SystemEntryWithTree>
129
- let node: SystemEntryWithTree | undefined
130
- for (const seg of segments) {
131
- node = current[seg]
132
- if (node === undefined) return undefined
133
- current = childSystemsOf(node)
134
- }
135
- return node
136
- }
137
-
138
- /**
139
- * Return the root-first ancestor chain for a system path, including the system
140
- * itself as the last element.
141
- *
142
- * `getSystemAncestors(model, 'sales.crm')` → `[salesEntry, crmEntry]`
143
- *
144
- * Returns an empty array if the path cannot be resolved.
145
- */
146
- export function getSystemAncestors(model: OrganizationModel, path: string): SystemEntryWithTree[] {
147
- const segments = path.split('.')
148
- const result: SystemEntryWithTree[] = []
149
- let current: Record<string, SystemEntryWithTree> = model.systems as Record<string, SystemEntryWithTree>
150
- for (const seg of segments) {
151
- const node = current[seg]
152
- if (node === undefined) return []
153
- result.push(node)
154
- current = childSystemsOf(node)
155
- }
156
- return result
157
- }
158
-
159
- /**
160
- * Flat depth-first enumeration of every system in the model tree.
161
- * Each entry carries the computed full path and the system node.
162
- * Order: parent before children (pre-order DFS).
163
- *
164
- * Example result paths: `'sales'`, `'sales.crm'`, `'sales.crm.dispositions'`
165
- */
166
- export function listAllSystems(model: OrganizationModel): Array<{ path: string; system: SystemEntryWithTree }> {
167
- const results: Array<{ path: string; system: SystemEntryWithTree }> = []
168
-
169
- function walk(map: Record<string, SystemEntryWithTree>, prefix: string): void {
170
- for (const [localId, system] of Object.entries(map)) {
171
- const fullPath = prefix ? `${prefix}.${localId}` : localId
172
- results.push({ path: fullPath, system })
173
- const childSystems = childSystemsOf(system)
174
- if (Object.keys(childSystems).length > 0) {
175
- walk(childSystems, fullPath)
176
- }
177
- }
178
- }
179
-
180
- walk(model.systems as Record<string, SystemEntryWithTree>, '')
181
- return results
182
- }
183
-
184
-
185
- function isPlainJsonObject(value: JsonValue | undefined): value is Record<string, JsonValue> {
186
- return typeof value === 'object' && value !== null && !Array.isArray(value)
187
- }
188
-
189
- function mergeJsonConfig(
190
- base: Record<string, JsonValue>,
191
- override: Record<string, JsonValue>
192
- ): Record<string, JsonValue> {
193
- const result: Record<string, JsonValue> = { ...base }
194
-
195
- for (const [key, value] of Object.entries(override)) {
196
- const existing = result[key]
197
- result[key] =
198
- isPlainJsonObject(existing) && isPlainJsonObject(value) ? mergeJsonConfig(existing, value) : value
199
- }
200
-
201
- return result
202
- }
203
-
204
- /** Resolve a system's effective local config from first-class `System.config`. */
205
- export function resolveSystemConfig(model: OrganizationModel, path: string): Record<string, JsonValue> {
206
- const system = getSystem(model, path)
207
- if (system === undefined) return {}
208
-
209
- return mergeJsonConfig({}, system.config ?? {})
210
- }
211
-
212
- /**
213
- * Return all resources whose `systemPath` belongs to the given system.
214
- *
215
- * When `includeDescendants` is false (default), only resources whose
216
- * `systemPath` exactly matches `systemPath` are returned.
217
- *
218
- * When `includeDescendants` is true, resources attached to any descendant
219
- * system are also included. Descendant matching is segment-aware: the path
220
- * `'sales'` does NOT match `'salesforce.foo'` because segment boundaries are
221
- * enforced by splitting on `'.'`.
222
- *
223
- * @example
224
- * getResourcesForSystem(model, 'sales')
225
- * // → resources where systemPath === 'sales'
226
- *
227
- * getResourcesForSystem(model, 'sales', { includeDescendants: true })
228
- * // → resources where systemPath === 'sales' OR systemPath starts with 'sales.'
229
- */
230
- export function getResourcesForSystem(
231
- model: OrganizationModel,
232
- systemPath: string,
233
- options: { includeDescendants?: boolean } = {}
234
- ): ResourceEntry[] {
235
- const { includeDescendants = false } = options
236
- const prefix = systemPath + '.'
237
- return Object.values(model.resources ?? {}).filter(
238
- (r) => r.systemPath === systemPath || (includeDescendants && r.systemPath.startsWith(prefix))
239
- )
240
- }
241
-
242
- export interface SystemTopologyEdge {
243
- id: string
244
- relationship: OmTopologyRelationship
245
- }
246
-
247
- export interface SystemDeprecationDependents {
248
- resources: ResourceEntry[]
249
- topologyEdges: SystemTopologyEdge[]
250
- }
251
-
252
- function systemPathIsInScope(candidate: string, systemPath: string, includeDescendants: boolean): boolean {
253
- return candidate === systemPath || (includeDescendants && candidate.startsWith(`${systemPath}.`))
254
- }
255
-
256
- function topologyRelationshipTouchesSystem(
257
- relationship: OmTopologyRelationship,
258
- systemPath: string,
259
- resourceIds: Set<string>,
260
- includeDescendants: boolean
261
- ): boolean {
262
- if (
263
- relationship.systemPath !== undefined &&
264
- systemPathIsInScope(relationship.systemPath, systemPath, includeDescendants)
265
- ) {
266
- return true
267
- }
268
-
269
- for (const ref of [relationship.from, relationship.to]) {
270
- if (ref.kind === 'system' && systemPathIsInScope(ref.id, systemPath, includeDescendants)) return true
271
- if (ref.kind === 'resource' && resourceIds.has(ref.id)) return true
272
- }
273
-
274
- return false
275
- }
276
-
277
- export function getTopologyEdgesForSystem(
278
- model: OrganizationModel,
279
- systemPath: string,
280
- options: { includeDescendants?: boolean; resourceIds?: Iterable<string> } = {}
281
- ): SystemTopologyEdge[] {
282
- const { includeDescendants = false } = options
283
- const resourceIds = new Set(
284
- options.resourceIds ?? getResourcesForSystem(model, systemPath, { includeDescendants }).map((r) => r.id)
285
- )
286
-
287
- return Object.entries(model.topology?.relationships ?? {})
288
- .filter(([, relationship]) =>
289
- topologyRelationshipTouchesSystem(relationship, systemPath, resourceIds, includeDescendants)
290
- )
291
- .map(([id, relationship]) => ({ id, relationship }))
292
- }
293
-
294
- export function getSystemDeprecationDependents(
295
- model: OrganizationModel,
296
- systemPath: string,
297
- options: { includeDescendants?: boolean } = {}
298
- ): SystemDeprecationDependents {
299
- const resources = getResourcesForSystem(model, systemPath, options).filter((resource) => resource.status === 'active')
300
- const resourceIds = new Set(resources.map((resource) => resource.id))
301
-
302
- return {
303
- resources,
304
- topologyEdges: getTopologyEdgesForSystem(model, systemPath, { ...options, resourceIds })
305
- }
306
- }
1
+ import { z } from 'zod'
2
+ import type { OrganizationModel, OrganizationModelSystemEntry } from './types'
3
+ import type { JsonValue } from './domains/systems'
4
+ import type { ResourceEntry } from './domains/resources'
5
+ import type { OmTopologyRelationship } from './domains/topology'
6
+
7
+ /**
8
+ * Generic domain-record factory. Validates every entry through the given Zod
9
+ * schema (throws `ZodError` on invalid input) and returns an id-keyed map.
10
+ *
11
+ * Each entry MUST carry an `id` field validated at runtime via the parsed
12
+ * result. Use the per-domain `defineX` / `defineXs` wrappers at call sites for
13
+ * schema-specific type inference; this function is the shared implementation.
14
+ *
15
+ * @example
16
+ * const actions = defineDomainRecord(ActionSchema, [
17
+ * { id: 'my-action', order: 10, label: 'My Action', invocations: [] }
18
+ * ])
19
+ * // → { 'my-action': { id: 'my-action', order: 10, label: 'My Action', invocations: [], ... } }
20
+ */
21
+ export function defineDomainRecord<TSchema extends z.ZodTypeAny>(
22
+ schema: TSchema,
23
+ entries: readonly z.input<TSchema>[]
24
+ ): Record<string, z.infer<TSchema>> {
25
+ return Object.fromEntries(
26
+ entries.map((entry) => {
27
+ const parsed = schema.parse(entry) as z.infer<TSchema> & { id: string }
28
+ return [parsed.id, parsed]
29
+ })
30
+ )
31
+ }
32
+
33
+ // Phase 4: removed locally-scoped inferred types for deleted compound-domain schemas
34
+ // (SalesPipeline, ProspectingBuildTemplate, etc.). migration-helpers.ts imports them directly.
35
+
36
+ /**
37
+ * SystemEntry extended with the recursive tree slots (`systems`, `subsystems`).
38
+ * `OrganizationModelSystemEntry` is inferred from `SystemEntrySchema: ZodType<SystemEntry>`
39
+ * so it already carries these fields; this alias re-exports it under the tree-walker name
40
+ * for clarity at call sites.
41
+ */
42
+ export type SystemEntryWithTree = OrganizationModelSystemEntry
43
+
44
+ function childSystemsOf(system: SystemEntryWithTree): Record<string, SystemEntryWithTree> {
45
+ return system.systems ?? system.subsystems ?? {}
46
+ }
47
+
48
+ export function defaultPathFor(id: string): string {
49
+ return `/${id.replaceAll('.', '/')}`
50
+ }
51
+
52
+ export function parentIdOf(id: string): string | undefined {
53
+ const index = id.lastIndexOf('.')
54
+ return index === -1 ? undefined : id.slice(0, index)
55
+ }
56
+
57
+ /**
58
+ * Return all entries of an OM domain map sorted by their declared `order` field.
59
+ * Use this whenever iteration order matters for UI or deterministic output.
60
+ */
61
+ export function listDomain<T extends { id: string; order: number }>(record: Record<string, T>): T[] {
62
+ return Object.values(record).sort((a, b) => a.order - b.order)
63
+ }
64
+
65
+ export function findById(
66
+ systems: Record<string, OrganizationModelSystemEntry>,
67
+ id: string
68
+ ): OrganizationModelSystemEntry | undefined {
69
+ return systems[id]
70
+ }
71
+
72
+ export function findByPath(
73
+ systems: Record<string, OrganizationModelSystemEntry>,
74
+ path: string
75
+ ): OrganizationModelSystemEntry | undefined {
76
+ return Object.values(systems).find((system) => (system.path ?? system.ui?.path ?? defaultPathFor(system.id)) === path)
77
+ }
78
+
79
+ export function childrenOf(
80
+ systems: Record<string, OrganizationModelSystemEntry>,
81
+ id: string
82
+ ): OrganizationModelSystemEntry[] {
83
+ const prefix = `${id}.`
84
+ return Object.values(systems).filter(
85
+ (system) => system.id.startsWith(prefix) && !system.id.slice(prefix.length).includes('.')
86
+ )
87
+ }
88
+
89
+ export function topLevel(systems: Record<string, OrganizationModelSystemEntry>): OrganizationModelSystemEntry[] {
90
+ return Object.values(systems).filter((system) => !system.id.includes('.'))
91
+ }
92
+
93
+ export function ancestorsOf(
94
+ systems: Record<string, OrganizationModelSystemEntry>,
95
+ id: string
96
+ ): OrganizationModelSystemEntry[] {
97
+ const segments = id.split('.')
98
+ const ids = segments.map((_, index) => segments.slice(0, index + 1).join('.'))
99
+ return ids
100
+ .map((ancestorId) => findById(systems, ancestorId))
101
+ .filter((system): system is OrganizationModelSystemEntry => Boolean(system))
102
+ }
103
+
104
+ export function parentOf(
105
+ systems: Record<string, OrganizationModelSystemEntry>,
106
+ id: string
107
+ ): OrganizationModelSystemEntry | undefined {
108
+ const parentId = parentIdOf(id)
109
+ return parentId ? findById(systems, parentId) : undefined
110
+ }
111
+
112
+ function inheritedValue<T>(
113
+ systems: Record<string, OrganizationModelSystemEntry>,
114
+ id: string,
115
+ getValue: (system: OrganizationModelSystemEntry) => T | undefined
116
+ ): T | undefined {
117
+ return ancestorsOf(systems, id)
118
+ .slice()
119
+ .reverse()
120
+ .map(getValue)
121
+ .find((value): value is T => value !== undefined)
122
+ }
123
+
124
+ export function uiPositionFor(systems: Record<string, OrganizationModelSystemEntry>, id: string) {
125
+ return inheritedValue(systems, id, (system) => system.uiPosition)
126
+ }
127
+
128
+ export function requiresAdminFor(systems: Record<string, OrganizationModelSystemEntry>, id: string): boolean {
129
+ return inheritedValue(systems, id, (system) => system.requiresAdmin) ?? false
130
+ }
131
+
132
+ export function devOnlyFor(systems: Record<string, OrganizationModelSystemEntry>, id: string): boolean {
133
+ return inheritedValue(systems, id, (system) => system.devOnly || system.lifecycle === 'beta' || undefined) ?? false
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Recursive system-tree helpers (Wave 1B)
138
+ // These operate on the NEW recursive System shape introduced by W1A, where
139
+ // `model.systems` is `Record<LocalId, SystemEntry>` (nested via `subsystems`).
140
+ // `SystemEntry` here is the extended interface defined at the top of this file.
141
+ // ---------------------------------------------------------------------------
142
+
143
+ /**
144
+ * Resolve a system by its dot-separated path string.
145
+ *
146
+ * `getSystem(model, 'sales.crm.dispositions')` walks:
147
+ * `model.systems['sales'] .subsystems['crm'] → .subsystems['dispositions']`
148
+ *
149
+ * Returns `undefined` if any segment is missing.
150
+ */
151
+ export function getSystem(model: OrganizationModel, path: string): SystemEntryWithTree | undefined {
152
+ const segments = path.split('.')
153
+ // model.systems is typed as Record<string, OrganizationModelSystemEntry>; cast
154
+ // to SystemEntryWithTree (which extends it with optional content + subsystems).
155
+ let current: Record<string, SystemEntryWithTree> = model.systems as Record<string, SystemEntryWithTree>
156
+ let node: SystemEntryWithTree | undefined
157
+ for (const seg of segments) {
158
+ node = current[seg]
159
+ if (node === undefined) return undefined
160
+ current = childSystemsOf(node)
161
+ }
162
+ return node
163
+ }
164
+
165
+ /**
166
+ * Return the root-first ancestor chain for a system path, including the system
167
+ * itself as the last element.
168
+ *
169
+ * `getSystemAncestors(model, 'sales.crm')` `[salesEntry, crmEntry]`
170
+ *
171
+ * Returns an empty array if the path cannot be resolved.
172
+ */
173
+ export function getSystemAncestors(model: OrganizationModel, path: string): SystemEntryWithTree[] {
174
+ const segments = path.split('.')
175
+ const result: SystemEntryWithTree[] = []
176
+ let current: Record<string, SystemEntryWithTree> = model.systems as Record<string, SystemEntryWithTree>
177
+ for (const seg of segments) {
178
+ const node = current[seg]
179
+ if (node === undefined) return []
180
+ result.push(node)
181
+ current = childSystemsOf(node)
182
+ }
183
+ return result
184
+ }
185
+
186
+ /**
187
+ * Flat depth-first enumeration of every system in the model tree.
188
+ * Each entry carries the computed full path and the system node.
189
+ * Order: parent before children (pre-order DFS).
190
+ *
191
+ * Example result paths: `'sales'`, `'sales.crm'`, `'sales.crm.dispositions'`
192
+ */
193
+ export function listAllSystems(model: OrganizationModel): Array<{ path: string; system: SystemEntryWithTree }> {
194
+ const results: Array<{ path: string; system: SystemEntryWithTree }> = []
195
+
196
+ function walk(map: Record<string, SystemEntryWithTree>, prefix: string): void {
197
+ for (const [localId, system] of Object.entries(map)) {
198
+ const fullPath = prefix ? `${prefix}.${localId}` : localId
199
+ results.push({ path: fullPath, system })
200
+ const childSystems = childSystemsOf(system)
201
+ if (Object.keys(childSystems).length > 0) {
202
+ walk(childSystems, fullPath)
203
+ }
204
+ }
205
+ }
206
+
207
+ walk(model.systems as Record<string, SystemEntryWithTree>, '')
208
+ return results
209
+ }
210
+
211
+ function isPlainJsonObject(value: JsonValue | undefined): value is Record<string, JsonValue> {
212
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
213
+ }
214
+
215
+ function mergeJsonConfig(
216
+ base: Record<string, JsonValue>,
217
+ override: Record<string, JsonValue>
218
+ ): Record<string, JsonValue> {
219
+ const result: Record<string, JsonValue> = { ...base }
220
+
221
+ for (const [key, value] of Object.entries(override)) {
222
+ const existing = result[key]
223
+ result[key] = isPlainJsonObject(existing) && isPlainJsonObject(value) ? mergeJsonConfig(existing, value) : value
224
+ }
225
+
226
+ return result
227
+ }
228
+
229
+ /** Resolve a system's effective local config from first-class `System.config`. */
230
+ export function resolveSystemConfig(model: OrganizationModel, path: string): Record<string, JsonValue> {
231
+ const system = getSystem(model, path)
232
+ if (system === undefined) return {}
233
+
234
+ return mergeJsonConfig({}, system.config ?? {})
235
+ }
236
+
237
+ /**
238
+ * Return all resources whose `systemPath` belongs to the given system.
239
+ *
240
+ * When `includeDescendants` is false (default), only resources whose
241
+ * `systemPath` exactly matches `systemPath` are returned.
242
+ *
243
+ * When `includeDescendants` is true, resources attached to any descendant
244
+ * system are also included. Descendant matching is segment-aware: the path
245
+ * `'sales'` does NOT match `'salesforce.foo'` because segment boundaries are
246
+ * enforced by splitting on `'.'`.
247
+ *
248
+ * @example
249
+ * getResourcesForSystem(model, 'sales')
250
+ * // → resources where systemPath === 'sales'
251
+ *
252
+ * getResourcesForSystem(model, 'sales', { includeDescendants: true })
253
+ * // → resources where systemPath === 'sales' OR systemPath starts with 'sales.'
254
+ */
255
+ export function getResourcesForSystem(
256
+ model: OrganizationModel,
257
+ systemPath: string,
258
+ options: { includeDescendants?: boolean } = {}
259
+ ): ResourceEntry[] {
260
+ const { includeDescendants = false } = options
261
+ const prefix = systemPath + '.'
262
+ return Object.values(model.resources ?? {}).filter(
263
+ (r) => r.systemPath === systemPath || (includeDescendants && r.systemPath.startsWith(prefix))
264
+ )
265
+ }
266
+
267
+ export interface SystemTopologyEdge {
268
+ id: string
269
+ relationship: OmTopologyRelationship
270
+ }
271
+
272
+ export interface SystemDeprecationDependents {
273
+ resources: ResourceEntry[]
274
+ topologyEdges: SystemTopologyEdge[]
275
+ }
276
+
277
+ function systemPathIsInScope(candidate: string, systemPath: string, includeDescendants: boolean): boolean {
278
+ return candidate === systemPath || (includeDescendants && candidate.startsWith(`${systemPath}.`))
279
+ }
280
+
281
+ function topologyRelationshipTouchesSystem(
282
+ relationship: OmTopologyRelationship,
283
+ systemPath: string,
284
+ resourceIds: Set<string>,
285
+ includeDescendants: boolean
286
+ ): boolean {
287
+ if (
288
+ relationship.systemPath !== undefined &&
289
+ systemPathIsInScope(relationship.systemPath, systemPath, includeDescendants)
290
+ ) {
291
+ return true
292
+ }
293
+
294
+ for (const ref of [relationship.from, relationship.to]) {
295
+ if (ref.kind === 'system' && systemPathIsInScope(ref.id, systemPath, includeDescendants)) return true
296
+ if (ref.kind === 'resource' && resourceIds.has(ref.id)) return true
297
+ }
298
+
299
+ return false
300
+ }
301
+
302
+ export function getTopologyEdgesForSystem(
303
+ model: OrganizationModel,
304
+ systemPath: string,
305
+ options: { includeDescendants?: boolean; resourceIds?: Iterable<string> } = {}
306
+ ): SystemTopologyEdge[] {
307
+ const { includeDescendants = false } = options
308
+ const resourceIds = new Set(
309
+ options.resourceIds ?? getResourcesForSystem(model, systemPath, { includeDescendants }).map((r) => r.id)
310
+ )
311
+
312
+ return Object.entries(model.topology?.relationships ?? {})
313
+ .filter(([, relationship]) =>
314
+ topologyRelationshipTouchesSystem(relationship, systemPath, resourceIds, includeDescendants)
315
+ )
316
+ .map(([id, relationship]) => ({ id, relationship }))
317
+ }
318
+
319
+ export function getSystemDeprecationDependents(
320
+ model: OrganizationModel,
321
+ systemPath: string,
322
+ options: { includeDescendants?: boolean } = {}
323
+ ): SystemDeprecationDependents {
324
+ const resources = getResourcesForSystem(model, systemPath, options).filter((resource) => resource.status === 'active')
325
+ const resourceIds = new Set(resources.map((resource) => resource.id))
326
+
327
+ return {
328
+ resources,
329
+ topologyEdges: getTopologyEdgesForSystem(model, systemPath, { ...options, resourceIds })
330
+ }
331
+ }