@elevasis/core 0.42.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/auth/index.d.ts +6 -1
  2. package/dist/auth/index.js +6 -0
  3. package/dist/business/entities-published.d.ts +1 -1
  4. package/dist/index.d.ts +3 -4
  5. package/dist/index.js +37 -15
  6. package/dist/knowledge/index.d.ts +92 -4
  7. package/dist/knowledge/index.js +168 -1
  8. package/dist/organization-model/index.d.ts +3 -4
  9. package/dist/organization-model/index.js +37 -15
  10. package/dist/test-utils/index.d.ts +3 -4
  11. package/dist/test-utils/index.js +12 -6
  12. package/package.json +1 -1
  13. package/src/auth/__tests__/access-test-fixtures.ts +9 -7
  14. package/src/auth/access-keys.ts +6 -0
  15. package/src/business/base-entities.ts +1 -1
  16. package/src/business/clients/api-schemas.test.ts +0 -1
  17. package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-adapter.ts +9 -14
  18. package/src/knowledge/cli-helpers.ts +211 -0
  19. package/src/knowledge/index.ts +13 -0
  20. package/src/knowledge/published.ts +18 -5
  21. package/src/organization-model/__tests__/cross-ref.test.ts +11 -1
  22. package/src/organization-model/__tests__/domains/systems.test.ts +34 -8
  23. package/src/organization-model/__tests__/scaffolders.test.ts +30 -1
  24. package/src/organization-model/__tests__/schema-refinements.test.ts +178 -0
  25. package/src/organization-model/cross-ref.ts +41 -5
  26. package/src/organization-model/defaults.ts +2 -2
  27. package/src/organization-model/domains/actions.ts +1 -1
  28. package/src/organization-model/domains/systems.ts +0 -4
  29. package/src/organization-model/organization-graph.mdx +9 -8
  30. package/src/organization-model/resolve.ts +9 -7
  31. package/src/organization-model/scaffolders/scaffoldKnowledgeNode.ts +1 -0
  32. package/src/organization-model/scaffolders/scaffoldOntologyRecord.ts +28 -6
  33. package/src/organization-model/scaffolders/scaffoldResource.ts +1 -0
  34. package/src/organization-model/scaffolders/scaffoldSystem.ts +2 -1
  35. package/src/organization-model/schema-refinements.ts +3 -5
  36. package/src/platform/constants/versions.ts +1 -1
  37. package/src/platform/registry/__tests__/validation.test.ts +28 -0
  38. package/src/platform/registry/validation.ts +18 -0
  39. package/src/scaffold-registry/index.ts +11 -11
  40. package/src/scaffold-registry/schema.ts +30 -12
  41. package/src/test-utils/mocks/supabase.ts +1 -1
  42. package/src/test-utils/mocks/workos.ts +2 -2
@@ -1101,8 +1101,6 @@ var SystemEntrySchema = z.object({
1101
1101
  path: PathSchema.optional(),
1102
1102
  /** @deprecated Use ui.icon. Kept for one-cycle Feature compatibility. */
1103
1103
  icon: IconNameSchema.optional(),
1104
- /** @deprecated Feature color token, retained for one-cycle compatibility. */
1105
- color: ColorTokenSchema.optional(),
1106
1104
  /** @deprecated UI placement hint, retained for one-cycle compatibility. */
1107
1105
  uiPosition: UiPositionSchema.optional(),
1108
1106
  /** @deprecated Use lifecycle. */
@@ -1803,7 +1801,8 @@ var ONTOLOGY_REFERENCE_KEY_KINDS = {
1803
1801
  surfaceType: "surface",
1804
1802
  stepCatalog: "catalog"
1805
1803
  };
1806
- function buildOmCrossRefIndex(model) {
1804
+ var omCompilationContextCache = /* @__PURE__ */ new WeakMap();
1805
+ function buildOmCrossRefIndexFromOntology(model, ontologyCompilation) {
1807
1806
  const systemsById = /* @__PURE__ */ new Map();
1808
1807
  for (const { path, system } of listAllSystems(model)) {
1809
1808
  systemsById.set(path, system);
@@ -1817,7 +1816,6 @@ function buildOmCrossRefIndex(model) {
1817
1816
  const actionIds = new Set(Object.keys(model.actions ?? {}));
1818
1817
  const customerSegmentIds = new Set(Object.keys(model.customers ?? {}));
1819
1818
  const offeringIds = new Set(Object.keys(model.offerings ?? {}));
1820
- const ontologyCompilation = compileOrganizationOntology(model);
1821
1819
  const ontologyIndexByKind = {
1822
1820
  object: ontologyCompilation.ontology.objectTypes,
1823
1821
  link: ontologyCompilation.ontology.linkTypes,
@@ -1856,6 +1854,15 @@ function buildOmCrossRefIndex(model) {
1856
1854
  stageIds
1857
1855
  };
1858
1856
  }
1857
+ function buildOmCompilationContext(model) {
1858
+ const cached = omCompilationContextCache.get(model);
1859
+ if (cached !== void 0) return cached;
1860
+ const ontologyCompilation = compileOrganizationOntology(model);
1861
+ const crossRefIndex = buildOmCrossRefIndexFromOntology(model, ontologyCompilation);
1862
+ const context = { crossRefIndex, ontologyCompilation };
1863
+ omCompilationContextCache.set(model, context);
1864
+ return context;
1865
+ }
1859
1866
  function knowledgeTargetExists(index, kind, id) {
1860
1867
  if (kind === "system") return index.systemsById.has(id);
1861
1868
  if (kind === "client") return index.clientIds.has(id);
@@ -2192,9 +2199,8 @@ function refineOrganizationModel(model, ctx) {
2192
2199
  }
2193
2200
  });
2194
2201
  });
2195
- const idx = buildOmCrossRefIndex(model);
2202
+ const { crossRefIndex: idx, ontologyCompilation } = buildOmCompilationContext(model);
2196
2203
  const { ontologyIndexByKind, ontologyIds } = idx;
2197
- const ontologyCompilation = compileOrganizationOntology(model);
2198
2204
  function topologyTargetExists(ref) {
2199
2205
  if (ref.kind === "system") return systemsById.has(ref.id);
2200
2206
  if (ref.kind === "resource") return resourcesById.has(ref.id);
@@ -3369,12 +3375,34 @@ Capture the durable operating knowledge here.
3369
3375
  }
3370
3376
 
3371
3377
  // src/organization-model/scaffolders/scaffoldOntologyRecord.ts
3378
+ function kindSpecificFields(spec) {
3379
+ if (spec.kind === "link") {
3380
+ return {
3381
+ from: makeOntologyId(spec.systemPath, "object", "source-object"),
3382
+ to: makeOntologyId(spec.systemPath, "object", "target-object")
3383
+ };
3384
+ }
3385
+ if (spec.kind === "action") {
3386
+ return { actsOn: [] };
3387
+ }
3388
+ if (spec.kind === "group") {
3389
+ return { members: [] };
3390
+ }
3391
+ return {};
3392
+ }
3372
3393
  function scaffoldOntologyRecord(model, spec) {
3373
3394
  assertSystemExists(model, spec.systemPath);
3374
3395
  const localId = spec.localId ?? spec.id;
3375
3396
  const ontologyId = makeOntologyId(spec.systemPath, spec.kind, localId);
3376
3397
  const label = spec.label ?? titleize(localId);
3377
3398
  const mapName = ontologyMapName(spec.kind);
3399
+ const snippetRecord = {
3400
+ id: ontologyId,
3401
+ label,
3402
+ ownerSystemId: spec.systemPath,
3403
+ ...spec.description === void 0 ? {} : { description: spec.description },
3404
+ ...kindSpecificFields(spec)
3405
+ };
3378
3406
  return {
3379
3407
  intent: "ontology",
3380
3408
  id: ontologyId,
@@ -3384,12 +3412,7 @@ function scaffoldOntologyRecord(model, spec) {
3384
3412
  {
3385
3413
  path: "packages/elevasis-core/src/organization-model/systems.ts",
3386
3414
  description: `Add this record under ${spec.systemPath}.ontology.${mapName}.`,
3387
- snippet: `${JSON.stringify(ontologyId)}: {
3388
- id: ${JSON.stringify(ontologyId)},
3389
- label: ${JSON.stringify(label)},
3390
- ownerSystemId: ${JSON.stringify(spec.systemPath)}${spec.description === void 0 ? "" : `,
3391
- description: ${JSON.stringify(spec.description)}`}
3392
- }`
3415
+ snippet: `${JSON.stringify(ontologyId)}: ${JSON.stringify(snippetRecord, null, 2)}`
3393
3416
  }
3394
3417
  ],
3395
3418
  warnings: [],
@@ -3503,9 +3526,8 @@ function scaffoldSystem(model, spec) {
3503
3526
  content: `import type { SystemModule } from '@repo/ui'
3504
3527
 
3505
3528
  export const ${featureSlug.replaceAll("-", "_")}Manifest: SystemModule = {
3506
- systemId: ${JSON.stringify(systemPath)},
3507
- label: ${JSON.stringify(label)},
3508
- routes: []
3529
+ key: ${JSON.stringify(featureSlug)},
3530
+ systemId: ${JSON.stringify(systemPath)}
3509
3531
  }
3510
3532
  `
3511
3533
  },
@@ -3448,7 +3448,7 @@ interface MockSupabaseFixtures {
3448
3448
  *
3449
3449
  * Usage:
3450
3450
  * ```typescript
3451
- * import { createMockSupabaseClient, TEST_USERS, TEST_ORGS } from '@repo/core/test-utils'
3451
+ * import { createMockSupabaseClient, TEST_USERS, TEST_ORGS } from '@elevasis/core/test-utils'
3452
3452
  *
3453
3453
  * const mockClient = createMockSupabaseClient({
3454
3454
  * users: [TEST_USERS.admin, TEST_USERS.regularUser],
@@ -3501,7 +3501,7 @@ interface UserContext {
3501
3501
  *
3502
3502
  * Usage:
3503
3503
  * ```typescript
3504
- * import { createMockWorkOSClient, TEST_USERS } from '@repo/core/test-utils'
3504
+ * import { createMockWorkOSClient, TEST_USERS } from '@elevasis/core/test-utils'
3505
3505
  *
3506
3506
  * const mockWorkOS = createMockWorkOSClient({
3507
3507
  * validTokens: {
@@ -3542,7 +3542,7 @@ declare function createMockWorkOSClient(options?: {
3542
3542
  *
3543
3543
  * Usage:
3544
3544
  * ```typescript
3545
- * import { createMockVerifyJWT, TEST_USERS } from '@repo/core/test-utils'
3545
+ * import { createMockVerifyJWT, TEST_USERS } from '@elevasis/core/test-utils'
3546
3546
  *
3547
3547
  * const mockVerifyJWT = createMockVerifyJWT({
3548
3548
  * 'valid-token': {
@@ -3843,7 +3843,6 @@ interface SystemEntry {
3843
3843
  status?: 'active' | 'deprecated' | 'archived';
3844
3844
  path?: string;
3845
3845
  icon?: string;
3846
- color?: string;
3847
3846
  uiPosition?: 'sidebar-primary' | 'sidebar-bottom';
3848
3847
  enabled?: boolean;
3849
3848
  devOnly?: boolean;
@@ -20415,8 +20415,6 @@ var SystemEntrySchema = z.object({
20415
20415
  path: PathSchema.optional(),
20416
20416
  /** @deprecated Use ui.icon. Kept for one-cycle Feature compatibility. */
20417
20417
  icon: IconNameSchema.optional(),
20418
- /** @deprecated Feature color token, retained for one-cycle compatibility. */
20419
- color: ColorTokenSchema.optional(),
20420
20418
  /** @deprecated UI placement hint, retained for one-cycle compatibility. */
20421
20419
  uiPosition: UiPositionSchema.optional(),
20422
20420
  /** @deprecated Use lifecycle. */
@@ -20942,7 +20940,8 @@ var ONTOLOGY_REFERENCE_KEY_KINDS = {
20942
20940
  surfaceType: "surface",
20943
20941
  stepCatalog: "catalog"
20944
20942
  };
20945
- function buildOmCrossRefIndex(model) {
20943
+ var omCompilationContextCache = /* @__PURE__ */ new WeakMap();
20944
+ function buildOmCrossRefIndexFromOntology(model, ontologyCompilation) {
20946
20945
  const systemsById = /* @__PURE__ */ new Map();
20947
20946
  for (const { path, system } of listAllSystems(model)) {
20948
20947
  systemsById.set(path, system);
@@ -20956,7 +20955,6 @@ function buildOmCrossRefIndex(model) {
20956
20955
  const actionIds = new Set(Object.keys(model.actions ?? {}));
20957
20956
  const customerSegmentIds = new Set(Object.keys(model.customers ?? {}));
20958
20957
  const offeringIds = new Set(Object.keys(model.offerings ?? {}));
20959
- const ontologyCompilation = compileOrganizationOntology(model);
20960
20958
  const ontologyIndexByKind = {
20961
20959
  object: ontologyCompilation.ontology.objectTypes,
20962
20960
  link: ontologyCompilation.ontology.linkTypes,
@@ -20995,6 +20993,15 @@ function buildOmCrossRefIndex(model) {
20995
20993
  stageIds
20996
20994
  };
20997
20995
  }
20996
+ function buildOmCompilationContext(model) {
20997
+ const cached = omCompilationContextCache.get(model);
20998
+ if (cached !== void 0) return cached;
20999
+ const ontologyCompilation = compileOrganizationOntology(model);
21000
+ const crossRefIndex = buildOmCrossRefIndexFromOntology(model, ontologyCompilation);
21001
+ const context = { crossRefIndex, ontologyCompilation };
21002
+ omCompilationContextCache.set(model, context);
21003
+ return context;
21004
+ }
20998
21005
  function knowledgeTargetExists(index2, kind, id) {
20999
21006
  if (kind === "system") return index2.systemsById.has(id);
21000
21007
  if (kind === "client") return index2.clientIds.has(id);
@@ -21331,9 +21338,8 @@ function refineOrganizationModel(model, ctx) {
21331
21338
  }
21332
21339
  });
21333
21340
  });
21334
- const idx = buildOmCrossRefIndex(model);
21341
+ const { crossRefIndex: idx, ontologyCompilation } = buildOmCompilationContext(model);
21335
21342
  const { ontologyIndexByKind, ontologyIds } = idx;
21336
- const ontologyCompilation = compileOrganizationOntology(model);
21337
21343
  function topologyTargetExists(ref) {
21338
21344
  if (ref.kind === "system") return systemsById.has(ref.id);
21339
21345
  if (ref.kind === "resource") return resourcesById.has(ref.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elevasis/core",
3
- "version": "0.42.0",
3
+ "version": "0.43.0",
4
4
  "license": "MIT",
5
5
  "description": "Minimal shared constants across Elevasis monorepo",
6
6
  "sideEffects": false,
@@ -6,7 +6,15 @@ export const accessTestOrganizationModel = {
6
6
  id: 'platform',
7
7
  label: 'Platform',
8
8
  lifecycle: 'active',
9
- order: 10
9
+ order: 10,
10
+ systems: {
11
+ projects: {
12
+ id: 'projects',
13
+ label: 'Projects',
14
+ lifecycle: 'active',
15
+ order: 10
16
+ }
17
+ }
10
18
  },
11
19
  sales: {
12
20
  id: 'sales',
@@ -28,12 +36,6 @@ export const accessTestOrganizationModel = {
28
36
  }
29
37
  }
30
38
  },
31
- projects: {
32
- id: 'projects',
33
- label: 'Projects',
34
- lifecycle: 'active',
35
- order: 30
36
- },
37
39
  clients: {
38
40
  id: 'clients',
39
41
  label: 'Clients',
@@ -51,14 +51,20 @@ export const AccessKeys = {
51
51
  organizationManage: { systemPath: 'permission.org', action: 'manage' },
52
52
  membersManage: { systemPath: 'permission.members', action: 'manage' },
53
53
  rolesManage: { systemPath: 'permission.roles', action: 'manage' },
54
+ /** @reserved Reserved for first-class secret administration routes and command surfaces. */
54
55
  secretsManage: { systemPath: 'permission.secrets', action: 'manage' },
55
56
  operationsRead: { systemPath: 'permission.operations', action: DEFAULT_ACCESS_ACTION },
57
+ /** @reserved Reserved for write-level operations administration beyond read-only monitoring. */
56
58
  operationsManage: { systemPath: 'permission.operations', action: 'manage' },
57
59
  leadGenManage: { systemPath: 'sales.lead-gen', action: 'manage' },
60
+ /** @reserved Reserved for acquisition administration once that permission namespace is wired. */
58
61
  acquisitionManage: { systemPath: 'permission.acquisition', action: 'manage' },
62
+ /** @reserved Reserved for project administration permission catalog compatibility. */
59
63
  projectsManage: { systemPath: 'permission.projects', action: 'manage' },
64
+ /** @reserved Reserved for client administration permission catalog compatibility. */
60
65
  clientsManage: { systemPath: 'permission.clients', action: 'manage' },
61
66
  operationsOverview: { systemPath: 'diagnostic.operations.overview', action: DEFAULT_ACCESS_ACTION },
67
+ /** @reserved Reserved for a diagnostics surface that lists recent operations executions. */
62
68
  operationsRecentExecutions: { systemPath: 'diagnostic.operations.recent-executions', action: DEFAULT_ACCESS_ACTION },
63
69
  monitoringExecutionLogs: { systemPath: 'diagnostic.monitoring.execution-logs', action: DEFAULT_ACCESS_ACTION },
64
70
  monitoringNotifications: { systemPath: 'diagnostic.monitoring.notifications', action: DEFAULT_ACCESS_ACTION }
@@ -12,7 +12,7 @@ import { z } from 'zod'
12
12
  * layer; use the row types internally for direct Supabase access.
13
13
  *
14
14
  * Usage in a client project:
15
- * import { BaseProject } from '@elevasis/core' (or '@repo/core' internally)
15
+ * import { BaseProject } from '@elevasis/core'
16
16
  * type Project = BaseProject<{ budget: number; riskScore: 'low' | 'medium' | 'high' }>
17
17
  *
18
18
  * Extending a Zod schema in a client project:
@@ -114,7 +114,6 @@ describe('client API schemas', () => {
114
114
  source: 'word_of_mouth',
115
115
  metadata: {
116
116
  externalProjectSlug: 'developer-workspace-slug',
117
- workspacePath: 'client-workspace',
118
117
  campaignSlug: 'byron-for-irvine'
119
118
  }
120
119
  }).success
@@ -159,20 +159,15 @@ export class GmailAdapter implements BaseIntegrationAdapter {
159
159
  requestBody: {
160
160
  raw: encodedMessage
161
161
  }
162
- })
163
-
164
- if (context?.logger) {
165
- console.log('[GmailAdapter] Email sent:', {
166
- organizationId: context.organizationId,
167
- executionId: context.executionId,
168
- to: params.to,
169
- subject: params.subject,
170
- messageId: response.data.id,
171
- threadId: response.data.threadId
172
- })
173
- }
174
-
175
- return {
162
+ })
163
+
164
+ if (context?.logger) {
165
+ context.logger.info(
166
+ `[GmailAdapter] Email sent: organizationId=${context.organizationId} executionId=${context.executionId} messageId=${response.data.id} threadId=${response.data.threadId} to=${params.to}`
167
+ )
168
+ }
169
+
170
+ return {
176
171
  messageId: response.data.id!,
177
172
  threadId: response.data.threadId!
178
173
  }
@@ -0,0 +1,211 @@
1
+ import type {
2
+ OrganizationModel,
3
+ OrganizationModelAction,
4
+ OrganizationModelActionInvocation,
5
+ OrganizationModelAgentResourceEntry
6
+ } from '../organization-model/types'
7
+ import { getSystem } from '../organization-model/helpers'
8
+ import type { listAllResources, listAllRoles, listAllSystemsFlat, OmSearchHitKind } from './queries'
9
+
10
+ export const OM_SEARCH_HIT_KINDS: OmSearchHitKind[] = ['system', 'resource', 'knowledge', 'ontology', 'role', 'policy']
11
+
12
+ export function parseKnowledgeSearchLimit(raw: string | undefined, commandName = 'knowledge:search'): number {
13
+ if (raw === undefined) return 10
14
+ const n = Number.parseInt(raw, 10)
15
+ if (Number.isNaN(n) || n < 0) {
16
+ throw new Error(`${commandName}: --limit must be a non-negative integer (got "${raw}")`)
17
+ }
18
+ return n
19
+ }
20
+
21
+ export function parseKnowledgeSearchKinds(
22
+ raw: string | undefined,
23
+ commandName = 'knowledge:search'
24
+ ): OmSearchHitKind[] | undefined {
25
+ if (raw === undefined) return undefined
26
+ const requested = raw
27
+ .split(',')
28
+ .map((s) => s.trim().toLowerCase())
29
+ .filter((s) => s.length > 0)
30
+ const valid: OmSearchHitKind[] = []
31
+ const invalid: string[] = []
32
+ for (const kind of requested) {
33
+ if (OM_SEARCH_HIT_KINDS.includes(kind as OmSearchHitKind)) {
34
+ valid.push(kind as OmSearchHitKind)
35
+ } else {
36
+ invalid.push(kind)
37
+ }
38
+ }
39
+ if (invalid.length > 0) {
40
+ throw new Error(`${commandName}: unknown kind(s) "${invalid.join(', ')}". Valid: ${OM_SEARCH_HIT_KINDS.join(', ')}`)
41
+ }
42
+ return valid.length > 0 ? valid : undefined
43
+ }
44
+
45
+ export function formatKnowledgeSystemsList(entries: ReturnType<typeof listAllSystemsFlat>): string {
46
+ if (entries.length === 0) return '(no results)'
47
+ const pathWidth = Math.max(...entries.map((e) => e.path.length), 4)
48
+ const header = `${'PATH'.padEnd(pathWidth)} LABEL`
49
+ const divider = '-'.repeat(header.length + 20)
50
+ const rows = entries.map((e) => {
51
+ const label = e.system.label ?? e.system.title ?? e.path
52
+ return `${e.path.padEnd(pathWidth)} ${label}`
53
+ })
54
+ return [header, divider, ...rows].join('\n')
55
+ }
56
+
57
+ export function formatKnowledgeResourcesList(resources: ReturnType<typeof listAllResources>): string {
58
+ if (resources.length === 0) return '(no results)'
59
+ const idWidth = Math.max(...resources.map((r) => r.id.length), 4)
60
+ const kindWidth = Math.max(...resources.map((r) => r.kind.length), 4)
61
+ const header = `${'ID'.padEnd(idWidth)} ${'KIND'.padEnd(kindWidth)} TITLE`
62
+ const divider = '-'.repeat(header.length + 20)
63
+ const rows = resources.map((r) => {
64
+ const title = r.title ?? r.id
65
+ return `${r.id.padEnd(idWidth)} ${r.kind.padEnd(kindWidth)} ${title}`
66
+ })
67
+ return [header, divider, ...rows].join('\n')
68
+ }
69
+
70
+ export function formatKnowledgeRolesList(roles: ReturnType<typeof listAllRoles>): string {
71
+ if (roles.length === 0) return '(no results)'
72
+ const idWidth = Math.max(...roles.map((r) => r.id.length), 4)
73
+ const header = `${'ID'.padEnd(idWidth)} TITLE`
74
+ const divider = '-'.repeat(header.length + 20)
75
+ const rows = roles.map((r) => `${r.id.padEnd(idWidth)} ${r.title}`)
76
+ return [header, divider, ...rows].join('\n')
77
+ }
78
+
79
+ export interface KnowledgeInvocationSource {
80
+ kind: 'action' | 'agent-resource'
81
+ id: string
82
+ label: string
83
+ targetNodeId: string
84
+ invocations: OrganizationModelActionInvocation[]
85
+ }
86
+
87
+ export interface KnowledgeInvocationNeighborsResult {
88
+ id: string
89
+ title: string
90
+ invocationSources: KnowledgeInvocationSource[]
91
+ }
92
+
93
+ export function normalizeKnowledgeNodeId(id: string): string {
94
+ return id.startsWith('knowledge:') ? id.slice('knowledge:'.length) : id
95
+ }
96
+
97
+ export function formatKnowledgeInvocationLabel(invocation: OrganizationModelActionInvocation): string {
98
+ switch (invocation.kind) {
99
+ case 'slash-command':
100
+ return invocation.command
101
+ case 'mcp-tool':
102
+ return `${invocation.server}/${invocation.name}`
103
+ case 'api-endpoint':
104
+ return `${invocation.method} ${invocation.path}`
105
+ case 'script-execution':
106
+ return invocation.resourceId
107
+ }
108
+ }
109
+
110
+ function pushActionSource(
111
+ sources: KnowledgeInvocationSource[],
112
+ seen: Set<string>,
113
+ action: OrganizationModelAction | undefined
114
+ ): void {
115
+ if (!action || action.invocations.length === 0) return
116
+
117
+ const key = `action:${action.id}`
118
+ if (seen.has(key)) return
119
+ seen.add(key)
120
+
121
+ sources.push({
122
+ kind: 'action',
123
+ id: action.id,
124
+ label: action.label,
125
+ targetNodeId: `action:${action.id}`,
126
+ invocations: action.invocations
127
+ })
128
+ }
129
+
130
+ function pushAgentResourceSource(
131
+ sources: KnowledgeInvocationSource[],
132
+ seen: Set<string>,
133
+ resource: OrganizationModel['resources'][string] | undefined
134
+ ): void {
135
+ if (!resource || resource.kind !== 'agent' || resource.invocations.length === 0) return
136
+
137
+ const agentResource = resource as OrganizationModelAgentResourceEntry
138
+ const key = `agent-resource:${agentResource.id}`
139
+ if (seen.has(key)) return
140
+ seen.add(key)
141
+
142
+ sources.push({
143
+ kind: 'agent-resource',
144
+ id: agentResource.id,
145
+ label: agentResource.id,
146
+ targetNodeId: `resource:${agentResource.id}`,
147
+ invocations: agentResource.invocations
148
+ })
149
+ }
150
+
151
+ export function resolveKnowledgeInvocationNeighbors(
152
+ model: OrganizationModel,
153
+ id: string
154
+ ): KnowledgeInvocationNeighborsResult | undefined {
155
+ const normalizedNodeId = normalizeKnowledgeNodeId(id)
156
+ const node = model.knowledge[normalizedNodeId]
157
+ if (!node) return undefined
158
+
159
+ const sources: KnowledgeInvocationSource[] = []
160
+ const seen = new Set<string>()
161
+
162
+ for (const link of node.links) {
163
+ const target = link.target
164
+
165
+ if (target.kind === 'action') {
166
+ pushActionSource(sources, seen, model.actions[target.id])
167
+ continue
168
+ }
169
+
170
+ if (target.kind === 'resource') {
171
+ pushAgentResourceSource(sources, seen, model.resources[target.id])
172
+ continue
173
+ }
174
+
175
+ if (target.kind === 'system') {
176
+ const system = getSystem(model, target.id)
177
+ for (const actionRef of system?.actions ?? []) {
178
+ pushActionSource(sources, seen, model.actions[actionRef.actionId])
179
+ }
180
+
181
+ for (const resource of Object.values(model.resources)) {
182
+ if (resource.systemPath === target.id) {
183
+ pushAgentResourceSource(sources, seen, resource)
184
+ }
185
+ }
186
+ }
187
+ }
188
+
189
+ return {
190
+ id: node.id,
191
+ title: node.title,
192
+ invocationSources: sources.sort((a, b) => a.targetNodeId.localeCompare(b.targetNodeId))
193
+ }
194
+ }
195
+
196
+ export function formatKnowledgeInvocationNeighbors(result: KnowledgeInvocationNeighborsResult): string {
197
+ const lines = [`Invocation-bearing graph neighbors for ${result.id}`, '']
198
+
199
+ if (result.invocationSources.length === 0) {
200
+ lines.push(' (none)')
201
+ } else {
202
+ for (const source of result.invocationSources) {
203
+ lines.push(`${source.kind}: ${source.id}`)
204
+ for (const invocation of source.invocations) {
205
+ lines.push(` ${invocation.kind}: ${formatKnowledgeInvocationLabel(invocation)}`)
206
+ }
207
+ }
208
+ }
209
+
210
+ return lines.join('\n')
211
+ }
@@ -29,3 +29,16 @@ export type {
29
29
 
30
30
  export { formatText, formatJson, formatIdsOnly, formatOmSearchHits, formatOmDescribe } from './format'
31
31
  export type { KnowledgeJsonEnvelope } from './format'
32
+ export {
33
+ OM_SEARCH_HIT_KINDS,
34
+ parseKnowledgeSearchLimit,
35
+ parseKnowledgeSearchKinds,
36
+ formatKnowledgeSystemsList,
37
+ formatKnowledgeResourcesList,
38
+ formatKnowledgeRolesList,
39
+ normalizeKnowledgeNodeId,
40
+ formatKnowledgeInvocationLabel,
41
+ resolveKnowledgeInvocationNeighbors,
42
+ formatKnowledgeInvocationNeighbors
43
+ } from './cli-helpers'
44
+ export type { KnowledgeInvocationSource, KnowledgeInvocationNeighborsResult } from './cli-helpers'
@@ -1,5 +1,18 @@
1
- export { bySystem, byKind, byOwner, governs, governedBy, parsePath } from './queries'
2
- export type { KnowledgeMount, ParsedKnowledgePath } from './queries'
3
-
4
- export { formatText, formatJson, formatIdsOnly } from './format'
5
- export type { KnowledgeJsonEnvelope } from './format'
1
+ export { bySystem, byKind, byOwner, governs, governedBy, parsePath } from './queries'
2
+ export type { KnowledgeMount, ParsedKnowledgePath, OmSearchHitKind } from './queries'
3
+
4
+ export { formatText, formatJson, formatIdsOnly } from './format'
5
+ export type { KnowledgeJsonEnvelope } from './format'
6
+ export {
7
+ OM_SEARCH_HIT_KINDS,
8
+ parseKnowledgeSearchLimit,
9
+ parseKnowledgeSearchKinds,
10
+ formatKnowledgeSystemsList,
11
+ formatKnowledgeResourcesList,
12
+ formatKnowledgeRolesList,
13
+ normalizeKnowledgeNodeId,
14
+ formatKnowledgeInvocationLabel,
15
+ resolveKnowledgeInvocationNeighbors,
16
+ formatKnowledgeInvocationNeighbors
17
+ } from './cli-helpers'
18
+ export type { KnowledgeInvocationSource, KnowledgeInvocationNeighborsResult } from './cli-helpers'
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import { DEFAULT_ORGANIZATION_MODEL } from '../defaults'
3
- import { buildOmCrossRefIndex, knowledgeTargetExists } from '../cross-ref'
3
+ import { buildOmCompilationContext, buildOmCrossRefIndex, knowledgeTargetExists } from '../cross-ref'
4
4
  import { OrganizationModelSchema } from '../schema'
5
5
  import type { OrganizationModel } from '../types'
6
6
 
@@ -80,6 +80,16 @@ function makeModelWithStage(): OrganizationModel {
80
80
  // ---------------------------------------------------------------------------
81
81
 
82
82
  describe('knowledgeTargetExists — stage kind', () => {
83
+ it('buildOmCompilationContext returns a memoized cross-ref index plus ontology compilation', () => {
84
+ const model = makeModelWithStage()
85
+ const context = buildOmCompilationContext(model)
86
+
87
+ expect(buildOmCompilationContext(model)).toBe(context)
88
+ expect(context.crossRefIndex.stageIds.has('prospect')).toBe(true)
89
+ expect(context.ontologyCompilation.ontology.catalogTypes['sales.lead-gen:catalog/company-stage']).toBeDefined()
90
+ expect(buildOmCrossRefIndex(model)).toBe(context.crossRefIndex)
91
+ })
92
+
83
93
  it('returns true for a stage id that exists in a stage-kind catalog', () => {
84
94
  const model = makeModelWithStage()
85
95
  const idx = buildOmCrossRefIndex(model)
@@ -123,10 +123,10 @@ describe('SystemEntrySchema - positive parse', () => {
123
123
  }
124
124
  })
125
125
 
126
- it('accepts deprecated status and maps it to lifecycle for one-cycle compatibility', () => {
127
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
128
-
129
- const result = SystemEntrySchema.safeParse({
126
+ it('accepts deprecated status and maps it to lifecycle for one-cycle compatibility', () => {
127
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
128
+
129
+ const result = SystemEntrySchema.safeParse({
130
130
  ...VALID_SYSTEM,
131
131
  status: 'deprecated'
132
132
  })
@@ -137,10 +137,32 @@ describe('SystemEntrySchema - positive parse', () => {
137
137
  expect(result.data.lifecycle).toBe('deprecated')
138
138
  }
139
139
  expect(warn).toHaveBeenCalledWith('[organization-model] System.status is deprecated; use System.lifecycle instead.')
140
-
141
- warn.mockRestore()
142
- })
143
- })
140
+
141
+ warn.mockRestore()
142
+ })
143
+
144
+ it('keeps non-color compatibility fields readable', () => {
145
+ const result = SystemEntrySchema.safeParse({
146
+ ...VALID_SYSTEM,
147
+ path: '/lead-gen',
148
+ icon: 'settings',
149
+ uiPosition: 'sidebar-primary',
150
+ enabled: true,
151
+ devOnly: false
152
+ })
153
+
154
+ expect(result.success).toBe(true)
155
+ if (result.success) {
156
+ expect(result.data).toMatchObject({
157
+ path: '/lead-gen',
158
+ icon: 'settings',
159
+ uiPosition: 'sidebar-primary',
160
+ enabled: true,
161
+ devOnly: false
162
+ })
163
+ }
164
+ })
165
+ })
144
166
 
145
167
  describe('SystemEntrySchema - negative parse', () => {
146
168
  it('rejects missing required fields', () => {
@@ -159,6 +181,10 @@ describe('SystemEntrySchema - negative parse', () => {
159
181
  expect(SystemEntrySchema.safeParse({ ...VALID_SYSTEM, id: 'Sys Lead Gen' }).success).toBe(false)
160
182
  })
161
183
 
184
+ it('rejects the retired color compatibility field', () => {
185
+ expect(SystemEntrySchema.safeParse({ ...VALID_SYSTEM, color: 'blue' }).success).toBe(false)
186
+ })
187
+
162
188
  it('rejects malformed System Interface states', () => {
163
189
  expect(SystemApiInterfaceSchema.safeParse({ kind: 'api', lifecycle: 'active' }).success).toBe(false)
164
190
  expect(SystemApiInterfaceSchema.safeParse({ lifecycle: 'paused' }).success).toBe(false)