@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
package/src/knowledge/queries.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import type { OrganizationGraph } from '../organization-model/graph/types'
|
|
1
|
+
import type { OrganizationGraph } from '../organization-model/graph/types'
|
|
2
2
|
import type { OrgKnowledgeKind, OrgKnowledgeNode } from '../organization-model/domains/knowledge'
|
|
3
3
|
import { OntologyIdSchema, ontologyGraphNodeId } from '../organization-model/ontology'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
*
|
|
13
|
-
|
|
4
|
+
import type { OrganizationModel } from '../organization-model/types'
|
|
5
|
+
import { listAllSystems } from '../organization-model/helpers'
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Internal helpers
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Graph node ID prefix for knowledge nodes.
|
|
13
|
+
* Graph node IDs follow the convention: `knowledge:<om-node-id>`
|
|
14
|
+
* e.g. `knowledge:knowledge.outreach-playbook`
|
|
15
|
+
*/
|
|
14
16
|
function toGraphNodeId(omNodeId: string): string {
|
|
15
17
|
return `knowledge:${omNodeId}`
|
|
16
18
|
}
|
|
@@ -33,58 +35,87 @@ function toTargetGraphNodeId(nodeId: string): string {
|
|
|
33
35
|
|
|
34
36
|
return `system:${nodeId}`
|
|
35
37
|
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Resolve the sourceId of every knowledge node in the graph, keyed by graph
|
|
39
|
-
* node ID. The `sourceId` on a knowledge graph node is the OM node id
|
|
40
|
-
* (e.g. `knowledge.outreach-playbook`).
|
|
41
|
-
*/
|
|
42
|
-
function buildKnowledgeSourceIdMap(graph: OrganizationGraph): Map<string, string> {
|
|
43
|
-
const map = new Map<string, string>()
|
|
44
|
-
for (const node of graph.nodes) {
|
|
45
|
-
if (node.kind === 'knowledge' && node.sourceId) {
|
|
46
|
-
map.set(node.id, node.sourceId)
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return map
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
// Query functions
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Returns all knowledge nodes whose `governs` edges point to the given
|
|
58
|
-
* systemId (graph node ID: `system:<systemId>`).
|
|
59
|
-
*
|
|
60
|
-
* @param graph - The built OrganizationGraph.
|
|
61
|
-
* @param systemId - The dotted system id (e.g. `sales.crm`).
|
|
62
|
-
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the sourceId of every knowledge node in the graph, keyed by graph
|
|
41
|
+
* node ID. The `sourceId` on a knowledge graph node is the OM node id
|
|
42
|
+
* (e.g. `knowledge.outreach-playbook`).
|
|
43
|
+
*/
|
|
44
|
+
function buildKnowledgeSourceIdMap(graph: OrganizationGraph): Map<string, string> {
|
|
45
|
+
const map = new Map<string, string>()
|
|
46
|
+
for (const node of graph.nodes) {
|
|
47
|
+
if (node.kind === 'knowledge' && node.sourceId) {
|
|
48
|
+
map.set(node.id, node.sourceId)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return map
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Query functions
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Returns all knowledge nodes whose `governs` edges point to the given
|
|
60
|
+
* systemId (graph node ID: `system:<systemId>`).
|
|
61
|
+
*
|
|
62
|
+
* @param graph - The built OrganizationGraph.
|
|
63
|
+
* @param systemId - The dotted system id (e.g. `sales.crm`).
|
|
64
|
+
*/
|
|
63
65
|
export function bySystem(
|
|
64
|
-
graph: OrganizationGraph,
|
|
65
|
-
systemId: string,
|
|
66
|
-
knowledgeNodes: OrgKnowledgeNode[]
|
|
67
|
-
): OrgKnowledgeNode[] {
|
|
68
|
-
const targetGraphNodeId = `system:${systemId}`
|
|
69
|
-
|
|
70
|
-
// Find all knowledge graph node IDs that have a `governs` edge to the target
|
|
71
|
-
const governingKnowledgeNodeIds = new Set<string>()
|
|
72
|
-
for (const edge of graph.edges) {
|
|
73
|
-
if (edge.kind === 'governs' && edge.targetId === targetGraphNodeId && edge.sourceId.startsWith('knowledge:')) {
|
|
74
|
-
governingKnowledgeNodeIds.add(edge.sourceId)
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Build a lookup from graph-node-id -> OM sourceId
|
|
79
|
-
const sourceIdMap = buildKnowledgeSourceIdMap(graph)
|
|
80
|
-
|
|
81
|
-
// Collect the OM node ids that correspond to those graph nodes
|
|
82
|
-
const matchingOmIds = new Set<string>()
|
|
83
|
-
for (const graphNodeId of governingKnowledgeNodeIds) {
|
|
84
|
-
const omId = sourceIdMap.get(graphNodeId)
|
|
85
|
-
if (omId) matchingOmIds.add(omId)
|
|
86
|
-
}
|
|
87
|
-
|
|
66
|
+
graph: OrganizationGraph,
|
|
67
|
+
systemId: string,
|
|
68
|
+
knowledgeNodes: OrgKnowledgeNode[]
|
|
69
|
+
): OrgKnowledgeNode[] {
|
|
70
|
+
const targetGraphNodeId = `system:${systemId}`
|
|
71
|
+
|
|
72
|
+
// Find all knowledge graph node IDs that have a `governs` edge to the target
|
|
73
|
+
const governingKnowledgeNodeIds = new Set<string>()
|
|
74
|
+
for (const edge of graph.edges) {
|
|
75
|
+
if (edge.kind === 'governs' && edge.targetId === targetGraphNodeId && edge.sourceId.startsWith('knowledge:')) {
|
|
76
|
+
governingKnowledgeNodeIds.add(edge.sourceId)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Build a lookup from graph-node-id -> OM sourceId
|
|
81
|
+
const sourceIdMap = buildKnowledgeSourceIdMap(graph)
|
|
82
|
+
|
|
83
|
+
// Collect the OM node ids that correspond to those graph nodes
|
|
84
|
+
const matchingOmIds = new Set<string>()
|
|
85
|
+
for (const graphNodeId of governingKnowledgeNodeIds) {
|
|
86
|
+
const omId = sourceIdMap.get(graphNodeId)
|
|
87
|
+
if (omId) matchingOmIds.add(omId)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return knowledgeNodes.filter((n) => matchingOmIds.has(n.id))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function bySystemTree(
|
|
94
|
+
graph: OrganizationGraph,
|
|
95
|
+
systemId: string,
|
|
96
|
+
knowledgeNodes: OrgKnowledgeNode[]
|
|
97
|
+
): OrgKnowledgeNode[] {
|
|
98
|
+
const prefix = `${systemId}.`
|
|
99
|
+
const targetGraphNodeIds = new Set(
|
|
100
|
+
graph.nodes
|
|
101
|
+
.filter((node) => node.id === `system:${systemId}` || node.id.startsWith(`system:${prefix}`))
|
|
102
|
+
.map((node) => node.id)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
const governingKnowledgeNodeIds = new Set<string>()
|
|
106
|
+
for (const edge of graph.edges) {
|
|
107
|
+
if (edge.kind === 'governs' && targetGraphNodeIds.has(edge.targetId) && edge.sourceId.startsWith('knowledge:')) {
|
|
108
|
+
governingKnowledgeNodeIds.add(edge.sourceId)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const sourceIdMap = buildKnowledgeSourceIdMap(graph)
|
|
113
|
+
const matchingOmIds = new Set<string>()
|
|
114
|
+
for (const graphNodeId of governingKnowledgeNodeIds) {
|
|
115
|
+
const omId = sourceIdMap.get(graphNodeId)
|
|
116
|
+
if (omId) matchingOmIds.add(omId)
|
|
117
|
+
}
|
|
118
|
+
|
|
88
119
|
return knowledgeNodes.filter((n) => matchingOmIds.has(n.id))
|
|
89
120
|
}
|
|
90
121
|
|
|
@@ -111,149 +142,149 @@ export function byOntology(
|
|
|
111
142
|
|
|
112
143
|
return knowledgeNodes.filter((n) => matchingOmIds.has(n.id))
|
|
113
144
|
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Returns all knowledge nodes whose `kind` matches the given kind.
|
|
117
|
-
*
|
|
118
|
-
* @param graph - The built OrganizationGraph (unused structurally; kept for API symmetry).
|
|
119
|
-
* @param kind - The knowledge kind (`'playbook' | 'strategy' | 'reference'`).
|
|
120
|
-
* @param knowledgeNodes - Flat array of OrgKnowledgeNode from the OrganizationModel.
|
|
121
|
-
*/
|
|
122
|
-
export function byKind(
|
|
123
|
-
_graph: OrganizationGraph,
|
|
124
|
-
kind: OrgKnowledgeKind,
|
|
125
|
-
knowledgeNodes: OrgKnowledgeNode[]
|
|
126
|
-
): OrgKnowledgeNode[] {
|
|
127
|
-
return knowledgeNodes.filter((n) => n.kind === kind)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Returns all knowledge nodes where `ownerIds` includes the given `ownerId`.
|
|
132
|
-
*
|
|
133
|
-
* @param _graph - The built OrganizationGraph (unused; kept for API symmetry).
|
|
134
|
-
* @param ownerId - The owner id string (matches values in `node.ownerIds`).
|
|
135
|
-
* @param knowledgeNodes - Flat array of OrgKnowledgeNode from the OrganizationModel.
|
|
136
|
-
*/
|
|
137
|
-
export function byOwner(
|
|
138
|
-
_graph: OrganizationGraph,
|
|
139
|
-
ownerId: string,
|
|
140
|
-
knowledgeNodes: OrgKnowledgeNode[]
|
|
141
|
-
): OrgKnowledgeNode[] {
|
|
142
|
-
return knowledgeNodes.filter((n) => n.ownerIds.includes(ownerId))
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Returns the IDs of the graph nodes that the given knowledge node governs
|
|
147
|
-
* (outgoing `governs` edges from the knowledge graph node).
|
|
148
|
-
*
|
|
149
|
-
* The graph node ID for a knowledge OM node with id `X` is `knowledge:X`.
|
|
150
|
-
*
|
|
151
|
-
* @param graph - The built OrganizationGraph.
|
|
152
|
-
* @param nodeId - The OM knowledge node id (e.g. `knowledge.outreach-playbook`).
|
|
153
|
-
* Also accepts the graph node ID format (`knowledge:knowledge.outreach-playbook`).
|
|
154
|
-
* @returns Array of target graph node IDs (e.g. `['system:sales.crm', ...]`).
|
|
155
|
-
*/
|
|
156
|
-
export function governs(graph: OrganizationGraph, nodeId: string): string[] {
|
|
157
|
-
const graphNodeId = nodeId.startsWith('knowledge:') ? nodeId : toGraphNodeId(nodeId)
|
|
158
|
-
|
|
159
|
-
const results: string[] = []
|
|
160
|
-
for (const edge of graph.edges) {
|
|
161
|
-
if (edge.kind === 'governs' && edge.sourceId === graphNodeId) {
|
|
162
|
-
results.push(edge.targetId)
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return results
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Returns the IDs of the knowledge graph nodes that govern the given node
|
|
170
|
-
* (incoming `governs` edges pointing to `nodeId`).
|
|
171
|
-
*
|
|
172
|
-
* @param graph - The built OrganizationGraph.
|
|
173
|
-
* @param nodeId - The target graph node ID (e.g. `system:sales.crm`) or a
|
|
174
|
-
* bare system id (e.g. `sales.crm` — will be prefixed with `system:`).
|
|
175
|
-
* @returns Array of source graph node IDs (e.g. `['knowledge:knowledge.outreach-playbook', ...]`).
|
|
176
|
-
*/
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Returns all knowledge nodes whose `kind` matches the given kind.
|
|
148
|
+
*
|
|
149
|
+
* @param graph - The built OrganizationGraph (unused structurally; kept for API symmetry).
|
|
150
|
+
* @param kind - The knowledge kind (`'playbook' | 'strategy' | 'reference'`).
|
|
151
|
+
* @param knowledgeNodes - Flat array of OrgKnowledgeNode from the OrganizationModel.
|
|
152
|
+
*/
|
|
153
|
+
export function byKind(
|
|
154
|
+
_graph: OrganizationGraph,
|
|
155
|
+
kind: OrgKnowledgeKind,
|
|
156
|
+
knowledgeNodes: OrgKnowledgeNode[]
|
|
157
|
+
): OrgKnowledgeNode[] {
|
|
158
|
+
return knowledgeNodes.filter((n) => n.kind === kind)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Returns all knowledge nodes where `ownerIds` includes the given `ownerId`.
|
|
163
|
+
*
|
|
164
|
+
* @param _graph - The built OrganizationGraph (unused; kept for API symmetry).
|
|
165
|
+
* @param ownerId - The owner id string (matches values in `node.ownerIds`).
|
|
166
|
+
* @param knowledgeNodes - Flat array of OrgKnowledgeNode from the OrganizationModel.
|
|
167
|
+
*/
|
|
168
|
+
export function byOwner(
|
|
169
|
+
_graph: OrganizationGraph,
|
|
170
|
+
ownerId: string,
|
|
171
|
+
knowledgeNodes: OrgKnowledgeNode[]
|
|
172
|
+
): OrgKnowledgeNode[] {
|
|
173
|
+
return knowledgeNodes.filter((n) => n.ownerIds.includes(ownerId))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Returns the IDs of the graph nodes that the given knowledge node governs
|
|
178
|
+
* (outgoing `governs` edges from the knowledge graph node).
|
|
179
|
+
*
|
|
180
|
+
* The graph node ID for a knowledge OM node with id `X` is `knowledge:X`.
|
|
181
|
+
*
|
|
182
|
+
* @param graph - The built OrganizationGraph.
|
|
183
|
+
* @param nodeId - The OM knowledge node id (e.g. `knowledge.outreach-playbook`).
|
|
184
|
+
* Also accepts the graph node ID format (`knowledge:knowledge.outreach-playbook`).
|
|
185
|
+
* @returns Array of target graph node IDs (e.g. `['system:sales.crm', ...]`).
|
|
186
|
+
*/
|
|
187
|
+
export function governs(graph: OrganizationGraph, nodeId: string): string[] {
|
|
188
|
+
const graphNodeId = nodeId.startsWith('knowledge:') ? nodeId : toGraphNodeId(nodeId)
|
|
189
|
+
|
|
190
|
+
const results: string[] = []
|
|
191
|
+
for (const edge of graph.edges) {
|
|
192
|
+
if (edge.kind === 'governs' && edge.sourceId === graphNodeId) {
|
|
193
|
+
results.push(edge.targetId)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return results
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Returns the IDs of the knowledge graph nodes that govern the given node
|
|
201
|
+
* (incoming `governs` edges pointing to `nodeId`).
|
|
202
|
+
*
|
|
203
|
+
* @param graph - The built OrganizationGraph.
|
|
204
|
+
* @param nodeId - The target graph node ID (e.g. `system:sales.crm`) or a
|
|
205
|
+
* bare system id (e.g. `sales.crm` — will be prefixed with `system:`).
|
|
206
|
+
* @returns Array of source graph node IDs (e.g. `['knowledge:knowledge.outreach-playbook', ...]`).
|
|
207
|
+
*/
|
|
177
208
|
export function governedBy(graph: OrganizationGraph, nodeId: string): string[] {
|
|
178
209
|
// Accept prefixed graph IDs, bare system IDs, and bare ontology IDs.
|
|
179
210
|
const targetId = toTargetGraphNodeId(nodeId)
|
|
180
|
-
|
|
181
|
-
const results: string[] = []
|
|
182
|
-
for (const edge of graph.edges) {
|
|
183
|
-
if (edge.kind === 'governs' && edge.targetId === targetId) {
|
|
184
|
-
results.push(edge.sourceId)
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
return results
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// ---------------------------------------------------------------------------
|
|
191
|
-
// Path parser
|
|
192
|
-
// ---------------------------------------------------------------------------
|
|
193
|
-
|
|
194
|
-
/** The recognized mount axes for Knowledge Map paths. */
|
|
211
|
+
|
|
212
|
+
const results: string[] = []
|
|
213
|
+
for (const edge of graph.edges) {
|
|
214
|
+
if (edge.kind === 'governs' && edge.targetId === targetId) {
|
|
215
|
+
results.push(edge.sourceId)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return results
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// Path parser
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
/** The recognized mount axes for Knowledge Map paths. */
|
|
195
226
|
export type KnowledgeMount = 'by-system' | 'by-ontology' | 'by-kind' | 'by-owner' | 'graph' | 'node'
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* The result of parsing a Knowledge Map path string.
|
|
199
|
-
*
|
|
200
|
-
* Shape: `{ mount: KnowledgeMount, args: string[] }`
|
|
201
|
-
*
|
|
202
|
-
* Per-mount arg arrays:
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The result of parsing a Knowledge Map path string.
|
|
230
|
+
*
|
|
231
|
+
* Shape: `{ mount: KnowledgeMount, args: string[] }`
|
|
232
|
+
*
|
|
233
|
+
* Per-mount arg arrays:
|
|
203
234
|
* - `by-system`: `[systemId]` (e.g. `['sales.crm']`)
|
|
204
235
|
* - `by-ontology`:`[ontologyId]` (e.g. `['sales.crm:object/deal']`)
|
|
205
|
-
* - `by-kind`: `[kind]` (e.g. `['playbook']`)
|
|
206
|
-
* - `by-owner`: `[ownerId]` (e.g. `['role.ops-lead']`)
|
|
207
|
-
* - `graph`: `[nodeId, verb]` where verb is `'governs'` or `'governed-by'`
|
|
208
|
-
* - `node`: `[nodeId]` (single node lookup, no sub-path)
|
|
209
|
-
*/
|
|
210
|
-
export interface ParsedKnowledgePath {
|
|
211
|
-
mount: KnowledgeMount
|
|
212
|
-
args: string[]
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Parses a Knowledge Map path string into a `{ mount, args }` descriptor.
|
|
217
|
-
*
|
|
218
|
-
* Supported path patterns:
|
|
236
|
+
* - `by-kind`: `[kind]` (e.g. `['playbook']`)
|
|
237
|
+
* - `by-owner`: `[ownerId]` (e.g. `['role.ops-lead']`)
|
|
238
|
+
* - `graph`: `[nodeId, verb]` where verb is `'governs'` or `'governed-by'`
|
|
239
|
+
* - `node`: `[nodeId]` (single node lookup, no sub-path)
|
|
240
|
+
*/
|
|
241
|
+
export interface ParsedKnowledgePath {
|
|
242
|
+
mount: KnowledgeMount
|
|
243
|
+
args: string[]
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Parses a Knowledge Map path string into a `{ mount, args }` descriptor.
|
|
248
|
+
*
|
|
249
|
+
* Supported path patterns:
|
|
219
250
|
* `/by-system/<systemId>` -> `{ mount: 'by-system', args: ['<systemId>'] }`
|
|
220
251
|
* `/by-ontology/<ontologyId>` -> `{ mount: 'by-ontology', args: ['<ontologyId>'] }`
|
|
221
|
-
* `/by-kind/<kind>` → `{ mount: 'by-kind', args: ['<kind>'] }`
|
|
222
|
-
* `/by-owner/<ownerId>` → `{ mount: 'by-owner', args: ['<ownerId>'] }`
|
|
223
|
-
* `/graph/<nodeId>/governs` → `{ mount: 'graph', args: ['<nodeId>', 'governs'] }`
|
|
224
|
-
* `/graph/<nodeId>/governed-by` → `{ mount: 'graph', args: ['<nodeId>', 'governed-by'] }`
|
|
225
|
-
* `/<nodeId>` → `{ mount: 'node', args: ['<nodeId>'] }`
|
|
226
|
-
*
|
|
227
|
-
* The path MUST start with `/`. Trailing slashes are stripped before parsing.
|
|
228
|
-
*
|
|
229
|
-
* @throws {Error} If the path is empty, missing a leading slash, or does not
|
|
230
|
-
* match any supported mount pattern.
|
|
231
|
-
*/
|
|
232
|
-
export function parsePath(pathString: string): ParsedKnowledgePath {
|
|
233
|
-
if (!pathString || typeof pathString !== 'string') {
|
|
234
|
-
throw new Error('parsePath: path must be a non-empty string')
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (!pathString.startsWith('/')) {
|
|
238
|
-
throw new Error(`parsePath: path must start with "/", got: "${pathString}"`)
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Strip trailing slashes then split
|
|
242
|
-
const normalized = pathString.replace(/\/+$/, '')
|
|
243
|
-
const segments = normalized.split('/').filter((s) => s.length > 0)
|
|
244
|
-
|
|
245
|
-
if (segments.length === 0) {
|
|
246
|
-
throw new Error(`parsePath: path resolves to root with no mount: "${pathString}"`)
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const [first, ...rest] = segments
|
|
250
|
-
|
|
251
|
-
// /by-system/<systemId>
|
|
252
|
+
* `/by-kind/<kind>` → `{ mount: 'by-kind', args: ['<kind>'] }`
|
|
253
|
+
* `/by-owner/<ownerId>` → `{ mount: 'by-owner', args: ['<ownerId>'] }`
|
|
254
|
+
* `/graph/<nodeId>/governs` → `{ mount: 'graph', args: ['<nodeId>', 'governs'] }`
|
|
255
|
+
* `/graph/<nodeId>/governed-by` → `{ mount: 'graph', args: ['<nodeId>', 'governed-by'] }`
|
|
256
|
+
* `/<nodeId>` → `{ mount: 'node', args: ['<nodeId>'] }`
|
|
257
|
+
*
|
|
258
|
+
* The path MUST start with `/`. Trailing slashes are stripped before parsing.
|
|
259
|
+
*
|
|
260
|
+
* @throws {Error} If the path is empty, missing a leading slash, or does not
|
|
261
|
+
* match any supported mount pattern.
|
|
262
|
+
*/
|
|
263
|
+
export function parsePath(pathString: string): ParsedKnowledgePath {
|
|
264
|
+
if (!pathString || typeof pathString !== 'string') {
|
|
265
|
+
throw new Error('parsePath: path must be a non-empty string')
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!pathString.startsWith('/')) {
|
|
269
|
+
throw new Error(`parsePath: path must start with "/", got: "${pathString}"`)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Strip trailing slashes then split
|
|
273
|
+
const normalized = pathString.replace(/\/+$/, '')
|
|
274
|
+
const segments = normalized.split('/').filter((s) => s.length > 0)
|
|
275
|
+
|
|
276
|
+
if (segments.length === 0) {
|
|
277
|
+
throw new Error(`parsePath: path resolves to root with no mount: "${pathString}"`)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const [first, ...rest] = segments
|
|
281
|
+
|
|
282
|
+
// /by-system/<systemId>
|
|
252
283
|
if (first === 'by-system') {
|
|
253
|
-
if (rest.length === 0) {
|
|
254
|
-
throw new Error(`parsePath: /by-system requires a systemId argument, got: "${pathString}"`)
|
|
255
|
-
}
|
|
256
|
-
return { mount: 'by-system', args: [rest.join('/')] }
|
|
284
|
+
if (rest.length === 0) {
|
|
285
|
+
throw new Error(`parsePath: /by-system requires a systemId argument, got: "${pathString}"`)
|
|
286
|
+
}
|
|
287
|
+
return { mount: 'by-system', args: [rest.join('/')] }
|
|
257
288
|
}
|
|
258
289
|
|
|
259
290
|
// /by-ontology/<ontologyId>
|
|
@@ -263,46 +294,780 @@ export function parsePath(pathString: string): ParsedKnowledgePath {
|
|
|
263
294
|
}
|
|
264
295
|
return { mount: 'by-ontology', args: [rest.join('/')] }
|
|
265
296
|
}
|
|
266
|
-
|
|
267
|
-
// /by-kind/<kind>
|
|
268
|
-
if (first === 'by-kind') {
|
|
269
|
-
if (rest.length === 0) {
|
|
270
|
-
throw new Error(`parsePath: /by-kind requires a kind argument, got: "${pathString}"`)
|
|
271
|
-
}
|
|
272
|
-
return { mount: 'by-kind', args: [rest[0]] }
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// /by-owner/<ownerId>
|
|
276
|
-
if (first === 'by-owner') {
|
|
277
|
-
if (rest.length === 0) {
|
|
278
|
-
throw new Error(`parsePath: /by-owner requires an ownerId argument, got: "${pathString}"`)
|
|
279
|
-
}
|
|
280
|
-
return { mount: 'by-owner', args: [rest.join('/')] }
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// /graph/<nodeId>/governs or /graph/<nodeId>/governed-by
|
|
284
|
-
if (first === 'graph') {
|
|
285
|
-
if (rest.length < 2) {
|
|
286
|
-
throw new Error(`parsePath: /graph requires <nodeId>/<verb> (governs|governed-by), got: "${pathString}"`)
|
|
287
|
-
}
|
|
288
|
-
const graphNodeId = rest.slice(0, -1).join('/')
|
|
289
|
-
const verb = rest[rest.length - 1]
|
|
290
|
-
if (verb !== 'governs' && verb !== 'governed-by') {
|
|
291
|
-
throw new Error(
|
|
292
|
-
`parsePath: /graph/<nodeId> verb must be "governs" or "governed-by", got: "${verb}" in "${pathString}"`
|
|
293
|
-
)
|
|
294
|
-
}
|
|
295
|
-
return { mount: 'graph', args: [graphNodeId, verb] }
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// /<nodeId> (single node)
|
|
299
|
-
// first must not be a recognized mount prefix
|
|
300
|
-
if (segments.length === 1) {
|
|
301
|
-
return { mount: 'node', args: [first] }
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
throw new Error(
|
|
305
|
-
`parsePath: unrecognized path pattern "${pathString}". Supported: /by-system/<id>, /by-kind/<kind>, /by-owner/<id>, /graph/<nodeId>/governs, /graph/<nodeId>/governed-by, /<nodeId>`
|
|
306
|
-
)
|
|
307
|
-
}
|
|
308
|
-
|
|
297
|
+
|
|
298
|
+
// /by-kind/<kind>
|
|
299
|
+
if (first === 'by-kind') {
|
|
300
|
+
if (rest.length === 0) {
|
|
301
|
+
throw new Error(`parsePath: /by-kind requires a kind argument, got: "${pathString}"`)
|
|
302
|
+
}
|
|
303
|
+
return { mount: 'by-kind', args: [rest[0]] }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// /by-owner/<ownerId>
|
|
307
|
+
if (first === 'by-owner') {
|
|
308
|
+
if (rest.length === 0) {
|
|
309
|
+
throw new Error(`parsePath: /by-owner requires an ownerId argument, got: "${pathString}"`)
|
|
310
|
+
}
|
|
311
|
+
return { mount: 'by-owner', args: [rest.join('/')] }
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// /graph/<nodeId>/governs or /graph/<nodeId>/governed-by
|
|
315
|
+
if (first === 'graph') {
|
|
316
|
+
if (rest.length < 2) {
|
|
317
|
+
throw new Error(`parsePath: /graph requires <nodeId>/<verb> (governs|governed-by), got: "${pathString}"`)
|
|
318
|
+
}
|
|
319
|
+
const graphNodeId = rest.slice(0, -1).join('/')
|
|
320
|
+
const verb = rest[rest.length - 1]
|
|
321
|
+
if (verb !== 'governs' && verb !== 'governed-by') {
|
|
322
|
+
throw new Error(
|
|
323
|
+
`parsePath: /graph/<nodeId> verb must be "governs" or "governed-by", got: "${verb}" in "${pathString}"`
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
return { mount: 'graph', args: [graphNodeId, verb] }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// /<nodeId> (single node)
|
|
330
|
+
// first must not be a recognized mount prefix
|
|
331
|
+
if (segments.length === 1) {
|
|
332
|
+
return { mount: 'node', args: [first] }
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
throw new Error(
|
|
336
|
+
`parsePath: unrecognized path pattern "${pathString}". Supported: /by-system/<id>, /by-kind/<kind>, /by-owner/<id>, /graph/<nodeId>/governs, /graph/<nodeId>/governed-by, /<nodeId>`
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
// omSearch — universal keyword search across all OM surfaces
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* The kind of OM node that produced a search hit.
|
|
346
|
+
*
|
|
347
|
+
* Maps to the surface being searched:
|
|
348
|
+
* - `system` — recursive systems tree (model.systems)
|
|
349
|
+
* - `resource` — model.resources (workflows, agents, integrations, scripts)
|
|
350
|
+
* - `knowledge` — model.knowledge (playbooks, strategies, references)
|
|
351
|
+
* - `ontology` — system.ontology and model.ontology (objects, actions, events, catalogs, links, ...)
|
|
352
|
+
* - `role` — model.roles
|
|
353
|
+
* - `policy` — model.policies
|
|
354
|
+
*/
|
|
355
|
+
export type OmSearchHitKind = 'system' | 'resource' | 'knowledge' | 'ontology' | 'role' | 'policy'
|
|
356
|
+
|
|
357
|
+
export interface OmSearchHit {
|
|
358
|
+
/** Surface that produced the hit. */
|
|
359
|
+
kind: OmSearchHitKind
|
|
360
|
+
/** Canonical id (system path, resource id, knowledge id, ontology id, role id, policy id). */
|
|
361
|
+
id: string
|
|
362
|
+
/** Display title (label/title/summary fallback). */
|
|
363
|
+
title: string
|
|
364
|
+
/** One-line description or summary (may be empty). */
|
|
365
|
+
summary: string
|
|
366
|
+
/** Ranking score; higher is better. */
|
|
367
|
+
score: number
|
|
368
|
+
/** Optional sub-kind for ontology / resource (`object`, `action`, `event`, `workflow`, ...). */
|
|
369
|
+
subKind?: string
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export interface OmSearchOptions {
|
|
373
|
+
/** Max number of hits to return. Defaults to 10. Pass 0 for unlimited. */
|
|
374
|
+
limit?: number
|
|
375
|
+
/** Restrict to specific OM surfaces. Defaults to all kinds. */
|
|
376
|
+
kinds?: OmSearchHitKind[]
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Universal keyword search across the entire Organization Model.
|
|
381
|
+
*
|
|
382
|
+
* Iterates over every surface (knowledge, systems, resources, ontology, roles, policies),
|
|
383
|
+
* scores each entry by term overlap, and returns the top hits ranked by score.
|
|
384
|
+
*
|
|
385
|
+
* Scoring (per query term, summed across terms):
|
|
386
|
+
* - title/label match: ×8
|
|
387
|
+
* - summary/description match: ×4
|
|
388
|
+
* - tags/responsibilities/meta match: ×3
|
|
389
|
+
* - body match: ×1
|
|
390
|
+
* Bonuses: exact id match (+50 for single-term queries), all terms appear in id (+5).
|
|
391
|
+
*
|
|
392
|
+
* Case-insensitive. Empty query returns no hits.
|
|
393
|
+
*/
|
|
394
|
+
export function omSearch(model: OrganizationModel, query: string, options: OmSearchOptions = {}): OmSearchHit[] {
|
|
395
|
+
const limit = options.limit ?? 10
|
|
396
|
+
const kinds = options.kinds ? new Set(options.kinds) : null
|
|
397
|
+
|
|
398
|
+
const terms = tokenize(query)
|
|
399
|
+
if (terms.length === 0) return []
|
|
400
|
+
|
|
401
|
+
const entries = collectSearchable(model, kinds)
|
|
402
|
+
|
|
403
|
+
const scored: OmSearchHit[] = []
|
|
404
|
+
for (const entry of entries) {
|
|
405
|
+
const score = scoreEntry(entry, terms)
|
|
406
|
+
if (score > 0) {
|
|
407
|
+
scored.push({
|
|
408
|
+
kind: entry.kind,
|
|
409
|
+
id: entry.id,
|
|
410
|
+
title: entry.title,
|
|
411
|
+
summary: entry.summary,
|
|
412
|
+
score,
|
|
413
|
+
subKind: entry.subKind
|
|
414
|
+
})
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
scored.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id))
|
|
419
|
+
|
|
420
|
+
return limit > 0 ? scored.slice(0, limit) : scored
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
// omSearch internals
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
interface SearchableEntry {
|
|
428
|
+
kind: OmSearchHitKind
|
|
429
|
+
id: string
|
|
430
|
+
title: string
|
|
431
|
+
summary: string
|
|
432
|
+
subKind?: string
|
|
433
|
+
/** Title-tier fields. Weight ×8 per term match. */
|
|
434
|
+
high: string[]
|
|
435
|
+
/** Description-tier fields. Weight ×4 per term match. */
|
|
436
|
+
mid: string[]
|
|
437
|
+
/** Tag/meta-tier fields (kind, systemPath, responsibilities, owner). Weight ×3 per term match. */
|
|
438
|
+
meta: string[]
|
|
439
|
+
/** Body-tier fields (MDX body, etc). Weight ×1 per term match. */
|
|
440
|
+
low: string[]
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function tokenize(query: string): string[] {
|
|
444
|
+
return query
|
|
445
|
+
.toLowerCase()
|
|
446
|
+
.split(/[\s,/]+/)
|
|
447
|
+
.map((t) => t.replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, ''))
|
|
448
|
+
.filter((t) => t.length >= 2)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Split a title/id into searchable tokens, breaking on whitespace AND structural
|
|
453
|
+
* separators (`.` `-` `_` `:`). Used for exact-name-match boosts.
|
|
454
|
+
*/
|
|
455
|
+
function structuralTokens(text: string): Set<string> {
|
|
456
|
+
return new Set(
|
|
457
|
+
text
|
|
458
|
+
.toLowerCase()
|
|
459
|
+
.split(/[\s.\-_:/,;]+/)
|
|
460
|
+
.map((t) => t.replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, ''))
|
|
461
|
+
.filter((t) => t.length >= 2)
|
|
462
|
+
)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function setsEqual(a: Set<string>, b: Set<string>): boolean {
|
|
466
|
+
if (a.size !== b.size) return false
|
|
467
|
+
for (const item of a) if (!b.has(item)) return false
|
|
468
|
+
return true
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function countOccurrences(haystack: string, needle: string): number {
|
|
472
|
+
if (!haystack || !needle) return 0
|
|
473
|
+
const lower = haystack.toLowerCase()
|
|
474
|
+
let count = 0
|
|
475
|
+
let idx = 0
|
|
476
|
+
while ((idx = lower.indexOf(needle, idx)) !== -1) {
|
|
477
|
+
count++
|
|
478
|
+
idx += needle.length
|
|
479
|
+
}
|
|
480
|
+
return count
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Per-kind base boost applied when an entry scores at all. Reflects how
|
|
485
|
+
* "primary" each surface is in the OM: structural identifiers (systems,
|
|
486
|
+
* resources, roles) outrank descriptive content (knowledge bodies) on close
|
|
487
|
+
* ties. Knowledge nodes still dominate when nothing else matches.
|
|
488
|
+
*/
|
|
489
|
+
const KIND_BOOST: Record<OmSearchHitKind, number> = {
|
|
490
|
+
system: 5,
|
|
491
|
+
resource: 3,
|
|
492
|
+
role: 3,
|
|
493
|
+
policy: 3,
|
|
494
|
+
ontology: 1,
|
|
495
|
+
knowledge: 0
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function scoreEntry(entry: SearchableEntry, terms: string[]): number {
|
|
499
|
+
let total = 0
|
|
500
|
+
const idLower = entry.id.toLowerCase()
|
|
501
|
+
|
|
502
|
+
for (const term of terms) {
|
|
503
|
+
for (const text of entry.high) total += 8 * countOccurrences(text, term)
|
|
504
|
+
for (const text of entry.mid) total += 4 * countOccurrences(text, term)
|
|
505
|
+
for (const text of entry.meta) total += 3 * countOccurrences(text, term)
|
|
506
|
+
for (const text of entry.low) total += 1 * countOccurrences(text, term)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (total === 0) return 0
|
|
510
|
+
|
|
511
|
+
const queryTokenSet = new Set(terms)
|
|
512
|
+
|
|
513
|
+
// Exact-id boost (single-term query): massive jump so /knowledge.outreach-playbook lands first.
|
|
514
|
+
if (terms.length === 1 && idLower === terms[0]) total += 50
|
|
515
|
+
|
|
516
|
+
// Exact-title-match boost: the query tokens exactly match the entity's title tokens.
|
|
517
|
+
// This precisely rewards "the user typed the entity's name" (e.g. "lead gen" -> "Lead Gen" system).
|
|
518
|
+
const titleTokens = structuralTokens(entry.title)
|
|
519
|
+
if (setsEqual(queryTokenSet, titleTokens)) total += 25
|
|
520
|
+
|
|
521
|
+
// Exact-last-id-segment match: query tokens equal the last dotted id segment's tokens.
|
|
522
|
+
// Helps "lead gen" match `sales.lead-gen` (last segment "lead-gen" -> {lead, gen}).
|
|
523
|
+
const lastSegment = idLower.split('.').pop() ?? idLower
|
|
524
|
+
if (setsEqual(queryTokenSet, structuralTokens(lastSegment))) total += 20
|
|
525
|
+
|
|
526
|
+
// All-terms-in-id boost: helpful when no exact match but id contains every term as substring.
|
|
527
|
+
if (terms.every((t) => idLower.includes(t))) total += 5
|
|
528
|
+
|
|
529
|
+
// Kind-aware structural preference.
|
|
530
|
+
total += KIND_BOOST[entry.kind]
|
|
531
|
+
|
|
532
|
+
return total
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function collectSearchable(model: OrganizationModel, kinds: Set<OmSearchHitKind> | null): SearchableEntry[] {
|
|
536
|
+
const entries: SearchableEntry[] = []
|
|
537
|
+
|
|
538
|
+
const include = (kind: OmSearchHitKind): boolean => kinds === null || kinds.has(kind)
|
|
539
|
+
|
|
540
|
+
// Knowledge nodes
|
|
541
|
+
if (include('knowledge')) {
|
|
542
|
+
for (const node of Object.values(model.knowledge ?? {})) {
|
|
543
|
+
entries.push({
|
|
544
|
+
kind: 'knowledge',
|
|
545
|
+
id: node.id,
|
|
546
|
+
title: node.title,
|
|
547
|
+
summary: node.summary,
|
|
548
|
+
subKind: node.kind,
|
|
549
|
+
high: [node.id, node.title],
|
|
550
|
+
mid: [node.summary],
|
|
551
|
+
meta: [node.kind, ...(node.ownerIds ?? [])],
|
|
552
|
+
low: [node.body]
|
|
553
|
+
})
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Systems (recursive tree)
|
|
558
|
+
if (include('system')) {
|
|
559
|
+
for (const { path, system } of listAllSystems(model)) {
|
|
560
|
+
const title = system.label ?? system.title ?? path
|
|
561
|
+
const description = system.description ?? ''
|
|
562
|
+
entries.push({
|
|
563
|
+
kind: 'system',
|
|
564
|
+
id: path,
|
|
565
|
+
title,
|
|
566
|
+
summary: description,
|
|
567
|
+
subKind: system.kind,
|
|
568
|
+
high: [path, title],
|
|
569
|
+
mid: [description],
|
|
570
|
+
meta: [system.kind ?? '', system.lifecycle ?? '', system.responsibleRoleId ?? ''],
|
|
571
|
+
low: []
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
// Per-system ontology entries
|
|
575
|
+
if (include('ontology') && system.ontology) {
|
|
576
|
+
entries.push(...collectOntologyEntries(system.ontology))
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
} else if (include('ontology')) {
|
|
580
|
+
// If systems are excluded but ontology isn't, still walk systems to find ontology
|
|
581
|
+
for (const { system } of listAllSystems(model)) {
|
|
582
|
+
if (system.ontology) entries.push(...collectOntologyEntries(system.ontology))
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Top-level ontology (global scope)
|
|
587
|
+
if (include('ontology') && model.ontology) {
|
|
588
|
+
entries.push(...collectOntologyEntries(model.ontology))
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Resources
|
|
592
|
+
if (include('resource')) {
|
|
593
|
+
for (const resource of Object.values(model.resources ?? {})) {
|
|
594
|
+
const title = resource.title ?? resource.id
|
|
595
|
+
const description = resource.description ?? ''
|
|
596
|
+
entries.push({
|
|
597
|
+
kind: 'resource',
|
|
598
|
+
id: resource.id,
|
|
599
|
+
title,
|
|
600
|
+
summary: description,
|
|
601
|
+
subKind: resource.kind,
|
|
602
|
+
high: [resource.id, title],
|
|
603
|
+
mid: [description],
|
|
604
|
+
meta: [resource.kind, resource.systemPath, resource.status, resource.ownerRoleId ?? ''],
|
|
605
|
+
low: []
|
|
606
|
+
})
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Roles
|
|
611
|
+
if (include('role')) {
|
|
612
|
+
for (const role of Object.values(model.roles ?? {})) {
|
|
613
|
+
const responsibilities = role.responsibilities ?? []
|
|
614
|
+
entries.push({
|
|
615
|
+
kind: 'role',
|
|
616
|
+
id: role.id,
|
|
617
|
+
title: role.title,
|
|
618
|
+
summary: responsibilities[0] ?? '',
|
|
619
|
+
high: [role.id, role.title],
|
|
620
|
+
mid: [],
|
|
621
|
+
meta: responsibilities,
|
|
622
|
+
low: []
|
|
623
|
+
})
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Policies
|
|
628
|
+
if (include('policy')) {
|
|
629
|
+
for (const policy of Object.values(model.policies ?? {})) {
|
|
630
|
+
const description = policy.description ?? ''
|
|
631
|
+
entries.push({
|
|
632
|
+
kind: 'policy',
|
|
633
|
+
id: policy.id,
|
|
634
|
+
title: policy.label,
|
|
635
|
+
summary: description,
|
|
636
|
+
subKind: policy.trigger?.kind,
|
|
637
|
+
high: [policy.id, policy.label],
|
|
638
|
+
mid: [description],
|
|
639
|
+
meta: [policy.trigger?.kind ?? ''],
|
|
640
|
+
low: []
|
|
641
|
+
})
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return entries
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
interface OntologyRecordLike {
|
|
649
|
+
id?: string
|
|
650
|
+
label?: string
|
|
651
|
+
description?: string
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function collectOntologyEntries(scope: {
|
|
655
|
+
objectTypes?: Record<string, OntologyRecordLike>
|
|
656
|
+
linkTypes?: Record<string, OntologyRecordLike>
|
|
657
|
+
actionTypes?: Record<string, OntologyRecordLike>
|
|
658
|
+
catalogTypes?: Record<string, OntologyRecordLike>
|
|
659
|
+
eventTypes?: Record<string, OntologyRecordLike>
|
|
660
|
+
interfaceTypes?: Record<string, OntologyRecordLike>
|
|
661
|
+
valueTypes?: Record<string, OntologyRecordLike>
|
|
662
|
+
sharedProperties?: Record<string, OntologyRecordLike>
|
|
663
|
+
groups?: Record<string, OntologyRecordLike>
|
|
664
|
+
surfaces?: Record<string, OntologyRecordLike>
|
|
665
|
+
}): SearchableEntry[] {
|
|
666
|
+
const entries: SearchableEntry[] = []
|
|
667
|
+
|
|
668
|
+
const buckets: Array<[string, Record<string, OntologyRecordLike> | undefined]> = [
|
|
669
|
+
['object', scope.objectTypes],
|
|
670
|
+
['link', scope.linkTypes],
|
|
671
|
+
['action', scope.actionTypes],
|
|
672
|
+
['catalog', scope.catalogTypes],
|
|
673
|
+
['event', scope.eventTypes],
|
|
674
|
+
['interface', scope.interfaceTypes],
|
|
675
|
+
['value-type', scope.valueTypes],
|
|
676
|
+
['property', scope.sharedProperties],
|
|
677
|
+
['group', scope.groups],
|
|
678
|
+
['surface', scope.surfaces]
|
|
679
|
+
]
|
|
680
|
+
|
|
681
|
+
for (const [subKind, records] of buckets) {
|
|
682
|
+
if (!records) continue
|
|
683
|
+
for (const [recordId, record] of Object.entries(records)) {
|
|
684
|
+
const id = record.id ?? recordId
|
|
685
|
+
const title = record.label ?? id
|
|
686
|
+
const description = record.description ?? ''
|
|
687
|
+
entries.push({
|
|
688
|
+
kind: 'ontology',
|
|
689
|
+
id,
|
|
690
|
+
title,
|
|
691
|
+
summary: description,
|
|
692
|
+
subKind,
|
|
693
|
+
high: [id, title],
|
|
694
|
+
mid: [description],
|
|
695
|
+
meta: [subKind],
|
|
696
|
+
low: []
|
|
697
|
+
})
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return entries
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// ---------------------------------------------------------------------------
|
|
705
|
+
// omDescribe — neighborhood view for any OM node ID
|
|
706
|
+
// ---------------------------------------------------------------------------
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Structured neighborhood view of a single OM node.
|
|
710
|
+
*
|
|
711
|
+
* The `kind` discriminator selects the shape: `system` carries parent/children
|
|
712
|
+
* and resource summaries; `knowledge` carries title/summary/body and
|
|
713
|
+
* `governs` edges; etc. Use `formatOmDescribe()` to render as text or pass
|
|
714
|
+
* directly to `JSON.stringify` for machine-readable output.
|
|
715
|
+
*/
|
|
716
|
+
export type OmDescribeResult =
|
|
717
|
+
| OmDescribeSystem
|
|
718
|
+
| OmDescribeResource
|
|
719
|
+
| OmDescribeKnowledge
|
|
720
|
+
| OmDescribeOntology
|
|
721
|
+
| OmDescribeRole
|
|
722
|
+
| OmDescribePolicy
|
|
723
|
+
|
|
724
|
+
export interface OmDescribeSystem {
|
|
725
|
+
kind: 'system'
|
|
726
|
+
id: string
|
|
727
|
+
label: string
|
|
728
|
+
description: string
|
|
729
|
+
systemKind?: string
|
|
730
|
+
lifecycle?: string
|
|
731
|
+
parentSystemId?: string
|
|
732
|
+
responsibleRoleId?: string
|
|
733
|
+
childSystemPaths: string[]
|
|
734
|
+
governingKnowledgeIds: string[]
|
|
735
|
+
resourceCountsByKind: Record<string, number>
|
|
736
|
+
resourceIdsByKind: Record<string, string[]>
|
|
737
|
+
ontologyCountsByKind: Record<string, number>
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
export interface OmDescribeResource {
|
|
741
|
+
kind: 'resource'
|
|
742
|
+
id: string
|
|
743
|
+
resourceKind: string
|
|
744
|
+
systemPath: string
|
|
745
|
+
title: string
|
|
746
|
+
description: string
|
|
747
|
+
status: string
|
|
748
|
+
ownerRoleId?: string
|
|
749
|
+
ontology?: {
|
|
750
|
+
primaryAction?: string
|
|
751
|
+
actions?: string[]
|
|
752
|
+
reads?: string[]
|
|
753
|
+
writes?: string[]
|
|
754
|
+
emits?: string[]
|
|
755
|
+
usesCatalogs?: string[]
|
|
756
|
+
}
|
|
757
|
+
codeRefPaths: string[]
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export interface OmDescribeKnowledge {
|
|
761
|
+
kind: 'knowledge'
|
|
762
|
+
id: string
|
|
763
|
+
knowledgeKind: string
|
|
764
|
+
title: string
|
|
765
|
+
summary: string
|
|
766
|
+
bodyExcerpt: string
|
|
767
|
+
bodyLineCount: number
|
|
768
|
+
ownerIds: string[]
|
|
769
|
+
governs: string[]
|
|
770
|
+
updatedAt: string
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
export interface OmDescribeOntology {
|
|
774
|
+
kind: 'ontology'
|
|
775
|
+
id: string
|
|
776
|
+
ontologyKind: string
|
|
777
|
+
scope: string
|
|
778
|
+
localId: string
|
|
779
|
+
label: string
|
|
780
|
+
description: string
|
|
781
|
+
governingKnowledgeIds: string[]
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
export interface OmDescribeRole {
|
|
785
|
+
kind: 'role'
|
|
786
|
+
id: string
|
|
787
|
+
title: string
|
|
788
|
+
responsibilities: string[]
|
|
789
|
+
responsibleFor: string[]
|
|
790
|
+
reportsToId?: string
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
export interface OmDescribePolicy {
|
|
794
|
+
kind: 'policy'
|
|
795
|
+
id: string
|
|
796
|
+
label: string
|
|
797
|
+
description: string
|
|
798
|
+
triggerKind: string
|
|
799
|
+
appliesToSystemIds: string[]
|
|
800
|
+
appliesToActionIds: string[]
|
|
801
|
+
appliesToResourceIds: string[]
|
|
802
|
+
appliesToRoleIds: string[]
|
|
803
|
+
effectKinds: string[]
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Detect the OM surface this ID belongs to. Returns `undefined` when the
|
|
808
|
+
* shape doesn't match any known pattern.
|
|
809
|
+
*
|
|
810
|
+
* Detection rules (highest priority first):
|
|
811
|
+
* - Contains `:<ontology-kind>/<local-id>` → ontology
|
|
812
|
+
* - Starts with `knowledge.` → knowledge
|
|
813
|
+
* - Starts with `role.` → role
|
|
814
|
+
* - Starts with `policy.` → policy
|
|
815
|
+
* - Resolves via `getSystem()` → system
|
|
816
|
+
* - Matches a key in `model.resources` → resource
|
|
817
|
+
*/
|
|
818
|
+
function detectKind(model: OrganizationModel, id: string): OmSearchHitKind | undefined {
|
|
819
|
+
if (/:(object|action|event|catalog|link|interface|value-type|property|group|surface)\//.test(id)) {
|
|
820
|
+
return 'ontology'
|
|
821
|
+
}
|
|
822
|
+
if (id.startsWith('knowledge.')) return 'knowledge'
|
|
823
|
+
if (id.startsWith('role.')) return 'role'
|
|
824
|
+
if (id.startsWith('policy.')) return 'policy'
|
|
825
|
+
if (model.systems && getSystemByPath(model.systems, id)) return 'system'
|
|
826
|
+
if (model.resources?.[id]) return 'resource'
|
|
827
|
+
return undefined
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function getSystemByPath(
|
|
831
|
+
systems: Record<string, { systems?: Record<string, unknown>; subsystems?: Record<string, unknown> }>,
|
|
832
|
+
path: string
|
|
833
|
+
): unknown {
|
|
834
|
+
const segments = path.split('.')
|
|
835
|
+
let current: Record<string, unknown> = systems
|
|
836
|
+
for (const seg of segments) {
|
|
837
|
+
const node = current[seg]
|
|
838
|
+
if (node === undefined || typeof node !== 'object' || node === null) return undefined
|
|
839
|
+
const next = node as { systems?: Record<string, unknown>; subsystems?: Record<string, unknown> }
|
|
840
|
+
if (seg === segments[segments.length - 1]) return next
|
|
841
|
+
current = (next.systems ?? next.subsystems ?? {}) as Record<string, unknown>
|
|
842
|
+
}
|
|
843
|
+
return undefined
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Build a structured neighborhood view of any OM node.
|
|
848
|
+
*
|
|
849
|
+
* Returns `undefined` when no entity matches the given id. The result is a
|
|
850
|
+
* JSON-safe discriminated union — pass to `formatOmDescribe()` for human
|
|
851
|
+
* output or `JSON.stringify` for machine consumption.
|
|
852
|
+
*
|
|
853
|
+
* @example
|
|
854
|
+
* omDescribe(model, 'sales.lead-gen')
|
|
855
|
+
* // -> { kind: 'system', id: 'sales.lead-gen', resourceCountsByKind: {...}, ... }
|
|
856
|
+
*/
|
|
857
|
+
export function omDescribe(model: OrganizationModel, id: string): OmDescribeResult | undefined {
|
|
858
|
+
const kind = detectKind(model, id)
|
|
859
|
+
if (kind === undefined) return undefined
|
|
860
|
+
|
|
861
|
+
switch (kind) {
|
|
862
|
+
case 'system':
|
|
863
|
+
return describeSystem(model, id)
|
|
864
|
+
case 'resource':
|
|
865
|
+
return describeResource(model, id)
|
|
866
|
+
case 'knowledge':
|
|
867
|
+
return describeKnowledge(model, id)
|
|
868
|
+
case 'ontology':
|
|
869
|
+
return describeOntology(model, id)
|
|
870
|
+
case 'role':
|
|
871
|
+
return describeRole(model, id)
|
|
872
|
+
case 'policy':
|
|
873
|
+
return describePolicy(model, id)
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function describeSystem(model: OrganizationModel, path: string): OmDescribeSystem | undefined {
|
|
878
|
+
const allSystems = listAllSystems(model)
|
|
879
|
+
const match = allSystems.find((s) => s.path === path)
|
|
880
|
+
if (!match) return undefined
|
|
881
|
+
const { system } = match
|
|
882
|
+
|
|
883
|
+
const prefix = path + '.'
|
|
884
|
+
const childSystemPaths = allSystems
|
|
885
|
+
.filter((s) => s.path.startsWith(prefix) && !s.path.slice(prefix.length).includes('.'))
|
|
886
|
+
.map((s) => s.path)
|
|
887
|
+
|
|
888
|
+
const resources = Object.values(model.resources ?? {}).filter((r) => r.systemPath === path)
|
|
889
|
+
const resourceCountsByKind: Record<string, number> = {}
|
|
890
|
+
const resourceIdsByKind: Record<string, string[]> = {}
|
|
891
|
+
for (const r of resources) {
|
|
892
|
+
resourceCountsByKind[r.kind] = (resourceCountsByKind[r.kind] ?? 0) + 1
|
|
893
|
+
if (!resourceIdsByKind[r.kind]) resourceIdsByKind[r.kind] = []
|
|
894
|
+
resourceIdsByKind[r.kind].push(r.id)
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const governingKnowledgeIds: string[] = []
|
|
898
|
+
for (const node of Object.values(model.knowledge ?? {})) {
|
|
899
|
+
if ((node.links ?? []).some((link) => link.nodeId === `system:${path}`)) {
|
|
900
|
+
governingKnowledgeIds.push(node.id)
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const ontologyCountsByKind: Record<string, number> = {}
|
|
905
|
+
const ontology = system.ontology
|
|
906
|
+
if (ontology) {
|
|
907
|
+
const buckets: Array<[string, Record<string, unknown> | undefined]> = [
|
|
908
|
+
['object', ontology.objectTypes],
|
|
909
|
+
['action', ontology.actionTypes],
|
|
910
|
+
['event', ontology.eventTypes],
|
|
911
|
+
['catalog', ontology.catalogTypes],
|
|
912
|
+
['link', ontology.linkTypes],
|
|
913
|
+
['interface', ontology.interfaceTypes],
|
|
914
|
+
['value-type', ontology.valueTypes],
|
|
915
|
+
['property', ontology.sharedProperties],
|
|
916
|
+
['group', ontology.groups],
|
|
917
|
+
['surface', ontology.surfaces]
|
|
918
|
+
]
|
|
919
|
+
for (const [k, records] of buckets) {
|
|
920
|
+
const count = records ? Object.keys(records).length : 0
|
|
921
|
+
if (count > 0) ontologyCountsByKind[k] = count
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
return {
|
|
926
|
+
kind: 'system',
|
|
927
|
+
id: path,
|
|
928
|
+
label: system.label ?? system.title ?? path,
|
|
929
|
+
description: system.description ?? '',
|
|
930
|
+
systemKind: system.kind,
|
|
931
|
+
lifecycle: system.lifecycle,
|
|
932
|
+
parentSystemId: system.parentSystemId,
|
|
933
|
+
responsibleRoleId: system.responsibleRoleId,
|
|
934
|
+
childSystemPaths,
|
|
935
|
+
governingKnowledgeIds,
|
|
936
|
+
resourceCountsByKind,
|
|
937
|
+
resourceIdsByKind,
|
|
938
|
+
ontologyCountsByKind
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function describeResource(model: OrganizationModel, id: string): OmDescribeResource | undefined {
|
|
943
|
+
const r = model.resources?.[id]
|
|
944
|
+
if (!r) return undefined
|
|
945
|
+
return {
|
|
946
|
+
kind: 'resource',
|
|
947
|
+
id: r.id,
|
|
948
|
+
resourceKind: r.kind,
|
|
949
|
+
systemPath: r.systemPath,
|
|
950
|
+
title: r.title ?? r.id,
|
|
951
|
+
description: r.description ?? '',
|
|
952
|
+
status: r.status,
|
|
953
|
+
ownerRoleId: r.ownerRoleId,
|
|
954
|
+
ontology: r.ontology
|
|
955
|
+
? {
|
|
956
|
+
primaryAction: r.ontology.primaryAction,
|
|
957
|
+
actions: r.ontology.actions,
|
|
958
|
+
reads: r.ontology.reads,
|
|
959
|
+
writes: r.ontology.writes,
|
|
960
|
+
emits: r.ontology.emits,
|
|
961
|
+
usesCatalogs: r.ontology.usesCatalogs
|
|
962
|
+
}
|
|
963
|
+
: undefined,
|
|
964
|
+
codeRefPaths: (r.codeRefs ?? []).map((c) => c.path)
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function describeKnowledge(model: OrganizationModel, id: string): OmDescribeKnowledge | undefined {
|
|
969
|
+
const node = model.knowledge?.[id]
|
|
970
|
+
if (!node) return undefined
|
|
971
|
+
const bodyLines = node.body.split('\n')
|
|
972
|
+
const bodyExcerpt = bodyLines.slice(0, 6).join('\n') + (bodyLines.length > 6 ? '\n...' : '')
|
|
973
|
+
return {
|
|
974
|
+
kind: 'knowledge',
|
|
975
|
+
id: node.id,
|
|
976
|
+
knowledgeKind: node.kind,
|
|
977
|
+
title: node.title,
|
|
978
|
+
summary: node.summary,
|
|
979
|
+
bodyExcerpt,
|
|
980
|
+
bodyLineCount: bodyLines.length,
|
|
981
|
+
ownerIds: node.ownerIds ?? [],
|
|
982
|
+
governs: (node.links ?? []).map((link) => link.nodeId),
|
|
983
|
+
updatedAt: node.updatedAt
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function describeOntology(model: OrganizationModel, id: string): OmDescribeOntology | undefined {
|
|
988
|
+
const parsed = id.match(/^([^:]+):([a-z-]+)\/(.+)$/)
|
|
989
|
+
if (!parsed) return undefined
|
|
990
|
+
const [, scope, ontologyKind, localId] = parsed
|
|
991
|
+
|
|
992
|
+
// Walk all systems + top-level ontology to find the record.
|
|
993
|
+
const buckets: Array<[string, string]> = [
|
|
994
|
+
['object', 'objectTypes'],
|
|
995
|
+
['action', 'actionTypes'],
|
|
996
|
+
['event', 'eventTypes'],
|
|
997
|
+
['catalog', 'catalogTypes'],
|
|
998
|
+
['link', 'linkTypes'],
|
|
999
|
+
['interface', 'interfaceTypes'],
|
|
1000
|
+
['value-type', 'valueTypes'],
|
|
1001
|
+
['property', 'sharedProperties'],
|
|
1002
|
+
['group', 'groups'],
|
|
1003
|
+
['surface', 'surfaces']
|
|
1004
|
+
]
|
|
1005
|
+
const bucketKey = buckets.find((b) => b[0] === ontologyKind)?.[1]
|
|
1006
|
+
if (!bucketKey) return undefined
|
|
1007
|
+
|
|
1008
|
+
type OntologyRecord = { id?: string; label?: string; description?: string }
|
|
1009
|
+
type OntologyScopeShape = Record<string, Record<string, OntologyRecord> | undefined>
|
|
1010
|
+
|
|
1011
|
+
function findIn(scopeObj: OntologyScopeShape | undefined): OntologyRecord | undefined {
|
|
1012
|
+
if (!scopeObj) return undefined
|
|
1013
|
+
const records = scopeObj[bucketKey!]
|
|
1014
|
+
return records?.[id]
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
let record: OntologyRecord | undefined
|
|
1018
|
+
// Search per-system scopes first, then global
|
|
1019
|
+
for (const { system } of listAllSystems(model)) {
|
|
1020
|
+
record = findIn(system.ontology as OntologyScopeShape | undefined)
|
|
1021
|
+
if (record) break
|
|
1022
|
+
}
|
|
1023
|
+
if (!record) record = findIn(model.ontology as OntologyScopeShape | undefined)
|
|
1024
|
+
if (!record) return undefined
|
|
1025
|
+
|
|
1026
|
+
const governingKnowledgeIds: string[] = []
|
|
1027
|
+
for (const node of Object.values(model.knowledge ?? {})) {
|
|
1028
|
+
if ((node.links ?? []).some((link) => link.nodeId === `ontology:${id}`)) {
|
|
1029
|
+
governingKnowledgeIds.push(node.id)
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
return {
|
|
1034
|
+
kind: 'ontology',
|
|
1035
|
+
id,
|
|
1036
|
+
ontologyKind,
|
|
1037
|
+
scope,
|
|
1038
|
+
localId,
|
|
1039
|
+
label: record.label ?? id,
|
|
1040
|
+
description: record.description ?? '',
|
|
1041
|
+
governingKnowledgeIds
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function describeRole(model: OrganizationModel, id: string): OmDescribeRole | undefined {
|
|
1046
|
+
const role = model.roles?.[id]
|
|
1047
|
+
if (!role) return undefined
|
|
1048
|
+
return {
|
|
1049
|
+
kind: 'role',
|
|
1050
|
+
id: role.id,
|
|
1051
|
+
title: role.title,
|
|
1052
|
+
responsibilities: role.responsibilities ?? [],
|
|
1053
|
+
responsibleFor: role.responsibleFor ?? [],
|
|
1054
|
+
reportsToId: role.reportsToId
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function describePolicy(model: OrganizationModel, id: string): OmDescribePolicy | undefined {
|
|
1059
|
+
const policy = model.policies?.[id]
|
|
1060
|
+
if (!policy) return undefined
|
|
1061
|
+
return {
|
|
1062
|
+
kind: 'policy',
|
|
1063
|
+
id: policy.id,
|
|
1064
|
+
label: policy.label,
|
|
1065
|
+
description: policy.description ?? '',
|
|
1066
|
+
triggerKind: policy.trigger?.kind ?? 'unknown',
|
|
1067
|
+
appliesToSystemIds: policy.appliesTo?.systemIds ?? [],
|
|
1068
|
+
appliesToActionIds: policy.appliesTo?.actionIds ?? [],
|
|
1069
|
+
appliesToResourceIds: policy.appliesTo?.resourceIds ?? [],
|
|
1070
|
+
appliesToRoleIds: policy.appliesTo?.roleIds ?? [],
|
|
1071
|
+
effectKinds: (policy.actions ?? []).map((a) => a.kind)
|
|
1072
|
+
}
|
|
1073
|
+
}
|