@elevasis/core 0.24.1 → 0.26.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 (82) hide show
  1. package/dist/index.d.ts +239 -86
  2. package/dist/index.js +474 -1346
  3. package/dist/knowledge/index.d.ts +57 -39
  4. package/dist/knowledge/index.js +1 -1
  5. package/dist/organization-model/index.d.ts +239 -86
  6. package/dist/organization-model/index.js +474 -1346
  7. package/dist/test-utils/index.d.ts +24 -31
  8. package/dist/test-utils/index.js +76 -1238
  9. package/package.json +1 -1
  10. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +108 -96
  11. package/src/business/acquisition/api-schemas.test.ts +70 -77
  12. package/src/business/acquisition/api-schemas.ts +21 -42
  13. package/src/business/acquisition/derive-actions.test.ts +11 -21
  14. package/src/business/acquisition/derive-actions.ts +61 -14
  15. package/src/business/acquisition/ontology-validation.ts +4 -4
  16. package/src/business/acquisition/types.ts +7 -8
  17. package/src/execution/engine/llm/adapters/__tests__/openrouter.integration.test.ts +10 -10
  18. package/src/knowledge/__tests__/queries.test.ts +960 -546
  19. package/src/knowledge/format.ts +322 -100
  20. package/src/knowledge/index.ts +18 -5
  21. package/src/knowledge/queries.ts +1004 -240
  22. package/src/organization-model/__tests__/content-kinds-registry.test.ts +35 -210
  23. package/src/organization-model/__tests__/defaults.test.ts +4 -4
  24. package/src/organization-model/__tests__/deprecate-helpers.test.ts +71 -0
  25. package/src/organization-model/__tests__/domains/actions.test.ts +12 -36
  26. package/src/organization-model/__tests__/domains/offerings.test.ts +13 -6
  27. package/src/organization-model/__tests__/domains/resources.test.ts +497 -350
  28. package/src/organization-model/__tests__/domains/systems.test.ts +6 -7
  29. package/src/organization-model/__tests__/flatten-additive-merge.test.ts +68 -80
  30. package/src/organization-model/__tests__/foundation.test.ts +81 -14
  31. package/src/organization-model/__tests__/graph.test.ts +662 -694
  32. package/src/organization-model/__tests__/knowledge.test.ts +31 -17
  33. package/src/organization-model/__tests__/lookup-helpers.test.ts +128 -438
  34. package/src/organization-model/__tests__/migration-helpers.test.ts +362 -591
  35. package/src/organization-model/__tests__/prospecting-ssot.test.ts +68 -103
  36. package/src/organization-model/__tests__/published-zero-leak.test.ts +17 -0
  37. package/src/organization-model/__tests__/recursive-system-schema.test.ts +159 -532
  38. package/src/organization-model/__tests__/resolve.test.ts +88 -49
  39. package/src/organization-model/__tests__/scaffolders.test.ts +93 -0
  40. package/src/organization-model/__tests__/schema.test.ts +65 -56
  41. package/src/organization-model/catalogs/lead-gen.ts +0 -103
  42. package/src/organization-model/defaults.ts +17 -702
  43. package/src/organization-model/domains/actions.ts +116 -333
  44. package/src/organization-model/domains/knowledge.ts +15 -7
  45. package/src/organization-model/domains/projects.ts +4 -4
  46. package/src/organization-model/domains/prospecting.ts +405 -395
  47. package/src/organization-model/domains/resources.ts +206 -135
  48. package/src/organization-model/domains/sales.ts +5 -5
  49. package/src/organization-model/domains/systems.ts +8 -23
  50. package/src/organization-model/graph/build.ts +223 -294
  51. package/src/organization-model/graph/schema.ts +2 -3
  52. package/src/organization-model/graph/types.ts +12 -14
  53. package/src/organization-model/helpers.ts +120 -141
  54. package/src/organization-model/icons.ts +1 -0
  55. package/src/organization-model/index.ts +107 -126
  56. package/src/organization-model/migration-helpers.ts +211 -249
  57. package/src/organization-model/ontology.ts +0 -60
  58. package/src/organization-model/organization-graph.mdx +4 -5
  59. package/src/organization-model/organization-model.mdx +1 -1
  60. package/src/organization-model/published.ts +251 -228
  61. package/src/organization-model/resolve.ts +4 -5
  62. package/src/organization-model/scaffolders/helpers.ts +84 -0
  63. package/src/organization-model/scaffolders/index.ts +19 -0
  64. package/src/organization-model/scaffolders/scaffoldKnowledgeNode.ts +48 -0
  65. package/src/organization-model/scaffolders/scaffoldOntologyRecord.ts +38 -0
  66. package/src/organization-model/scaffolders/scaffoldResource.ts +59 -0
  67. package/src/organization-model/scaffolders/scaffoldSystem.ts +110 -0
  68. package/src/organization-model/scaffolders/types.ts +81 -0
  69. package/src/organization-model/schema.ts +610 -704
  70. package/src/organization-model/types.ts +167 -161
  71. package/src/platform/constants/versions.ts +1 -1
  72. package/src/platform/registry/__tests__/validation.test.ts +23 -0
  73. package/src/platform/registry/validation.ts +13 -2
  74. package/src/reference/_generated/contracts.md +108 -96
  75. package/src/reference/glossary.md +71 -69
  76. package/src/organization-model/content-kinds/config.ts +0 -36
  77. package/src/organization-model/content-kinds/index.ts +0 -78
  78. package/src/organization-model/content-kinds/pipeline.ts +0 -68
  79. package/src/organization-model/content-kinds/registry.ts +0 -44
  80. package/src/organization-model/content-kinds/status.ts +0 -71
  81. package/src/organization-model/content-kinds/template.ts +0 -83
  82. package/src/organization-model/content-kinds/types.ts +0 -117
@@ -1,74 +1,75 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { bySystem, byOntology, byKind, byOwner, governs, governedBy, parsePath } from '../queries'
1
+ import { describe, expect, it } from 'vitest'
2
+ import { bySystem, byOntology, byKind, byOwner, governs, governedBy, parsePath, omSearch, omDescribe } from '../queries'
3
3
  import { byOntology as publicByOntology } from '../index'
4
- import { formatText, formatJson, formatIdsOnly } from '../format'
5
- import type { OrganizationGraph } from '../../organization-model/graph/types'
6
- import type { OrgKnowledgeNode } from '../../organization-model/domains/knowledge'
7
-
8
- // ---------------------------------------------------------------------------
9
- // Synthetic fixtures
10
- // ---------------------------------------------------------------------------
11
-
12
- /**
13
- * Three knowledge nodes covering all three kinds.
14
- * - playbook-a governs system:sales.crm and system:sales.lead-gen
15
- * - strategy-b governs system:sales.lead-gen
16
- * - reference-c has no links (no governs edges)
17
- * - playbook-d is owned by "role.ops-lead"
18
- */
19
- const NODES: OrgKnowledgeNode[] = [
20
- {
21
- id: 'knowledge.playbook-a',
22
- kind: 'playbook',
23
- title: 'Playbook A',
24
- summary: 'Playbook A summary.',
25
- body: '## Playbook A\n\nContent.',
26
- links: [{ nodeId: 'system:sales.crm' }, { nodeId: 'system:sales.lead-gen' }],
27
- ownerIds: [],
28
- updatedAt: '2026-01-01'
29
- },
30
- {
31
- id: 'knowledge.strategy-b',
32
- kind: 'strategy',
33
- title: 'Strategy B',
34
- summary: 'Strategy B summary.',
35
- body: '## Strategy B\n\nContent.',
36
- links: [{ nodeId: 'system:sales.lead-gen' }],
37
- ownerIds: ['role.ops-lead'],
38
- updatedAt: '2026-01-02'
39
- },
40
- {
41
- id: 'knowledge.reference-c',
42
- kind: 'reference',
43
- title: 'Reference C',
44
- summary: 'Reference C summary.',
45
- body: '## Reference C\n\nContent.',
46
- links: [],
47
- ownerIds: [],
48
- updatedAt: '2026-01-03'
49
- },
50
- {
51
- id: 'knowledge.playbook-d',
52
- kind: 'playbook',
53
- title: 'Playbook D',
54
- summary: 'Playbook D summary.',
55
- body: '## Playbook D\n\nContent.',
56
- links: [],
57
- ownerIds: ['role.ops-lead', 'role.ceo'],
58
- updatedAt: '2026-01-04'
59
- }
60
- ]
61
-
62
- /**
63
- * Minimal synthetic OrganizationGraph derived from the above nodes.
64
- * Graph node IDs follow the convention: `knowledge:<om-node-id>`
65
- */
66
- const GRAPH: OrganizationGraph = {
67
- version: 1,
68
- organizationModelVersion: 1,
69
- nodes: [
70
- { id: 'organization-model', kind: 'organization', label: 'Organization Model' },
71
- { id: 'system:sales.crm', kind: 'system', label: 'CRM', sourceId: 'sales.crm', systemId: 'sales.crm' },
4
+ import { formatText, formatJson, formatIdsOnly, formatOmSearchHits, formatOmDescribe } from '../format'
5
+ import type { OrganizationGraph } from '../../organization-model/graph/types'
6
+ import type { OrgKnowledgeNode } from '../../organization-model/domains/knowledge'
7
+ import type { OrganizationModel } from '../../organization-model/types'
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Synthetic fixtures
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /**
14
+ * Three knowledge nodes covering all three kinds.
15
+ * - playbook-a governs system:sales.crm and system:sales.lead-gen
16
+ * - strategy-b governs system:sales.lead-gen
17
+ * - reference-c has no links (no governs edges)
18
+ * - playbook-d is owned by "role.ops-lead"
19
+ */
20
+ const NODES: OrgKnowledgeNode[] = [
21
+ {
22
+ id: 'knowledge.playbook-a',
23
+ kind: 'playbook',
24
+ title: 'Playbook A',
25
+ summary: 'Playbook A summary.',
26
+ body: '## Playbook A\n\nContent.',
27
+ links: [{ nodeId: 'system:sales.crm' }, { nodeId: 'system:sales.lead-gen' }],
28
+ ownerIds: [],
29
+ updatedAt: '2026-01-01'
30
+ },
31
+ {
32
+ id: 'knowledge.strategy-b',
33
+ kind: 'strategy',
34
+ title: 'Strategy B',
35
+ summary: 'Strategy B summary.',
36
+ body: '## Strategy B\n\nContent.',
37
+ links: [{ nodeId: 'system:sales.lead-gen' }],
38
+ ownerIds: ['role.ops-lead'],
39
+ updatedAt: '2026-01-02'
40
+ },
41
+ {
42
+ id: 'knowledge.reference-c',
43
+ kind: 'reference',
44
+ title: 'Reference C',
45
+ summary: 'Reference C summary.',
46
+ body: '## Reference C\n\nContent.',
47
+ links: [],
48
+ ownerIds: [],
49
+ updatedAt: '2026-01-03'
50
+ },
51
+ {
52
+ id: 'knowledge.playbook-d',
53
+ kind: 'playbook',
54
+ title: 'Playbook D',
55
+ summary: 'Playbook D summary.',
56
+ body: '## Playbook D\n\nContent.',
57
+ links: [],
58
+ ownerIds: ['role.ops-lead', 'role.ceo'],
59
+ updatedAt: '2026-01-04'
60
+ }
61
+ ]
62
+
63
+ /**
64
+ * Minimal synthetic OrganizationGraph derived from the above nodes.
65
+ * Graph node IDs follow the convention: `knowledge:<om-node-id>`
66
+ */
67
+ const GRAPH: OrganizationGraph = {
68
+ version: 1,
69
+ organizationModelVersion: 1,
70
+ nodes: [
71
+ { id: 'organization-model', kind: 'organization', label: 'Organization Model' },
72
+ { id: 'system:sales.crm', kind: 'system', label: 'CRM', sourceId: 'sales.crm', systemId: 'sales.crm' },
72
73
  {
73
74
  id: 'system:sales.lead-gen',
74
75
  kind: 'system',
@@ -85,21 +86,21 @@ const GRAPH: OrganizationGraph = {
85
86
  {
86
87
  id: 'knowledge:knowledge.playbook-a',
87
88
  kind: 'knowledge',
88
- label: 'Playbook A',
89
- sourceId: 'knowledge.playbook-a'
90
- },
91
- {
92
- id: 'knowledge:knowledge.strategy-b',
93
- kind: 'knowledge',
94
- label: 'Strategy B',
95
- sourceId: 'knowledge.strategy-b'
96
- },
97
- {
98
- id: 'knowledge:knowledge.reference-c',
99
- kind: 'knowledge',
100
- label: 'Reference C',
101
- sourceId: 'knowledge.reference-c'
102
- },
89
+ label: 'Playbook A',
90
+ sourceId: 'knowledge.playbook-a'
91
+ },
92
+ {
93
+ id: 'knowledge:knowledge.strategy-b',
94
+ kind: 'knowledge',
95
+ label: 'Strategy B',
96
+ sourceId: 'knowledge.strategy-b'
97
+ },
98
+ {
99
+ id: 'knowledge:knowledge.reference-c',
100
+ kind: 'knowledge',
101
+ label: 'Reference C',
102
+ sourceId: 'knowledge.reference-c'
103
+ },
103
104
  {
104
105
  id: 'knowledge:knowledge.playbook-d',
105
106
  kind: 'knowledge',
@@ -107,45 +108,45 @@ const GRAPH: OrganizationGraph = {
107
108
  sourceId: 'knowledge.playbook-d'
108
109
  }
109
110
  ],
110
- edges: [
111
- // contains edges (organization -> knowledge nodes)
112
- {
113
- id: 'edge:contains:organization-model:knowledge:knowledge.playbook-a',
114
- kind: 'contains',
115
- sourceId: 'organization-model',
116
- targetId: 'knowledge:knowledge.playbook-a'
117
- },
118
- {
119
- id: 'edge:contains:organization-model:knowledge:knowledge.strategy-b',
120
- kind: 'contains',
121
- sourceId: 'organization-model',
122
- targetId: 'knowledge:knowledge.strategy-b'
123
- },
124
- {
125
- id: 'edge:contains:organization-model:knowledge:knowledge.reference-c',
126
- kind: 'contains',
127
- sourceId: 'organization-model',
128
- targetId: 'knowledge:knowledge.reference-c'
129
- },
130
- {
131
- id: 'edge:contains:organization-model:knowledge:knowledge.playbook-d',
132
- kind: 'contains',
133
- sourceId: 'organization-model',
134
- targetId: 'knowledge:knowledge.playbook-d'
135
- },
136
- // governs edges
137
- {
138
- id: 'edge:governs:knowledge:knowledge.playbook-a:system:sales.crm',
139
- kind: 'governs',
140
- sourceId: 'knowledge:knowledge.playbook-a',
141
- targetId: 'system:sales.crm'
142
- },
143
- {
144
- id: 'edge:governs:knowledge:knowledge.playbook-a:system:sales.lead-gen',
145
- kind: 'governs',
146
- sourceId: 'knowledge:knowledge.playbook-a',
147
- targetId: 'system:sales.lead-gen'
148
- },
111
+ edges: [
112
+ // contains edges (organization -> knowledge nodes)
113
+ {
114
+ id: 'edge:contains:organization-model:knowledge:knowledge.playbook-a',
115
+ kind: 'contains',
116
+ sourceId: 'organization-model',
117
+ targetId: 'knowledge:knowledge.playbook-a'
118
+ },
119
+ {
120
+ id: 'edge:contains:organization-model:knowledge:knowledge.strategy-b',
121
+ kind: 'contains',
122
+ sourceId: 'organization-model',
123
+ targetId: 'knowledge:knowledge.strategy-b'
124
+ },
125
+ {
126
+ id: 'edge:contains:organization-model:knowledge:knowledge.reference-c',
127
+ kind: 'contains',
128
+ sourceId: 'organization-model',
129
+ targetId: 'knowledge:knowledge.reference-c'
130
+ },
131
+ {
132
+ id: 'edge:contains:organization-model:knowledge:knowledge.playbook-d',
133
+ kind: 'contains',
134
+ sourceId: 'organization-model',
135
+ targetId: 'knowledge:knowledge.playbook-d'
136
+ },
137
+ // governs edges
138
+ {
139
+ id: 'edge:governs:knowledge:knowledge.playbook-a:system:sales.crm',
140
+ kind: 'governs',
141
+ sourceId: 'knowledge:knowledge.playbook-a',
142
+ targetId: 'system:sales.crm'
143
+ },
144
+ {
145
+ id: 'edge:governs:knowledge:knowledge.playbook-a:system:sales.lead-gen',
146
+ kind: 'governs',
147
+ sourceId: 'knowledge:knowledge.playbook-a',
148
+ targetId: 'system:sales.lead-gen'
149
+ },
149
150
  {
150
151
  id: 'edge:governs:knowledge:knowledge.strategy-b:system:sales.lead-gen',
151
152
  kind: 'governs',
@@ -160,34 +161,34 @@ const GRAPH: OrganizationGraph = {
160
161
  }
161
162
  ]
162
163
  }
163
-
164
- // ---------------------------------------------------------------------------
165
- // bySystem
166
- // ---------------------------------------------------------------------------
167
-
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // bySystem
167
+ // ---------------------------------------------------------------------------
168
+
168
169
  describe('bySystem', () => {
169
- it('returns nodes that have governs edges to the given system', () => {
170
- const result = bySystem(GRAPH, 'sales.crm', NODES)
171
- expect(result).toHaveLength(1)
172
- expect(result[0].id).toBe('knowledge.playbook-a')
173
- })
174
-
175
- it('returns multiple nodes when multiple knowledge nodes govern the same system', () => {
176
- const result = bySystem(GRAPH, 'sales.lead-gen', NODES)
177
- const ids = result.map((n) => n.id).sort()
178
- expect(ids).toEqual(['knowledge.playbook-a', 'knowledge.strategy-b'].sort())
179
- })
180
-
181
- it('returns empty array when no node governs the system', () => {
182
- const result = bySystem(GRAPH, 'system.does-not-exist', NODES)
183
- expect(result).toHaveLength(0)
184
- })
185
-
186
- it('returns full OrgKnowledgeNode objects (not just graph stubs)', () => {
187
- const result = bySystem(GRAPH, 'sales.crm', NODES)
188
- expect(result[0].body).toBeDefined()
189
- expect(result[0].links).toBeDefined()
190
- expect(result[0].ownerIds).toBeDefined()
170
+ it('returns nodes that have governs edges to the given system', () => {
171
+ const result = bySystem(GRAPH, 'sales.crm', NODES)
172
+ expect(result).toHaveLength(1)
173
+ expect(result[0].id).toBe('knowledge.playbook-a')
174
+ })
175
+
176
+ it('returns multiple nodes when multiple knowledge nodes govern the same system', () => {
177
+ const result = bySystem(GRAPH, 'sales.lead-gen', NODES)
178
+ const ids = result.map((n) => n.id).sort()
179
+ expect(ids).toEqual(['knowledge.playbook-a', 'knowledge.strategy-b'].sort())
180
+ })
181
+
182
+ it('returns empty array when no node governs the system', () => {
183
+ const result = bySystem(GRAPH, 'system.does-not-exist', NODES)
184
+ expect(result).toHaveLength(0)
185
+ })
186
+
187
+ it('returns full OrgKnowledgeNode objects (not just graph stubs)', () => {
188
+ const result = bySystem(GRAPH, 'sales.crm', NODES)
189
+ expect(result[0].body).toBeDefined()
190
+ expect(result[0].links).toBeDefined()
191
+ expect(result[0].ownerIds).toBeDefined()
191
192
  })
192
193
  })
193
194
 
@@ -249,9 +250,7 @@ describe('byOntology', () => {
249
250
  }
250
251
  ]
251
252
 
252
- expect(byOntology(graph, 'sales.crm:object/deal', nodes).map((node) => node.id)).toEqual([
253
- 'knowledge.playbook-d'
254
- ])
253
+ expect(byOntology(graph, 'sales.crm:object/deal', nodes).map((node) => node.id)).toEqual(['knowledge.playbook-d'])
255
254
  })
256
255
 
257
256
  it('is exported from the public knowledge query API', () => {
@@ -264,136 +263,136 @@ describe('byOntology', () => {
264
263
 
265
264
  // ---------------------------------------------------------------------------
266
265
  // byKind
267
- // ---------------------------------------------------------------------------
268
-
266
+ // ---------------------------------------------------------------------------
267
+
269
268
  describe('byKind', () => {
270
- it('returns all playbook nodes', () => {
271
- const result = byKind(GRAPH, 'playbook', NODES)
272
- expect(result).toHaveLength(2)
273
- expect(result.every((n) => n.kind === 'playbook')).toBe(true)
274
- })
275
-
276
- it('returns strategy nodes', () => {
277
- const result = byKind(GRAPH, 'strategy', NODES)
278
- expect(result).toHaveLength(1)
279
- expect(result[0].id).toBe('knowledge.strategy-b')
280
- })
281
-
282
- it('returns reference nodes', () => {
283
- const result = byKind(GRAPH, 'reference', NODES)
284
- expect(result).toHaveLength(1)
285
- expect(result[0].id).toBe('knowledge.reference-c')
286
- })
287
-
288
- it('returns empty array when no nodes match the kind', () => {
289
- const result = byKind(GRAPH, 'playbook', [])
290
- expect(result).toHaveLength(0)
291
- })
269
+ it('returns all playbook nodes', () => {
270
+ const result = byKind(GRAPH, 'playbook', NODES)
271
+ expect(result).toHaveLength(2)
272
+ expect(result.every((n) => n.kind === 'playbook')).toBe(true)
273
+ })
274
+
275
+ it('returns strategy nodes', () => {
276
+ const result = byKind(GRAPH, 'strategy', NODES)
277
+ expect(result).toHaveLength(1)
278
+ expect(result[0].id).toBe('knowledge.strategy-b')
279
+ })
280
+
281
+ it('returns reference nodes', () => {
282
+ const result = byKind(GRAPH, 'reference', NODES)
283
+ expect(result).toHaveLength(1)
284
+ expect(result[0].id).toBe('knowledge.reference-c')
285
+ })
286
+
287
+ it('returns empty array when no nodes match the kind', () => {
288
+ const result = byKind(GRAPH, 'playbook', [])
289
+ expect(result).toHaveLength(0)
290
+ })
292
291
  })
293
292
 
294
293
  // ---------------------------------------------------------------------------
295
294
  // byOwner
296
295
  // ---------------------------------------------------------------------------
297
-
298
- describe('byOwner', () => {
299
- it('returns nodes where ownerIds includes the given owner', () => {
300
- const result = byOwner(GRAPH, 'role.ops-lead', NODES)
301
- const ids = result.map((n) => n.id).sort()
302
- expect(ids).toEqual(['knowledge.playbook-d', 'knowledge.strategy-b'].sort())
303
- })
304
-
305
- it('returns nodes for a second owner', () => {
306
- const result = byOwner(GRAPH, 'role.ceo', NODES)
307
- expect(result).toHaveLength(1)
308
- expect(result[0].id).toBe('knowledge.playbook-d')
309
- })
310
-
311
- it('returns empty array when ownerId is not present in any node', () => {
312
- const result = byOwner(GRAPH, 'role.unknown', NODES)
313
- expect(result).toHaveLength(0)
314
- })
315
- })
316
-
317
- // ---------------------------------------------------------------------------
318
- // governs
319
- // ---------------------------------------------------------------------------
320
-
321
- describe('governs', () => {
322
- it('returns target graph node IDs for outgoing governs edges (OM node id input)', () => {
323
- const result = governs(GRAPH, 'knowledge.playbook-a')
324
- expect(result.sort()).toEqual(['system:sales.crm', 'system:sales.lead-gen'].sort())
325
- })
326
-
327
- it('accepts graph node ID format input', () => {
328
- const result = governs(GRAPH, 'knowledge:knowledge.playbook-a')
329
- expect(result.sort()).toEqual(['system:sales.crm', 'system:sales.lead-gen'].sort())
330
- })
331
-
332
- it('returns single target for a node with one link', () => {
333
- const result = governs(GRAPH, 'knowledge.strategy-b')
334
- expect(result).toEqual(['system:sales.lead-gen'])
335
- })
336
-
296
+
297
+ describe('byOwner', () => {
298
+ it('returns nodes where ownerIds includes the given owner', () => {
299
+ const result = byOwner(GRAPH, 'role.ops-lead', NODES)
300
+ const ids = result.map((n) => n.id).sort()
301
+ expect(ids).toEqual(['knowledge.playbook-d', 'knowledge.strategy-b'].sort())
302
+ })
303
+
304
+ it('returns nodes for a second owner', () => {
305
+ const result = byOwner(GRAPH, 'role.ceo', NODES)
306
+ expect(result).toHaveLength(1)
307
+ expect(result[0].id).toBe('knowledge.playbook-d')
308
+ })
309
+
310
+ it('returns empty array when ownerId is not present in any node', () => {
311
+ const result = byOwner(GRAPH, 'role.unknown', NODES)
312
+ expect(result).toHaveLength(0)
313
+ })
314
+ })
315
+
316
+ // ---------------------------------------------------------------------------
317
+ // governs
318
+ // ---------------------------------------------------------------------------
319
+
320
+ describe('governs', () => {
321
+ it('returns target graph node IDs for outgoing governs edges (OM node id input)', () => {
322
+ const result = governs(GRAPH, 'knowledge.playbook-a')
323
+ expect(result.sort()).toEqual(['system:sales.crm', 'system:sales.lead-gen'].sort())
324
+ })
325
+
326
+ it('accepts graph node ID format input', () => {
327
+ const result = governs(GRAPH, 'knowledge:knowledge.playbook-a')
328
+ expect(result.sort()).toEqual(['system:sales.crm', 'system:sales.lead-gen'].sort())
329
+ })
330
+
331
+ it('returns single target for a node with one link', () => {
332
+ const result = governs(GRAPH, 'knowledge.strategy-b')
333
+ expect(result).toEqual(['system:sales.lead-gen'])
334
+ })
335
+
337
336
  it('returns empty array for a node with no governs edges', () => {
338
337
  const result = governs(GRAPH, 'knowledge.reference-c')
339
338
  expect(result).toHaveLength(0)
340
339
  })
341
-
342
- it('returns empty array for an unknown nodeId', () => {
343
- const result = governs(GRAPH, 'knowledge.does-not-exist')
344
- expect(result).toHaveLength(0)
345
- })
346
- })
347
-
348
- // ---------------------------------------------------------------------------
349
- // governedBy
350
- // ---------------------------------------------------------------------------
351
-
352
- describe('governedBy', () => {
353
- it('returns knowledge graph node IDs for incoming governs edges (prefixed system id)', () => {
354
- const result = governedBy(GRAPH, 'system:sales.lead-gen')
355
- const sorted = result.sort()
356
- expect(sorted).toEqual(['knowledge:knowledge.playbook-a', 'knowledge:knowledge.strategy-b'].sort())
357
- })
358
-
359
- it('accepts bare system id (auto-prefixes with system:)', () => {
360
- const result = governedBy(GRAPH, 'sales.crm')
361
- expect(result).toEqual(['knowledge:knowledge.playbook-a'])
362
- })
363
-
364
- it('returns empty array when no knowledge node governs the target', () => {
365
- const result = governedBy(GRAPH, 'system:dashboard')
366
- expect(result).toHaveLength(0)
367
- })
368
-
340
+
341
+ it('returns empty array for an unknown nodeId', () => {
342
+ const result = governs(GRAPH, 'knowledge.does-not-exist')
343
+ expect(result).toHaveLength(0)
344
+ })
345
+ })
346
+
347
+ // ---------------------------------------------------------------------------
348
+ // governedBy
349
+ // ---------------------------------------------------------------------------
350
+
351
+ describe('governedBy', () => {
352
+ it('returns knowledge graph node IDs for incoming governs edges (prefixed system id)', () => {
353
+ const result = governedBy(GRAPH, 'system:sales.lead-gen')
354
+ const sorted = result.sort()
355
+ expect(sorted).toEqual(['knowledge:knowledge.playbook-a', 'knowledge:knowledge.strategy-b'].sort())
356
+ })
357
+
358
+ it('accepts bare system id (auto-prefixes with system:)', () => {
359
+ const result = governedBy(GRAPH, 'sales.crm')
360
+ expect(result).toEqual(['knowledge:knowledge.playbook-a'])
361
+ })
362
+
363
+ it('returns empty array when no knowledge node governs the target', () => {
364
+ const result = governedBy(GRAPH, 'system:dashboard')
365
+ expect(result).toHaveLength(0)
366
+ })
367
+
369
368
  it('returns empty array for unknown target node id', () => {
370
369
  const result = governedBy(GRAPH, 'system:does-not-exist')
371
370
  expect(result).toHaveLength(0)
372
371
  })
373
372
  })
374
-
375
- // ---------------------------------------------------------------------------
376
- // parsePath — happy paths (all five mount axes)
377
- // ---------------------------------------------------------------------------
378
-
379
- describe('parsePath — happy paths', () => {
380
- it('/by-kind/playbook → mount:by-kind args:[playbook]', () => {
381
- expect(parsePath('/by-kind/playbook')).toEqual({ mount: 'by-kind', args: ['playbook'] })
382
- })
383
-
384
- it('/by-kind/strategy → mount:by-kind args:[strategy]', () => {
385
- expect(parsePath('/by-kind/strategy')).toEqual({ mount: 'by-kind', args: ['strategy'] })
386
- })
387
-
388
- it('/by-system/sales.crm → mount:by-system args:[sales.crm]', () => {
389
- expect(parsePath('/by-system/sales.crm')).toEqual({ mount: 'by-system', args: ['sales.crm'] })
390
- })
391
-
392
- it('/by-system/sales/crm (slash-delimited) → mount:by-system args:[sales/crm]', () => {
393
- // systemId with slashes is joined and passed as-is for the caller to interpret
394
- expect(parsePath('/by-system/sales/crm')).toEqual({ mount: 'by-system', args: ['sales/crm'] })
395
- })
396
-
373
+
374
+ // ---------------------------------------------------------------------------
375
+ // parsePath — happy paths (all five mount axes)
376
+ // ---------------------------------------------------------------------------
377
+
378
+ describe('parsePath — happy paths', () => {
379
+ it('/by-kind/playbook → mount:by-kind args:[playbook]', () => {
380
+ expect(parsePath('/by-kind/playbook')).toEqual({ mount: 'by-kind', args: ['playbook'] })
381
+ })
382
+
383
+ it('/by-kind/strategy → mount:by-kind args:[strategy]', () => {
384
+ expect(parsePath('/by-kind/strategy')).toEqual({ mount: 'by-kind', args: ['strategy'] })
385
+ })
386
+
387
+ it('/by-system/sales.crm → mount:by-system args:[sales.crm]', () => {
388
+ expect(parsePath('/by-system/sales.crm')).toEqual({ mount: 'by-system', args: ['sales.crm'] })
389
+ })
390
+
391
+ it('/by-system/sales/crm (slash-delimited) → mount:by-system args:[sales/crm]', () => {
392
+ // systemId with slashes is joined and passed as-is for the caller to interpret
393
+ expect(parsePath('/by-system/sales/crm')).toEqual({ mount: 'by-system', args: ['sales/crm'] })
394
+ })
395
+
397
396
  it('/by-owner/role.ops-lead → mount:by-owner args:[role.ops-lead]', () => {
398
397
  expect(parsePath('/by-owner/role.ops-lead')).toEqual({ mount: 'by-owner', args: ['role.ops-lead'] })
399
398
  })
@@ -406,280 +405,695 @@ describe('parsePath — happy paths', () => {
406
405
  })
407
406
 
408
407
  it('/graph/knowledge.outreach-playbook/governs → mount:graph args:[knowledge.outreach-playbook,governs]', () => {
409
- expect(parsePath('/graph/knowledge.outreach-playbook/governs')).toEqual({
410
- mount: 'graph',
411
- args: ['knowledge.outreach-playbook', 'governs']
412
- })
413
- })
414
-
415
- it('/graph/knowledge.outreach-playbook/governed-by → mount:graph args:[knowledge.outreach-playbook,governed-by]', () => {
416
- expect(parsePath('/graph/knowledge.outreach-playbook/governed-by')).toEqual({
417
- mount: 'graph',
418
- args: ['knowledge.outreach-playbook', 'governed-by']
419
- })
420
- })
421
-
422
- it('/<nodeId> single segment → mount:node args:[nodeId]', () => {
423
- expect(parsePath('/knowledge.outreach-playbook')).toEqual({
424
- mount: 'node',
425
- args: ['knowledge.outreach-playbook']
426
- })
427
- })
428
-
429
- it('trailing slash is stripped before parsing', () => {
430
- expect(parsePath('/by-kind/playbook/')).toEqual({ mount: 'by-kind', args: ['playbook'] })
431
- })
432
- })
433
-
434
- // ---------------------------------------------------------------------------
435
- // parsePath — invalid inputs (5+)
436
- // ---------------------------------------------------------------------------
437
-
438
- describe('parsePath — invalid inputs', () => {
439
- it('throws on empty string', () => {
440
- expect(() => parsePath('')).toThrow('parsePath: path must be a non-empty string')
441
- })
442
-
443
- it('throws when path does not start with /', () => {
444
- expect(() => parsePath('by-kind/playbook')).toThrow('parsePath: path must start with "/"')
445
- })
446
-
447
- it('throws on bare root path "/"', () => {
448
- expect(() => parsePath('/')).toThrow('parsePath: path resolves to root with no mount')
449
- })
450
-
451
- it('throws on /by-system with no systemId argument', () => {
452
- expect(() => parsePath('/by-system')).toThrow('/by-system requires a systemId argument')
453
- })
454
-
455
- it('throws on /by-kind with no kind argument', () => {
456
- expect(() => parsePath('/by-kind')).toThrow('/by-kind requires a kind argument')
457
- })
458
-
459
- it('throws on /by-owner with no ownerId argument', () => {
460
- expect(() => parsePath('/by-owner')).toThrow('/by-owner requires an ownerId argument')
461
- })
462
-
463
- it('throws on /graph with only nodeId (missing verb)', () => {
464
- expect(() => parsePath('/graph/knowledge.outreach-playbook')).toThrow('/graph requires <nodeId>/<verb>')
465
- })
466
-
467
- it('throws on /graph/<nodeId>/<unknown-verb>', () => {
468
- expect(() => parsePath('/graph/knowledge.outreach-playbook/list')).toThrow(
469
- 'verb must be "governs" or "governed-by"'
470
- )
471
- })
472
-
473
- it('throws on multi-segment path with unrecognized first segment', () => {
474
- expect(() => parsePath('/unknown-mount/some-arg')).toThrow('unrecognized path pattern')
475
- })
476
- })
477
-
478
- // ---------------------------------------------------------------------------
479
- // parsePath — edge cases
480
- // ---------------------------------------------------------------------------
481
-
482
- describe('parsePath edge cases', () => {
483
- // Whitespace handling — the parser does NOT trim the incoming string.
484
- // A path with leading whitespace does not start with '/' so it throws.
485
- it('throws on path with leading whitespace', () => {
486
- expect(() => parsePath(' /by-kind/playbook')).toThrow('parsePath: path must start with "/"')
487
- })
488
-
489
- // Trailing whitespace becomes part of the last segment (not stripped).
490
- // The segment is non-empty so parsing proceeds and the whitespace is
491
- // included verbatim in the returned arg.
492
- it('preserves trailing whitespace inside the last segment', () => {
493
- const result = parsePath('/by-kind/playbook ')
494
- expect(result.mount).toBe('by-kind')
495
- expect(result.args[0]).toBe('playbook ')
496
- })
497
-
498
- // Multiple trailing slashes are all stripped (regex /\/+$/).
499
- it('strips multiple trailing slashes', () => {
500
- expect(parsePath('/by-kind/playbook///')).toEqual({ mount: 'by-kind', args: ['playbook'] })
501
- })
502
-
503
- // Lone-slash variants — a path of only slashes normalises to an empty
504
- // segments list and must throw the root-with-no-mount error.
505
- it('throws on path consisting only of slashes', () => {
506
- expect(() => parsePath('///')).toThrow('parsePath: path resolves to root with no mount')
507
- })
508
-
509
- // Case sensitivity — parsePath does not normalise case.
510
- // 'Playbook' (capitalised) is passed through as-is to the caller.
511
- it('preserves node-id case (no case normalisation)', () => {
512
- // /by-kind takes rest[0] verbatim
513
- const result = parsePath('/by-kind/Playbook')
514
- expect(result.args[0]).toBe('Playbook')
515
- })
516
-
517
- // system id: dot notation passes through unchanged.
518
- it('/by-system/sales.crm preserves dot notation', () => {
519
- expect(parsePath('/by-system/sales.crm')).toEqual({ mount: 'by-system', args: ['sales.crm'] })
520
- })
521
-
522
- // system id: slash notation joins rest segments with '/'.
523
- // The parser does NOT convert slashes to dots — the caller must normalise.
524
- it('/by-system/sales/crm joins segments with slash (no dot conversion)', () => {
525
- const result = parsePath('/by-system/sales/crm')
526
- expect(result.mount).toBe('by-system')
527
- // args[0] is 'sales/crm', NOT 'sales.crm'
528
- expect(result.args[0]).toBe('sales/crm')
529
- })
530
-
531
- // /by-kind silently ignores extra path segments beyond the kind argument.
532
- it('/by-kind/playbook/extra silently uses only first kind segment', () => {
533
- // First segment after 'by-kind' is taken; extra is ignored.
534
- const result = parsePath('/by-kind/playbook/extra')
535
- expect(result.mount).toBe('by-kind')
536
- expect(result.args[0]).toBe('playbook')
537
- expect(result.args).toHaveLength(1)
538
- })
539
-
540
- // /by-owner joins rest with '/' (same behaviour as /by-system).
541
- it('/by-owner/role.ops-lead/sub joins segments with slash', () => {
542
- const result = parsePath('/by-owner/role.ops-lead/sub')
543
- expect(result.mount).toBe('by-owner')
544
- expect(result.args[0]).toBe('role.ops-lead/sub')
545
- })
546
-
547
- // Graph node ID may contain a colon prefix (e.g. knowledge:knowledge.foo).
548
- // The parser accepts it — the rest before the verb is joined with '/'.
549
- it('/graph/<prefixed-nodeId>/governs round-trips the prefixed id', () => {
550
- const result = parsePath('/graph/knowledge:knowledge.test-node/governs')
551
- expect(result.mount).toBe('graph')
552
- expect(result.args[0]).toBe('knowledge:knowledge.test-node')
553
- expect(result.args[1]).toBe('governs')
554
- })
555
-
556
- // /graph with no arguments at all (just '/graph').
557
- it('throws on /graph with no nodeId or verb', () => {
558
- expect(() => parsePath('/graph')).toThrow('/graph requires <nodeId>/<verb>')
559
- })
560
-
561
- // /node mount — single-segment path works for any non-reserved first segment.
562
- it('single-segment non-reserved path is treated as node mount', () => {
563
- expect(parsePath('/knowledge.my-doc')).toEqual({ mount: 'node', args: ['knowledge.my-doc'] })
564
- })
565
- })
566
-
567
- // ---------------------------------------------------------------------------
568
- // formatText
569
- // ---------------------------------------------------------------------------
570
-
571
- describe('formatText', () => {
572
- it('returns "(no results)" for empty array', () => {
573
- expect(formatText([])).toBe('(no results)')
574
- })
575
-
576
- it('includes header row with KIND and ID columns', () => {
577
- const output = formatText([NODES[0]])
578
- expect(output).toContain('KIND')
579
- expect(output).toContain('ID')
580
- expect(output).toContain('TITLE')
581
- })
582
-
583
- it('includes node id and title in output', () => {
584
- const output = formatText([NODES[0]])
585
- expect(output).toContain('knowledge.playbook-a')
586
- expect(output).toContain('Playbook A')
587
- })
588
-
589
- it('truncates long summaries', () => {
590
- const longSummary = 'x'.repeat(200)
591
- const node: OrgKnowledgeNode = { ...NODES[0], summary: longSummary }
592
- const output = formatText([node])
593
- expect(output).toContain('...')
594
- })
595
- })
596
-
597
- // ---------------------------------------------------------------------------
598
- // formatJson — envelope shape
599
- // ---------------------------------------------------------------------------
600
-
601
- describe('formatJson', () => {
602
- it('returns a JSON string with top-level keys: path, mount, args, results', () => {
603
- const raw = formatJson({
604
- path: '/by-kind/playbook',
605
- parsed: { mount: 'by-kind', args: ['playbook'] },
606
- results: [NODES[0]]
607
- })
608
- const parsed = JSON.parse(raw) as Record<string, unknown>
609
- expect(Object.keys(parsed).sort()).toEqual(['args', 'mount', 'path', 'results'].sort())
610
- })
611
-
612
- it('preserves path in envelope', () => {
613
- const raw = formatJson({
614
- path: '/by-kind/playbook',
615
- parsed: { mount: 'by-kind', args: ['playbook'] },
616
- results: []
617
- })
618
- expect(JSON.parse(raw)).toMatchObject({ path: '/by-kind/playbook' })
619
- })
620
-
621
- it('preserves mount in envelope', () => {
622
- const raw = formatJson({
623
- path: '/by-kind/strategy',
624
- parsed: { mount: 'by-kind', args: ['strategy'] },
625
- results: []
626
- })
627
- expect(JSON.parse(raw)).toMatchObject({ mount: 'by-kind' })
628
- })
629
-
630
- it('preserves args array in envelope', () => {
631
- const raw = formatJson({
632
- path: '/by-system/sales.crm',
633
- parsed: { mount: 'by-system', args: ['sales.crm'] },
634
- results: []
635
- })
636
- expect(JSON.parse(raw)).toMatchObject({ args: ['sales.crm'] })
637
- })
638
-
639
- it('includes results in envelope for string[] (governs/governedBy)', () => {
640
- const raw = formatJson({
641
- path: '/graph/knowledge.playbook-a/governs',
642
- parsed: { mount: 'graph', args: ['knowledge.playbook-a', 'governs'] },
643
- results: ['system:sales.crm', 'system:sales.lead-gen']
644
- })
645
- const parsed = JSON.parse(raw) as { results: string[] }
646
- expect(parsed.results).toEqual(['system:sales.crm', 'system:sales.lead-gen'])
647
- })
648
-
649
- it('envelope is NOT flat { results } — must have path/mount/args siblings', () => {
650
- const raw = formatJson({
651
- path: '/by-kind/playbook',
652
- parsed: { mount: 'by-kind', args: ['playbook'] },
653
- results: [NODES[0]]
654
- })
655
- const parsed = JSON.parse(raw) as Record<string, unknown>
656
- // Must NOT be flat (only results key)
657
- expect(parsed).toHaveProperty('path')
658
- expect(parsed).toHaveProperty('mount')
659
- expect(parsed).toHaveProperty('args')
660
- expect(parsed).toHaveProperty('results')
661
- })
662
- })
663
-
664
- // ---------------------------------------------------------------------------
665
- // formatIdsOnly
666
- // ---------------------------------------------------------------------------
667
-
668
- describe('formatIdsOnly', () => {
669
- it('returns empty string for empty array', () => {
670
- expect(formatIdsOnly([])).toBe('')
671
- })
672
-
673
- it('returns newline-separated ids for OrgKnowledgeNode array', () => {
674
- const output = formatIdsOnly([NODES[0], NODES[1]])
675
- const lines = output.split('\n')
676
- expect(lines).toContain('knowledge.playbook-a')
677
- expect(lines).toContain('knowledge.strategy-b')
678
- })
679
-
680
- it('returns newline-separated strings for string[] (governs results)', () => {
681
- const output = formatIdsOnly(['system:sales.crm', 'system:sales.lead-gen'])
682
- expect(output).toBe('system:sales.crm\nsystem:sales.lead-gen')
683
- })
684
- })
685
-
408
+ expect(parsePath('/graph/knowledge.outreach-playbook/governs')).toEqual({
409
+ mount: 'graph',
410
+ args: ['knowledge.outreach-playbook', 'governs']
411
+ })
412
+ })
413
+
414
+ it('/graph/knowledge.outreach-playbook/governed-by → mount:graph args:[knowledge.outreach-playbook,governed-by]', () => {
415
+ expect(parsePath('/graph/knowledge.outreach-playbook/governed-by')).toEqual({
416
+ mount: 'graph',
417
+ args: ['knowledge.outreach-playbook', 'governed-by']
418
+ })
419
+ })
420
+
421
+ it('/<nodeId> single segment → mount:node args:[nodeId]', () => {
422
+ expect(parsePath('/knowledge.outreach-playbook')).toEqual({
423
+ mount: 'node',
424
+ args: ['knowledge.outreach-playbook']
425
+ })
426
+ })
427
+
428
+ it('trailing slash is stripped before parsing', () => {
429
+ expect(parsePath('/by-kind/playbook/')).toEqual({ mount: 'by-kind', args: ['playbook'] })
430
+ })
431
+ })
432
+
433
+ // ---------------------------------------------------------------------------
434
+ // parsePath — invalid inputs (5+)
435
+ // ---------------------------------------------------------------------------
436
+
437
+ describe('parsePath — invalid inputs', () => {
438
+ it('throws on empty string', () => {
439
+ expect(() => parsePath('')).toThrow('parsePath: path must be a non-empty string')
440
+ })
441
+
442
+ it('throws when path does not start with /', () => {
443
+ expect(() => parsePath('by-kind/playbook')).toThrow('parsePath: path must start with "/"')
444
+ })
445
+
446
+ it('throws on bare root path "/"', () => {
447
+ expect(() => parsePath('/')).toThrow('parsePath: path resolves to root with no mount')
448
+ })
449
+
450
+ it('throws on /by-system with no systemId argument', () => {
451
+ expect(() => parsePath('/by-system')).toThrow('/by-system requires a systemId argument')
452
+ })
453
+
454
+ it('throws on /by-kind with no kind argument', () => {
455
+ expect(() => parsePath('/by-kind')).toThrow('/by-kind requires a kind argument')
456
+ })
457
+
458
+ it('throws on /by-owner with no ownerId argument', () => {
459
+ expect(() => parsePath('/by-owner')).toThrow('/by-owner requires an ownerId argument')
460
+ })
461
+
462
+ it('throws on /graph with only nodeId (missing verb)', () => {
463
+ expect(() => parsePath('/graph/knowledge.outreach-playbook')).toThrow('/graph requires <nodeId>/<verb>')
464
+ })
465
+
466
+ it('throws on /graph/<nodeId>/<unknown-verb>', () => {
467
+ expect(() => parsePath('/graph/knowledge.outreach-playbook/list')).toThrow(
468
+ 'verb must be "governs" or "governed-by"'
469
+ )
470
+ })
471
+
472
+ it('throws on multi-segment path with unrecognized first segment', () => {
473
+ expect(() => parsePath('/unknown-mount/some-arg')).toThrow('unrecognized path pattern')
474
+ })
475
+ })
476
+
477
+ // ---------------------------------------------------------------------------
478
+ // parsePath — edge cases
479
+ // ---------------------------------------------------------------------------
480
+
481
+ describe('parsePath edge cases', () => {
482
+ // Whitespace handling — the parser does NOT trim the incoming string.
483
+ // A path with leading whitespace does not start with '/' so it throws.
484
+ it('throws on path with leading whitespace', () => {
485
+ expect(() => parsePath(' /by-kind/playbook')).toThrow('parsePath: path must start with "/"')
486
+ })
487
+
488
+ // Trailing whitespace becomes part of the last segment (not stripped).
489
+ // The segment is non-empty so parsing proceeds and the whitespace is
490
+ // included verbatim in the returned arg.
491
+ it('preserves trailing whitespace inside the last segment', () => {
492
+ const result = parsePath('/by-kind/playbook ')
493
+ expect(result.mount).toBe('by-kind')
494
+ expect(result.args[0]).toBe('playbook ')
495
+ })
496
+
497
+ // Multiple trailing slashes are all stripped (regex /\/+$/).
498
+ it('strips multiple trailing slashes', () => {
499
+ expect(parsePath('/by-kind/playbook///')).toEqual({ mount: 'by-kind', args: ['playbook'] })
500
+ })
501
+
502
+ // Lone-slash variants — a path of only slashes normalises to an empty
503
+ // segments list and must throw the root-with-no-mount error.
504
+ it('throws on path consisting only of slashes', () => {
505
+ expect(() => parsePath('///')).toThrow('parsePath: path resolves to root with no mount')
506
+ })
507
+
508
+ // Case sensitivity — parsePath does not normalise case.
509
+ // 'Playbook' (capitalised) is passed through as-is to the caller.
510
+ it('preserves node-id case (no case normalisation)', () => {
511
+ // /by-kind takes rest[0] verbatim
512
+ const result = parsePath('/by-kind/Playbook')
513
+ expect(result.args[0]).toBe('Playbook')
514
+ })
515
+
516
+ // system id: dot notation passes through unchanged.
517
+ it('/by-system/sales.crm preserves dot notation', () => {
518
+ expect(parsePath('/by-system/sales.crm')).toEqual({ mount: 'by-system', args: ['sales.crm'] })
519
+ })
520
+
521
+ // system id: slash notation joins rest segments with '/'.
522
+ // The parser does NOT convert slashes to dots — the caller must normalise.
523
+ it('/by-system/sales/crm joins segments with slash (no dot conversion)', () => {
524
+ const result = parsePath('/by-system/sales/crm')
525
+ expect(result.mount).toBe('by-system')
526
+ // args[0] is 'sales/crm', NOT 'sales.crm'
527
+ expect(result.args[0]).toBe('sales/crm')
528
+ })
529
+
530
+ // /by-kind silently ignores extra path segments beyond the kind argument.
531
+ it('/by-kind/playbook/extra silently uses only first kind segment', () => {
532
+ // First segment after 'by-kind' is taken; extra is ignored.
533
+ const result = parsePath('/by-kind/playbook/extra')
534
+ expect(result.mount).toBe('by-kind')
535
+ expect(result.args[0]).toBe('playbook')
536
+ expect(result.args).toHaveLength(1)
537
+ })
538
+
539
+ // /by-owner joins rest with '/' (same behaviour as /by-system).
540
+ it('/by-owner/role.ops-lead/sub joins segments with slash', () => {
541
+ const result = parsePath('/by-owner/role.ops-lead/sub')
542
+ expect(result.mount).toBe('by-owner')
543
+ expect(result.args[0]).toBe('role.ops-lead/sub')
544
+ })
545
+
546
+ // Graph node ID may contain a colon prefix (e.g. knowledge:knowledge.foo).
547
+ // The parser accepts it — the rest before the verb is joined with '/'.
548
+ it('/graph/<prefixed-nodeId>/governs round-trips the prefixed id', () => {
549
+ const result = parsePath('/graph/knowledge:knowledge.test-node/governs')
550
+ expect(result.mount).toBe('graph')
551
+ expect(result.args[0]).toBe('knowledge:knowledge.test-node')
552
+ expect(result.args[1]).toBe('governs')
553
+ })
554
+
555
+ // /graph with no arguments at all (just '/graph').
556
+ it('throws on /graph with no nodeId or verb', () => {
557
+ expect(() => parsePath('/graph')).toThrow('/graph requires <nodeId>/<verb>')
558
+ })
559
+
560
+ // /node mount — single-segment path works for any non-reserved first segment.
561
+ it('single-segment non-reserved path is treated as node mount', () => {
562
+ expect(parsePath('/knowledge.my-doc')).toEqual({ mount: 'node', args: ['knowledge.my-doc'] })
563
+ })
564
+ })
565
+
566
+ // ---------------------------------------------------------------------------
567
+ // formatText
568
+ // ---------------------------------------------------------------------------
569
+
570
+ describe('formatText', () => {
571
+ it('returns "(no results)" for empty array', () => {
572
+ expect(formatText([])).toBe('(no results)')
573
+ })
574
+
575
+ it('includes header row with KIND and ID columns', () => {
576
+ const output = formatText([NODES[0]])
577
+ expect(output).toContain('KIND')
578
+ expect(output).toContain('ID')
579
+ expect(output).toContain('TITLE')
580
+ })
581
+
582
+ it('includes node id and title in output', () => {
583
+ const output = formatText([NODES[0]])
584
+ expect(output).toContain('knowledge.playbook-a')
585
+ expect(output).toContain('Playbook A')
586
+ })
587
+
588
+ it('truncates long summaries', () => {
589
+ const longSummary = 'x'.repeat(200)
590
+ const node: OrgKnowledgeNode = { ...NODES[0], summary: longSummary }
591
+ const output = formatText([node])
592
+ expect(output).toContain('...')
593
+ })
594
+ })
595
+
596
+ // ---------------------------------------------------------------------------
597
+ // formatJson — envelope shape
598
+ // ---------------------------------------------------------------------------
599
+
600
+ describe('formatJson', () => {
601
+ it('returns a JSON string with top-level keys: path, mount, args, results', () => {
602
+ const raw = formatJson({
603
+ path: '/by-kind/playbook',
604
+ parsed: { mount: 'by-kind', args: ['playbook'] },
605
+ results: [NODES[0]]
606
+ })
607
+ const parsed = JSON.parse(raw) as Record<string, unknown>
608
+ expect(Object.keys(parsed).sort()).toEqual(['args', 'mount', 'path', 'results'].sort())
609
+ })
610
+
611
+ it('preserves path in envelope', () => {
612
+ const raw = formatJson({
613
+ path: '/by-kind/playbook',
614
+ parsed: { mount: 'by-kind', args: ['playbook'] },
615
+ results: []
616
+ })
617
+ expect(JSON.parse(raw)).toMatchObject({ path: '/by-kind/playbook' })
618
+ })
619
+
620
+ it('preserves mount in envelope', () => {
621
+ const raw = formatJson({
622
+ path: '/by-kind/strategy',
623
+ parsed: { mount: 'by-kind', args: ['strategy'] },
624
+ results: []
625
+ })
626
+ expect(JSON.parse(raw)).toMatchObject({ mount: 'by-kind' })
627
+ })
628
+
629
+ it('preserves args array in envelope', () => {
630
+ const raw = formatJson({
631
+ path: '/by-system/sales.crm',
632
+ parsed: { mount: 'by-system', args: ['sales.crm'] },
633
+ results: []
634
+ })
635
+ expect(JSON.parse(raw)).toMatchObject({ args: ['sales.crm'] })
636
+ })
637
+
638
+ it('includes results in envelope for string[] (governs/governedBy)', () => {
639
+ const raw = formatJson({
640
+ path: '/graph/knowledge.playbook-a/governs',
641
+ parsed: { mount: 'graph', args: ['knowledge.playbook-a', 'governs'] },
642
+ results: ['system:sales.crm', 'system:sales.lead-gen']
643
+ })
644
+ const parsed = JSON.parse(raw) as { results: string[] }
645
+ expect(parsed.results).toEqual(['system:sales.crm', 'system:sales.lead-gen'])
646
+ })
647
+
648
+ it('envelope is NOT flat { results } — must have path/mount/args siblings', () => {
649
+ const raw = formatJson({
650
+ path: '/by-kind/playbook',
651
+ parsed: { mount: 'by-kind', args: ['playbook'] },
652
+ results: [NODES[0]]
653
+ })
654
+ const parsed = JSON.parse(raw) as Record<string, unknown>
655
+ // Must NOT be flat (only results key)
656
+ expect(parsed).toHaveProperty('path')
657
+ expect(parsed).toHaveProperty('mount')
658
+ expect(parsed).toHaveProperty('args')
659
+ expect(parsed).toHaveProperty('results')
660
+ })
661
+ })
662
+
663
+ // ---------------------------------------------------------------------------
664
+ // formatIdsOnly
665
+ // ---------------------------------------------------------------------------
666
+
667
+ describe('formatIdsOnly', () => {
668
+ it('returns empty string for empty array', () => {
669
+ expect(formatIdsOnly([])).toBe('')
670
+ })
671
+
672
+ it('returns newline-separated ids for OrgKnowledgeNode array', () => {
673
+ const output = formatIdsOnly([NODES[0], NODES[1]])
674
+ const lines = output.split('\n')
675
+ expect(lines).toContain('knowledge.playbook-a')
676
+ expect(lines).toContain('knowledge.strategy-b')
677
+ })
678
+
679
+ it('returns newline-separated strings for string[] (governs results)', () => {
680
+ const output = formatIdsOnly(['system:sales.crm', 'system:sales.lead-gen'])
681
+ expect(output).toBe('system:sales.crm\nsystem:sales.lead-gen')
682
+ })
683
+ })
684
+
685
+ // ---------------------------------------------------------------------------
686
+ // omSearch — universal keyword search across OM surfaces
687
+ // ---------------------------------------------------------------------------
688
+
689
+ /**
690
+ * Minimal synthetic OrganizationModel for omSearch tests.
691
+ *
692
+ * The omSearch implementation is defensive (uses `?? {}` everywhere), so we
693
+ * only need to populate the surfaces we want to assert on. Cast through
694
+ * `unknown` because we are deliberately omitting domain fields.
695
+ */
696
+ const OM_SEARCH_MODEL = {
697
+ knowledge: {
698
+ 'knowledge.lead-gen-strategy': {
699
+ id: 'knowledge.lead-gen-strategy',
700
+ kind: 'strategy',
701
+ title: 'Lead Gen Strategy',
702
+ summary: 'ICP scoring and targeting criteria for outbound prospecting.',
703
+ body: '## Lead Gen Strategy\n\nDetailed body about lead gen.',
704
+ links: [],
705
+ ownerIds: ['role.growth-operator'],
706
+ updatedAt: '2026-01-01'
707
+ },
708
+ 'knowledge.outreach-playbook': {
709
+ id: 'knowledge.outreach-playbook',
710
+ kind: 'playbook',
711
+ title: 'Outreach Sequence Playbook',
712
+ summary: 'End-to-end cold outreach including warmup and reply handling.',
713
+ body: '## Outreach\n\nBody mentions lead-gen and warmup repeatedly.\nLine 3.\nLine 4.\nLine 5.\nLine 6.\nLine 7.',
714
+ links: [{ nodeId: 'system:sales.lead-gen' }, { nodeId: 'system:sales.crm' }],
715
+ ownerIds: ['role.growth-operator'],
716
+ updatedAt: '2026-02-01'
717
+ }
718
+ },
719
+ systems: {
720
+ sales: {
721
+ id: 'sales',
722
+ order: 10,
723
+ label: 'Sales',
724
+ description: 'Revenue workflows and customer acquisition.',
725
+ kind: 'operational',
726
+ lifecycle: 'active',
727
+ systems: {
728
+ 'lead-gen': {
729
+ id: 'sales.lead-gen',
730
+ order: 20,
731
+ label: 'Lead Gen',
732
+ description: 'Prospecting, qualification, and outreach preparation.',
733
+ kind: 'operational',
734
+ parentSystemId: 'sales',
735
+ lifecycle: 'active'
736
+ },
737
+ crm: {
738
+ id: 'sales.crm',
739
+ order: 30,
740
+ label: 'CRM',
741
+ description: 'Customer relationship management and deal pipeline.',
742
+ kind: 'operational',
743
+ parentSystemId: 'sales',
744
+ lifecycle: 'active'
745
+ }
746
+ }
747
+ },
748
+ seo: {
749
+ id: 'seo',
750
+ order: 40,
751
+ label: 'SEO',
752
+ description: 'Search engine optimization and inbound content.',
753
+ kind: 'operational',
754
+ lifecycle: 'active'
755
+ }
756
+ },
757
+ resources: {
758
+ 'lgn-apify-discovery-workflow': {
759
+ id: 'lgn-apify-discovery-workflow',
760
+ kind: 'workflow',
761
+ systemPath: 'sales.lead-gen',
762
+ title: 'Apify Discovery Workflow',
763
+ description: 'Discovers prospects via Apify website crawl.',
764
+ status: 'active',
765
+ order: 10,
766
+ codeRefs: []
767
+ },
768
+ 'integration-apollo-elevasis': {
769
+ id: 'integration-apollo-elevasis',
770
+ kind: 'integration',
771
+ systemPath: 'sales.lead-gen',
772
+ title: 'Apollo Integration',
773
+ description: 'Apollo contact and company data lookup.',
774
+ provider: 'apollo',
775
+ status: 'active',
776
+ order: 20,
777
+ codeRefs: []
778
+ }
779
+ },
780
+ roles: {
781
+ 'role.growth-operator': {
782
+ id: 'role.growth-operator',
783
+ order: 20,
784
+ title: 'Growth Operator',
785
+ responsibilities: ['Own lead generation, prospecting, outreach operations, and acquisition playbooks.'],
786
+ responsibleFor: ['sales.lead-gen']
787
+ },
788
+ 'role.crm-operator': {
789
+ id: 'role.crm-operator',
790
+ order: 30,
791
+ title: 'CRM Operator',
792
+ responsibilities: ['Own CRM pipeline workflows, inbound reply handling, and deal-stage actions.'],
793
+ responsibleFor: ['sales.crm']
794
+ }
795
+ },
796
+ policies: {
797
+ 'policy.approve-large-deals': {
798
+ id: 'policy.approve-large-deals',
799
+ order: 10,
800
+ label: 'Approve Large Deals',
801
+ description: 'Require approval before sending proposals above $10k.',
802
+ trigger: { kind: 'manual' },
803
+ predicate: { kind: 'always' },
804
+ actions: [{ kind: 'require-approval' }],
805
+ appliesTo: { systemIds: [], actionIds: [], resourceIds: [], roleIds: [] }
806
+ }
807
+ }
808
+ } as unknown as OrganizationModel
809
+
810
+ describe('omSearch', () => {
811
+ it('returns empty array for empty query', () => {
812
+ expect(omSearch(OM_SEARCH_MODEL, '')).toEqual([])
813
+ })
814
+
815
+ it('returns empty array for whitespace-only query', () => {
816
+ expect(omSearch(OM_SEARCH_MODEL, ' ')).toEqual([])
817
+ })
818
+
819
+ it('matches systems by label and ranks them by score', () => {
820
+ const hits = omSearch(OM_SEARCH_MODEL, 'lead gen')
821
+ expect(hits.length).toBeGreaterThan(0)
822
+ const topHit = hits[0]
823
+ expect(topHit.kind).toBe('system')
824
+ expect(topHit.id).toBe('sales.lead-gen')
825
+ })
826
+
827
+ it('finds knowledge nodes by title or summary', () => {
828
+ const hits = omSearch(OM_SEARCH_MODEL, 'outreach')
829
+ const ids = hits.map((h) => h.id)
830
+ expect(ids).toContain('knowledge.outreach-playbook')
831
+ })
832
+
833
+ it('finds resources by title', () => {
834
+ const hits = omSearch(OM_SEARCH_MODEL, 'apollo')
835
+ expect(hits.length).toBeGreaterThan(0)
836
+ expect(hits[0].id).toBe('integration-apollo-elevasis')
837
+ expect(hits[0].kind).toBe('resource')
838
+ })
839
+
840
+ it('finds roles by responsibilities', () => {
841
+ const hits = omSearch(OM_SEARCH_MODEL, 'crm operator')
842
+ const ids = hits.map((h) => h.id)
843
+ expect(ids).toContain('role.crm-operator')
844
+ })
845
+
846
+ it('finds policies by label and description', () => {
847
+ const hits = omSearch(OM_SEARCH_MODEL, 'approve large deals')
848
+ const ids = hits.map((h) => h.id)
849
+ expect(ids).toContain('policy.approve-large-deals')
850
+ })
851
+
852
+ it('is case-insensitive', () => {
853
+ const lower = omSearch(OM_SEARCH_MODEL, 'lead gen').map((h) => h.id)
854
+ const upper = omSearch(OM_SEARCH_MODEL, 'LEAD GEN').map((h) => h.id)
855
+ expect(lower).toEqual(upper)
856
+ })
857
+
858
+ it('returns hits ranked by score descending', () => {
859
+ const hits = omSearch(OM_SEARCH_MODEL, 'lead gen')
860
+ for (let i = 1; i < hits.length; i++) {
861
+ expect(hits[i - 1].score).toBeGreaterThanOrEqual(hits[i].score)
862
+ }
863
+ })
864
+
865
+ it('attaches subKind for typed surfaces', () => {
866
+ const hits = omSearch(OM_SEARCH_MODEL, 'apollo')
867
+ expect(hits[0].subKind).toBe('integration')
868
+ })
869
+
870
+ it('respects the limit option', () => {
871
+ const hits = omSearch(OM_SEARCH_MODEL, 'lead', { limit: 1 })
872
+ expect(hits).toHaveLength(1)
873
+ })
874
+
875
+ it('limit: 0 returns all matching hits', () => {
876
+ const limited = omSearch(OM_SEARCH_MODEL, 'lead', { limit: 1 })
877
+ const unlimited = omSearch(OM_SEARCH_MODEL, 'lead', { limit: 0 })
878
+ expect(unlimited.length).toBeGreaterThan(limited.length)
879
+ })
880
+
881
+ it('filters by kinds option', () => {
882
+ const only = omSearch(OM_SEARCH_MODEL, 'lead', { kinds: ['system'] })
883
+ expect(only.every((h) => h.kind === 'system')).toBe(true)
884
+ })
885
+
886
+ it('returns no hits when the query matches nothing', () => {
887
+ expect(omSearch(OM_SEARCH_MODEL, 'zzz-not-a-real-thing-xyz')).toHaveLength(0)
888
+ })
889
+
890
+ it('ranks an exact id match above partial matches', () => {
891
+ const hits = omSearch(OM_SEARCH_MODEL, 'sales.lead-gen')
892
+ expect(hits[0].id).toBe('sales.lead-gen')
893
+ expect(hits[0].kind).toBe('system')
894
+ })
895
+
896
+ it('ignores single-character tokens (noise filter)', () => {
897
+ // 'a' alone is filtered out by tokenize (length < 2); query collapses to empty
898
+ expect(omSearch(OM_SEARCH_MODEL, 'a')).toEqual([])
899
+ })
900
+ })
901
+
902
+ // ---------------------------------------------------------------------------
903
+ // formatOmSearchHits
904
+ // ---------------------------------------------------------------------------
905
+
906
+ describe('formatOmSearchHits', () => {
907
+ it('returns "(no results)" for empty array', () => {
908
+ expect(formatOmSearchHits([])).toBe('(no results)')
909
+ })
910
+
911
+ it('includes the kind bracket label for each hit', () => {
912
+ const hits = omSearch(OM_SEARCH_MODEL, 'lead gen')
913
+ const output = formatOmSearchHits(hits)
914
+ expect(output).toContain('[system')
915
+ })
916
+
917
+ it('includes the id and title for each hit', () => {
918
+ const hits = omSearch(OM_SEARCH_MODEL, 'lead gen')
919
+ const output = formatOmSearchHits(hits)
920
+ expect(output).toContain('sales.lead-gen')
921
+ expect(output).toContain('Lead Gen')
922
+ })
923
+
924
+ it('appends subKind to the bracket when present', () => {
925
+ const hits = omSearch(OM_SEARCH_MODEL, 'apollo')
926
+ const output = formatOmSearchHits(hits)
927
+ expect(output).toContain('[resource/integration]')
928
+ })
929
+ })
930
+
931
+ // ---------------------------------------------------------------------------
932
+ // omDescribe — neighborhood view for any OM node id
933
+ // ---------------------------------------------------------------------------
934
+
935
+ describe('omDescribe', () => {
936
+ it('returns undefined for an unknown id', () => {
937
+ expect(omDescribe(OM_SEARCH_MODEL, 'does-not-exist')).toBeUndefined()
938
+ })
939
+
940
+ describe('system', () => {
941
+ it('describes a system with parent, lifecycle, and description', () => {
942
+ const result = omDescribe(OM_SEARCH_MODEL, 'sales.lead-gen')
943
+ expect(result).toBeDefined()
944
+ if (result?.kind !== 'system') throw new Error('expected system')
945
+ expect(result.id).toBe('sales.lead-gen')
946
+ expect(result.label).toBe('Lead Gen')
947
+ expect(result.description).toContain('Prospecting')
948
+ expect(result.parentSystemId).toBe('sales')
949
+ expect(result.lifecycle).toBe('active')
950
+ })
951
+
952
+ it('counts resources by kind for the system', () => {
953
+ const result = omDescribe(OM_SEARCH_MODEL, 'sales.lead-gen')
954
+ if (result?.kind !== 'system') throw new Error('expected system')
955
+ expect(result.resourceCountsByKind).toEqual({ workflow: 1, integration: 1 })
956
+ expect(result.resourceIdsByKind.integration).toContain('integration-apollo-elevasis')
957
+ })
958
+
959
+ it('finds governing knowledge via knowledge.links', () => {
960
+ const result = omDescribe(OM_SEARCH_MODEL, 'sales.lead-gen')
961
+ if (result?.kind !== 'system') throw new Error('expected system')
962
+ expect(result.governingKnowledgeIds).toContain('knowledge.outreach-playbook')
963
+ })
964
+
965
+ it('lists immediate child systems only (not grandchildren)', () => {
966
+ const result = omDescribe(OM_SEARCH_MODEL, 'sales')
967
+ if (result?.kind !== 'system') throw new Error('expected system')
968
+ expect(result.childSystemPaths).toEqual(expect.arrayContaining(['sales.lead-gen', 'sales.crm']))
969
+ expect(result.childSystemPaths).toHaveLength(2)
970
+ })
971
+ })
972
+
973
+ describe('resource', () => {
974
+ it('describes a workflow resource with system and status', () => {
975
+ const result = omDescribe(OM_SEARCH_MODEL, 'lgn-apify-discovery-workflow')
976
+ expect(result).toBeDefined()
977
+ if (result?.kind !== 'resource') throw new Error('expected resource')
978
+ expect(result.resourceKind).toBe('workflow')
979
+ expect(result.systemPath).toBe('sales.lead-gen')
980
+ expect(result.title).toBe('Apify Discovery Workflow')
981
+ expect(result.status).toBe('active')
982
+ })
983
+
984
+ it('describes an integration resource', () => {
985
+ const result = omDescribe(OM_SEARCH_MODEL, 'integration-apollo-elevasis')
986
+ if (result?.kind !== 'resource') throw new Error('expected resource')
987
+ expect(result.resourceKind).toBe('integration')
988
+ })
989
+ })
990
+
991
+ describe('knowledge', () => {
992
+ it('describes a knowledge node with body excerpt and line count', () => {
993
+ const result = omDescribe(OM_SEARCH_MODEL, 'knowledge.outreach-playbook')
994
+ expect(result).toBeDefined()
995
+ if (result?.kind !== 'knowledge') throw new Error('expected knowledge')
996
+ expect(result.title).toBe('Outreach Sequence Playbook')
997
+ expect(result.knowledgeKind).toBe('playbook')
998
+ expect(result.bodyLineCount).toBeGreaterThan(0)
999
+ expect(result.bodyExcerpt).toContain('Outreach')
1000
+ })
1001
+
1002
+ it('exposes governs targets from links[].nodeId', () => {
1003
+ const result = omDescribe(OM_SEARCH_MODEL, 'knowledge.outreach-playbook')
1004
+ if (result?.kind !== 'knowledge') throw new Error('expected knowledge')
1005
+ expect(result.governs).toContain('system:sales.lead-gen')
1006
+ expect(result.governs).toContain('system:sales.crm')
1007
+ })
1008
+
1009
+ it('exposes ownerIds', () => {
1010
+ const result = omDescribe(OM_SEARCH_MODEL, 'knowledge.outreach-playbook')
1011
+ if (result?.kind !== 'knowledge') throw new Error('expected knowledge')
1012
+ expect(result.ownerIds).toContain('role.growth-operator')
1013
+ })
1014
+ })
1015
+
1016
+ describe('role', () => {
1017
+ it('describes a role with responsibilities and responsibleFor systems', () => {
1018
+ const result = omDescribe(OM_SEARCH_MODEL, 'role.growth-operator')
1019
+ expect(result).toBeDefined()
1020
+ if (result?.kind !== 'role') throw new Error('expected role')
1021
+ expect(result.title).toBe('Growth Operator')
1022
+ expect(result.responsibilities.length).toBeGreaterThan(0)
1023
+ expect(result.responsibleFor).toContain('sales.lead-gen')
1024
+ })
1025
+ })
1026
+
1027
+ describe('policy', () => {
1028
+ it('describes a policy with trigger kind and effects', () => {
1029
+ const result = omDescribe(OM_SEARCH_MODEL, 'policy.approve-large-deals')
1030
+ expect(result).toBeDefined()
1031
+ if (result?.kind !== 'policy') throw new Error('expected policy')
1032
+ expect(result.triggerKind).toBe('manual')
1033
+ expect(result.effectKinds).toContain('require-approval')
1034
+ })
1035
+ })
1036
+
1037
+ describe('kind detection', () => {
1038
+ it('detects ontology ids by :<kind>/ pattern', () => {
1039
+ // No ontology in fixture, but detection should classify the id correctly.
1040
+ // It returns undefined because the record can't be found, but classification reaches the
1041
+ // ontology branch (which then fails the lookup).
1042
+ expect(omDescribe(OM_SEARCH_MODEL, 'sales.lead-gen:object/list')).toBeUndefined()
1043
+ })
1044
+
1045
+ it('returns undefined when an id pattern matches but the entity is missing', () => {
1046
+ expect(omDescribe(OM_SEARCH_MODEL, 'knowledge.does-not-exist')).toBeUndefined()
1047
+ expect(omDescribe(OM_SEARCH_MODEL, 'role.does-not-exist')).toBeUndefined()
1048
+ expect(omDescribe(OM_SEARCH_MODEL, 'policy.does-not-exist')).toBeUndefined()
1049
+ })
1050
+ })
1051
+ })
1052
+
1053
+ // ---------------------------------------------------------------------------
1054
+ // formatOmDescribe
1055
+ // ---------------------------------------------------------------------------
1056
+
1057
+ describe('formatOmDescribe', () => {
1058
+ it('returns "(node not found)" for undefined', () => {
1059
+ expect(formatOmDescribe(undefined)).toBe('(node not found)')
1060
+ })
1061
+
1062
+ it('renders system header + sections', () => {
1063
+ const result = omDescribe(OM_SEARCH_MODEL, 'sales.lead-gen')
1064
+ const output = formatOmDescribe(result)
1065
+ expect(output).toContain('System: sales.lead-gen')
1066
+ expect(output).toContain('Label: Lead Gen')
1067
+ expect(output).toContain('Resources (2)')
1068
+ })
1069
+
1070
+ it('renders knowledge with body excerpt header', () => {
1071
+ const result = omDescribe(OM_SEARCH_MODEL, 'knowledge.outreach-playbook')
1072
+ const output = formatOmDescribe(result)
1073
+ expect(output).toContain('Knowledge: knowledge.outreach-playbook')
1074
+ expect(output).toContain('Body excerpt')
1075
+ expect(output).toContain('use knowledge:cat for full')
1076
+ })
1077
+
1078
+ it('renders role with responsibilities', () => {
1079
+ const result = omDescribe(OM_SEARCH_MODEL, 'role.growth-operator')
1080
+ const output = formatOmDescribe(result)
1081
+ expect(output).toContain('Role: role.growth-operator')
1082
+ expect(output).toContain('Title: Growth Operator')
1083
+ expect(output).toContain('Responsibilities')
1084
+ })
1085
+
1086
+ it('renders policy with trigger kind', () => {
1087
+ const result = omDescribe(OM_SEARCH_MODEL, 'policy.approve-large-deals')
1088
+ const output = formatOmDescribe(result)
1089
+ expect(output).toContain('Policy: policy.approve-large-deals')
1090
+ expect(output).toContain('Trigger: manual')
1091
+ })
1092
+
1093
+ it('renders resource with system path', () => {
1094
+ const result = omDescribe(OM_SEARCH_MODEL, 'integration-apollo-elevasis')
1095
+ const output = formatOmDescribe(result)
1096
+ expect(output).toContain('Resource: integration-apollo-elevasis')
1097
+ expect(output).toContain('System: sales.lead-gen')
1098
+ })
1099
+ })