@elevasis/core 0.24.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3117 -2166
- package/dist/index.js +574 -16
- package/dist/knowledge/index.d.ts +122 -7
- package/dist/organization-model/index.d.ts +3117 -2166
- package/dist/organization-model/index.js +574 -16
- package/dist/test-utils/index.d.ts +135 -45
- package/dist/test-utils/index.js +122 -14
- package/package.json +3 -3
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +139 -101
- package/src/execution/engine/llm/adapters/__tests__/openrouter.integration.test.ts +10 -10
- package/src/execution/engine/workflow/types.ts +5 -7
- package/src/knowledge/__tests__/queries.test.ts +960 -546
- package/src/knowledge/format.ts +322 -100
- package/src/knowledge/index.ts +18 -5
- package/src/knowledge/queries.ts +1004 -239
- package/src/organization-model/__tests__/deprecate-helpers.test.ts +71 -0
- package/src/organization-model/__tests__/domains/resources.test.ts +19 -8
- package/src/organization-model/__tests__/domains/topology.test.ts +188 -0
- package/src/organization-model/__tests__/graph.test.ts +98 -7
- package/src/organization-model/__tests__/resolve.test.ts +9 -7
- package/src/organization-model/__tests__/scaffolders.test.ts +93 -0
- package/src/organization-model/__tests__/schema.test.ts +14 -4
- package/src/organization-model/defaults.ts +5 -3
- package/src/organization-model/domains/resources.ts +63 -20
- package/src/organization-model/domains/topology.ts +261 -0
- package/src/organization-model/graph/build.ts +63 -15
- package/src/organization-model/graph/schema.ts +4 -3
- package/src/organization-model/graph/types.ts +5 -4
- package/src/organization-model/helpers.ts +76 -9
- package/src/organization-model/icons.ts +1 -0
- package/src/organization-model/index.ts +7 -5
- package/src/organization-model/ontology.ts +2 -5
- package/src/organization-model/organization-model.mdx +16 -11
- package/src/organization-model/published.ts +51 -15
- package/src/organization-model/scaffolders/helpers.ts +84 -0
- package/src/organization-model/scaffolders/index.ts +19 -0
- package/src/organization-model/scaffolders/scaffoldKnowledgeNode.ts +48 -0
- package/src/organization-model/scaffolders/scaffoldOntologyRecord.ts +38 -0
- package/src/organization-model/scaffolders/scaffoldResource.ts +59 -0
- package/src/organization-model/scaffolders/scaffoldSystem.ts +110 -0
- package/src/organization-model/scaffolders/types.ts +81 -0
- package/src/organization-model/schema.ts +51 -11
- package/src/organization-model/types.ts +25 -11
- package/src/platform/constants/versions.ts +1 -1
- package/src/platform/registry/__tests__/validation.test.ts +199 -14
- package/src/platform/registry/resource-registry.ts +11 -11
- package/src/platform/registry/validation.ts +226 -34
- package/src/reference/_generated/contracts.md +139 -101
- package/src/reference/glossary.md +74 -72
- package/src/supabase/database.types.ts +3156 -3153
|
@@ -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
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
*
|
|
15
|
-
* -
|
|
16
|
-
* -
|
|
17
|
-
* -
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
*
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
{ id: '
|
|
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
|
+
})
|