@elevasis/core 0.24.0 → 0.24.1

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 (32) hide show
  1. package/dist/index.d.ts +3192 -2313
  2. package/dist/index.js +243 -13
  3. package/dist/knowledge/index.d.ts +92 -6
  4. package/dist/organization-model/index.d.ts +3192 -2313
  5. package/dist/organization-model/index.js +243 -13
  6. package/dist/test-utils/index.d.ts +134 -45
  7. package/dist/test-utils/index.js +118 -11
  8. package/package.json +3 -3
  9. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +45 -7
  10. package/src/execution/engine/workflow/types.ts +5 -7
  11. package/src/organization-model/__tests__/domains/resources.test.ts +19 -8
  12. package/src/organization-model/__tests__/domains/topology.test.ts +188 -0
  13. package/src/organization-model/__tests__/graph.test.ts +98 -7
  14. package/src/organization-model/__tests__/schema.test.ts +14 -4
  15. package/src/organization-model/defaults.ts +2 -0
  16. package/src/organization-model/domains/resources.ts +63 -20
  17. package/src/organization-model/domains/topology.ts +261 -0
  18. package/src/organization-model/graph/build.ts +63 -15
  19. package/src/organization-model/graph/schema.ts +4 -3
  20. package/src/organization-model/graph/types.ts +5 -4
  21. package/src/organization-model/index.ts +4 -3
  22. package/src/organization-model/ontology.ts +2 -5
  23. package/src/organization-model/organization-model.mdx +16 -11
  24. package/src/organization-model/published.ts +36 -13
  25. package/src/organization-model/schema.ts +51 -11
  26. package/src/organization-model/types.ts +25 -11
  27. package/src/platform/registry/__tests__/validation.test.ts +199 -14
  28. package/src/platform/registry/resource-registry.ts +11 -11
  29. package/src/platform/registry/validation.ts +226 -34
  30. package/src/reference/_generated/contracts.md +45 -7
  31. package/src/reference/glossary.md +3 -3
  32. package/src/supabase/database.types.ts +3156 -3153
@@ -31,12 +31,13 @@ const VALID_ROLE = {
31
31
  const WORKFLOW_RESOURCE = {
32
32
  id: 'LGN-01-company-scrape',
33
33
  order: 10,
34
- kind: 'workflow' as const,
35
- systemPath: 'sys.lead-gen',
36
- ownerRoleId: 'role.sales-ops',
37
- status: 'active' as const,
38
- actionKey: 'lead-gen.company.scrape',
39
- codeRefs: [
34
+ kind: 'workflow' as const,
35
+ systemPath: 'sys.lead-gen',
36
+ title: 'Company Scrape',
37
+ description: 'Scrapes company data for lead generation.',
38
+ ownerRoleId: 'role.sales-ops',
39
+ status: 'active' as const,
40
+ codeRefs: [
40
41
  {
41
42
  path: 'operations/src/lead-gen/company-scrape/index.ts',
42
43
  role: 'entrypoint' as const,
@@ -231,7 +232,8 @@ describe('ResourceEntrySchema', () => {
231
232
 
232
233
  it('accepts optional ontology bindings on resource variants without changing top-level event emissions', () => {
233
234
  const ontology = {
234
- implements: ['sys.lead-gen:action/company.scrape'],
235
+ actions: ['sys.lead-gen:action/company.scrape'],
236
+ primaryAction: 'sys.lead-gen:action/company.scrape',
235
237
  reads: ['sys.lead-gen:object/company'],
236
238
  writes: ['sys.lead-gen:object/company'],
237
239
  usesCatalogs: ['sys.lead-gen:catalog/build-template'],
@@ -268,7 +270,16 @@ describe('ResourceEntrySchema', () => {
268
270
  it('rejects malformed ontology binding ids', () => {
269
271
  expect(
270
272
  ResourceOntologyBindingSchema.safeParse({
271
- implements: ['sys.lead-gen/action/company.scrape']
273
+ actions: ['sys.lead-gen/action/company.scrape']
274
+ }).success
275
+ ).toBe(false)
276
+ })
277
+
278
+ it('rejects primaryAction values outside actions', () => {
279
+ expect(
280
+ ResourceOntologyBindingSchema.safeParse({
281
+ actions: ['sys.lead-gen:action/company.scrape'],
282
+ primaryAction: 'sys.lead-gen:action/company.qualify'
272
283
  }).success
273
284
  ).toBe(false)
274
285
  })
@@ -0,0 +1,188 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { ZodError } from 'zod'
3
+ import {
4
+ OmTopologyDomainSchema,
5
+ OmTopologyMetadataSchema,
6
+ defineTopology,
7
+ defineTopologyRelationship,
8
+ parseTopologyNodeRef,
9
+ topologyRef
10
+ } from '../../domains/topology'
11
+ import { resolveOrganizationModel } from '../../resolve'
12
+
13
+ const VALID_SYSTEM = {
14
+ id: 'sales',
15
+ order: 10,
16
+ label: 'Sales',
17
+ enabled: true,
18
+ lifecycle: 'active' as const
19
+ }
20
+
21
+ const SOURCE_RESOURCE = {
22
+ id: 'email-discovery',
23
+ order: 10,
24
+ kind: 'workflow' as const,
25
+ systemPath: 'sales',
26
+ status: 'active' as const
27
+ }
28
+
29
+ const TARGET_RESOURCE = {
30
+ id: 'email-verification',
31
+ order: 20,
32
+ kind: 'workflow' as const,
33
+ systemPath: 'sales',
34
+ status: 'active' as const
35
+ }
36
+
37
+ describe('OM topology domain', () => {
38
+ it('accepts first-class topology relationships', () => {
39
+ const model = resolveOrganizationModel({
40
+ systems: { sales: VALID_SYSTEM },
41
+ resources: {
42
+ 'email-discovery': SOURCE_RESOURCE,
43
+ 'email-verification': TARGET_RESOURCE
44
+ },
45
+ topology: {
46
+ version: 1,
47
+ relationships: {
48
+ 'email-discovery-triggers-email-verification': {
49
+ from: { kind: 'resource', id: 'email-discovery' },
50
+ kind: 'triggers',
51
+ to: { kind: 'resource', id: 'email-verification' },
52
+ systemPath: 'sales',
53
+ required: true,
54
+ metadata: { source: 'authored' }
55
+ }
56
+ }
57
+ }
58
+ })
59
+
60
+ expect(model.topology.relationships['email-discovery-triggers-email-verification']).toMatchObject({
61
+ from: { kind: 'resource', id: 'email-discovery' },
62
+ kind: 'triggers',
63
+ to: { kind: 'resource', id: 'email-verification' },
64
+ required: true
65
+ })
66
+ })
67
+
68
+ it('rejects dangling first-class topology refs', () => {
69
+ expect(() =>
70
+ resolveOrganizationModel({
71
+ systems: { sales: VALID_SYSTEM },
72
+ resources: { 'email-discovery': SOURCE_RESOURCE },
73
+ topology: {
74
+ version: 1,
75
+ relationships: {
76
+ 'email-discovery-triggers-missing': {
77
+ from: { kind: 'resource', id: 'email-discovery' },
78
+ kind: 'triggers',
79
+ to: { kind: 'resource', id: 'missing-resource' }
80
+ }
81
+ }
82
+ }
83
+ })
84
+ ).toThrow(/unknown resource \\"missing-resource\\"/)
85
+ })
86
+
87
+ it('reports dangling topology refs on the relationship side path', () => {
88
+ let error: unknown
89
+
90
+ try {
91
+ resolveOrganizationModel({
92
+ systems: { sales: VALID_SYSTEM },
93
+ resources: { 'email-discovery': SOURCE_RESOURCE },
94
+ topology: {
95
+ version: 1,
96
+ relationships: {
97
+ 'email-discovery-triggers-missing': {
98
+ from: { kind: 'resource', id: 'email-discovery' },
99
+ kind: 'triggers',
100
+ to: { kind: 'resource', id: 'missing-resource' }
101
+ }
102
+ }
103
+ }
104
+ })
105
+ } catch (caught) {
106
+ error = caught
107
+ }
108
+
109
+ expect(error).toBeInstanceOf(ZodError)
110
+ if (!(error instanceof ZodError)) {
111
+ throw new Error('Expected resolveOrganizationModel to throw a ZodError')
112
+ }
113
+ expect(error.issues).toEqual(
114
+ expect.arrayContaining([
115
+ expect.objectContaining({
116
+ path: ['topology', 'relationships', 'email-discovery-triggers-missing', 'to']
117
+ })
118
+ ])
119
+ )
120
+ })
121
+
122
+ it('rejects secret-like topology metadata', () => {
123
+ expect(
124
+ OmTopologyMetadataSchema.safeParse({
125
+ owner: 'sales-ops',
126
+ credentials: { apiKey: 'sk-test-12345678901234567890' }
127
+ }).success
128
+ ).toBe(false)
129
+ })
130
+
131
+ it('compiles typed resource objects through authoring helpers', () => {
132
+ const relationship = defineTopologyRelationship({
133
+ from: SOURCE_RESOURCE,
134
+ kind: 'uses',
135
+ to: topologyRef.externalResource('apollo'),
136
+ required: true
137
+ })
138
+
139
+ expect(relationship).toEqual({
140
+ from: { kind: 'resource', id: 'email-discovery' },
141
+ kind: 'uses',
142
+ to: { kind: 'externalResource', id: 'apollo' },
143
+ required: true
144
+ })
145
+
146
+ expect(
147
+ defineTopology({
148
+ 'email-discovery-uses-apollo': {
149
+ from: SOURCE_RESOURCE,
150
+ kind: 'uses',
151
+ to: topologyRef.externalResource('apollo')
152
+ }
153
+ })
154
+ ).toMatchObject({
155
+ version: 1,
156
+ relationships: {
157
+ 'email-discovery-uses-apollo': {
158
+ from: { kind: 'resource', id: 'email-discovery' },
159
+ to: { kind: 'externalResource', id: 'apollo' }
160
+ }
161
+ }
162
+ })
163
+ })
164
+
165
+ it('parses boundary string refs without making raw strings canonical', () => {
166
+ expect(parseTopologyNodeRef('resource:email-discovery')).toEqual({
167
+ kind: 'resource',
168
+ id: 'email-discovery'
169
+ })
170
+ expect(parseTopologyNodeRef('ontology:sales:action/contact.verify-email')).toEqual({
171
+ kind: 'ontology',
172
+ id: 'sales:action/contact.verify-email'
173
+ })
174
+ expect(() => parseTopologyNodeRef('email-discovery')).toThrow(/must use <kind>:<id>/)
175
+ expect(() =>
176
+ OmTopologyDomainSchema.parse({
177
+ version: 1,
178
+ relationships: {
179
+ invalid: {
180
+ from: 'resource:email-discovery',
181
+ kind: 'uses',
182
+ to: { kind: 'resource', id: 'email-verification' }
183
+ }
184
+ }
185
+ })
186
+ ).toThrow()
187
+ })
188
+ })
@@ -115,15 +115,16 @@ describe('organization graph', () => {
115
115
  expect(() => OrganizationGraphEdgeKindSchema.parse('links')).not.toThrow()
116
116
  expect(() => OrganizationGraphEdgeKindSchema.parse('affects')).not.toThrow()
117
117
  expect(() => OrganizationGraphEdgeKindSchema.parse('emits')).not.toThrow()
118
- expect(() => OrganizationGraphEdgeKindSchema.parse('originates_from')).not.toThrow()
119
- expect(() => OrganizationGraphEdgeKindSchema.parse('triggers')).not.toThrow()
118
+ expect(() => OrganizationGraphEdgeKindSchema.parse('originates_from')).not.toThrow()
119
+ expect(() => OrganizationGraphEdgeKindSchema.parse('triggers')).not.toThrow()
120
+ expect(() => OrganizationGraphEdgeKindSchema.parse('approval')).not.toThrow()
120
121
  expect(() => OrganizationGraphNodeKindSchema.parse('surface')).not.toThrow()
121
122
  expect(() => OrganizationGraphNodeKindSchema.parse('customer-segment')).not.toThrow()
122
123
  expect(() => OrganizationGraphNodeKindSchema.parse('offering')).not.toThrow()
123
124
  expect(() => OrganizationGraphNodeKindSchema.parse('goal')).not.toThrow()
124
125
  expect(() => OrganizationGraphNodeKindSchema.parse('navigation-group')).not.toThrow()
125
126
  expect(() => OrganizationGraphNodeKindSchema.parse('ontology')).not.toThrow()
126
- expect(() => OrganizationGraphEdgeKindSchema.parse('implements')).not.toThrow()
127
+ expect(() => OrganizationGraphEdgeKindSchema.parse('actions')).not.toThrow()
127
128
  expect(() => OrganizationGraphEdgeKindSchema.parse('reads')).not.toThrow()
128
129
  expect(() => OrganizationGraphEdgeKindSchema.parse('writes')).not.toThrow()
129
130
  expect(() => OrganizationGraphEdgeKindSchema.parse('uses_catalog')).not.toThrow()
@@ -431,7 +432,8 @@ describe('organization graph', () => {
431
432
  systemPath: 'test.bindings',
432
433
  status: 'active',
433
434
  ontology: {
434
- implements: ['test.bindings:action/update-deal'],
435
+ actions: ['test.bindings:action/update-deal'],
436
+ primaryAction: 'test.bindings:action/update-deal',
435
437
  reads: ['test.bindings:object/deal'],
436
438
  writes: ['test.bindings:object/deal'],
437
439
  usesCatalogs: ['test.bindings:catalog/pipeline'],
@@ -445,7 +447,7 @@ describe('organization graph', () => {
445
447
  expect(
446
448
  graph.edges.find(
447
449
  (edge) =>
448
- edge.kind === 'implements' &&
450
+ edge.kind === 'actions' &&
449
451
  edge.sourceId === 'resource:deal-workflow' &&
450
452
  edge.targetId === 'ontology:test.bindings:action/update-deal'
451
453
  )
@@ -483,8 +485,97 @@ describe('organization graph', () => {
483
485
  )
484
486
  ).toBeDefined()
485
487
  })
486
-
487
- it('projects surface and navigation-group nodes from recursive sidebar leaves', () => {
488
+
489
+ it('projects topology relationships without collapsing ontology binding edges', () => {
490
+ const model = resolveOrganizationModel({
491
+ systems: {
492
+ 'test.topology': {
493
+ id: 'test.topology',
494
+ order: 10,
495
+ label: 'Topology System',
496
+ enabled: true,
497
+ ontology: {
498
+ actionTypes: {
499
+ 'test.topology:action/discover-email': {
500
+ id: 'test.topology:action/discover-email',
501
+ label: 'Discover Email'
502
+ }
503
+ }
504
+ }
505
+ }
506
+ },
507
+ resources: {
508
+ 'email-discovery': {
509
+ id: 'email-discovery',
510
+ order: 10,
511
+ kind: 'workflow',
512
+ systemPath: 'test.topology',
513
+ status: 'active',
514
+ ontology: {
515
+ actions: ['test.topology:action/discover-email'],
516
+ primaryAction: 'test.topology:action/discover-email'
517
+ }
518
+ },
519
+ 'email-verification': {
520
+ id: 'email-verification',
521
+ order: 20,
522
+ kind: 'workflow',
523
+ systemPath: 'test.topology',
524
+ status: 'active'
525
+ }
526
+ },
527
+ topology: {
528
+ version: 1,
529
+ relationships: {
530
+ 'email-discovery-triggers-email-verification': {
531
+ from: { kind: 'resource', id: 'email-discovery' },
532
+ kind: 'triggers',
533
+ to: { kind: 'resource', id: 'email-verification' },
534
+ required: true
535
+ },
536
+ 'email-verification-approval-human-review': {
537
+ from: { kind: 'resource', id: 'email-verification' },
538
+ kind: 'approval',
539
+ to: { kind: 'humanCheckpoint', id: 'email-review' }
540
+ }
541
+ }
542
+ }
543
+ })
544
+ const graph = buildOrganizationGraph({ organizationModel: model })
545
+
546
+ expect(
547
+ graph.edges.find(
548
+ (edge) =>
549
+ edge.kind === 'actions' &&
550
+ edge.sourceId === 'resource:email-discovery' &&
551
+ edge.targetId === 'ontology:test.topology:action/discover-email'
552
+ )
553
+ ).toBeDefined()
554
+ expect(
555
+ graph.edges.find(
556
+ (edge) =>
557
+ edge.kind === 'triggers' &&
558
+ edge.relationshipType === 'triggers' &&
559
+ edge.sourceId === 'resource:email-discovery' &&
560
+ edge.targetId === 'resource:email-verification'
561
+ )
562
+ ).toBeDefined()
563
+ expect(graph.nodes.find((node) => node.id === 'resource:email-review')).toMatchObject({
564
+ kind: 'resource',
565
+ resourceType: 'human_checkpoint'
566
+ })
567
+ expect(
568
+ graph.edges.find(
569
+ (edge) =>
570
+ edge.kind === 'approval' &&
571
+ edge.relationshipType === 'approval' &&
572
+ edge.sourceId === 'resource:email-verification' &&
573
+ edge.targetId === 'resource:email-review'
574
+ )
575
+ ).toBeDefined()
576
+ })
577
+
578
+ it('projects surface and navigation-group nodes from recursive sidebar leaves', () => {
488
579
  const model = resolveOrganizationModel({
489
580
  navigation: {
490
581
  sidebar: {
@@ -333,9 +333,17 @@ describe('ontology contract validation', () => {
333
333
  const compilation = compileOrganizationOntology(model)
334
334
 
335
335
  expect(compilation.diagnostics).toEqual([])
336
- expect(compilation.ontology.objectTypes['dashboard:object/crm.deal']?.legacyEntityId).toBe('crm.deal')
336
+ expect(compilation.ontology.objectTypes['dashboard:object/crm.deal']).not.toHaveProperty('legacyEntityId')
337
+ expect(compilation.ontology.objectTypes['dashboard:object/crm.deal']?.origin).toMatchObject({
338
+ source: 'legacy.entities',
339
+ legacyId: 'crm.deal'
340
+ })
337
341
  expect(compilation.ontology.actionTypes['dashboard:action/send_reply']?.legacyActionId).toBe('send_reply')
338
- expect(compilation.ontology.catalogTypes['dashboard:catalog/pipeline']?.legacyContentId).toBe('dashboard:pipeline')
342
+ expect(compilation.ontology.catalogTypes['dashboard:catalog/pipeline']).not.toHaveProperty('legacyContentId')
343
+ expect(compilation.ontology.catalogTypes['dashboard:catalog/pipeline']?.origin).toMatchObject({
344
+ source: 'legacy.system.content',
345
+ legacyId: 'dashboard:pipeline'
346
+ })
339
347
  })
340
348
 
341
349
  it('adds origin metadata to authored and projected compiled ontology records without mutating source', () => {
@@ -477,7 +485,8 @@ describe('ontology contract validation', () => {
477
485
  systemPath: 'dashboard',
478
486
  status: 'active',
479
487
  ontology: {
480
- implements: ['dashboard:action/process-task'],
488
+ actions: ['dashboard:action/process-task'],
489
+ primaryAction: 'dashboard:action/process-task',
481
490
  reads: ['dashboard:object/task'],
482
491
  writes: ['dashboard:object/task'],
483
492
  usesCatalogs: ['dashboard:catalog/task-status'],
@@ -605,7 +614,8 @@ describe('ontology contract validation', () => {
605
614
  systemPath: 'dashboard',
606
615
  status: 'active',
607
616
  ontology: {
608
- implements: ['dashboard:action/missing-task'],
617
+ actions: ['dashboard:action/missing-task'],
618
+ primaryAction: 'dashboard:action/missing-task',
609
619
  reads: ['dashboard:object/missing-task'],
610
620
  usesCatalogs: ['dashboard:catalog/missing-status'],
611
621
  emits: ['dashboard:event/missing-event']
@@ -17,6 +17,7 @@ import { DEFAULT_ORGANIZATION_MODEL_OFFERINGS } from './domains/offerings'
17
17
  import { DEFAULT_ORGANIZATION_MODEL_ROLES } from './domains/roles'
18
18
  import { DEFAULT_ORGANIZATION_MODEL_GOALS } from './domains/goals'
19
19
  import { DEFAULT_ORGANIZATION_MODEL_RESOURCES } from './domains/resources'
20
+ import { DEFAULT_ORGANIZATION_MODEL_TOPOLOGY } from './domains/topology'
20
21
  import { CRM_ACTION_ENTRIES, DEFAULT_ORGANIZATION_MODEL_ACTIONS, LEAD_GEN_ACTION_ENTRIES } from './domains/actions'
21
22
  import { DEFAULT_ORGANIZATION_MODEL_ENTITIES as DEFAULT_ENTITIES } from './domains/entities'
22
23
  import { DEFAULT_ORGANIZATION_MODEL_POLICIES } from './domains/policies'
@@ -741,6 +742,7 @@ export const DEFAULT_ORGANIZATION_MODEL: OrganizationModel = {
741
742
  },
742
743
  ontology: DEFAULT_ONTOLOGY_SCOPE,
743
744
  resources: DEFAULT_ORGANIZATION_MODEL_RESOURCES,
745
+ topology: DEFAULT_ORGANIZATION_MODEL_TOPOLOGY,
744
746
  actions: DEFAULT_ORGANIZATION_MODEL_ACTIONS,
745
747
  entities: DEFAULT_ORGANIZATION_MODEL_ENTITIES,
746
748
  policies: DEFAULT_ORGANIZATION_MODEL_POLICIES,
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod'
2
- import { ModelIdSchema } from './shared'
2
+ import { DescriptionSchema, LabelSchema, ModelIdSchema } from './shared'
3
3
  import { SystemPathSchema, SystemLifecycleSchema } from './systems'
4
4
  import { ActionInvocationSchema } from './actions'
5
5
  import { OntologyIdSchema } from '../ontology'
@@ -57,13 +57,35 @@ export const EventDescriptorSchema = EventEmissionDescriptorSchema.extend({
57
57
  ownerKind: z.enum(['resource', 'entity']).meta({ label: 'Owner kind' })
58
58
  })
59
59
 
60
- export const ResourceOntologyBindingSchema = z.object({
61
- implements: z.array(OntologyIdSchema).optional(),
62
- reads: z.array(OntologyIdSchema).optional(),
63
- writes: z.array(OntologyIdSchema).optional(),
64
- usesCatalogs: z.array(OntologyIdSchema).optional(),
65
- emits: z.array(OntologyIdSchema).optional()
66
- })
60
+ export const ResourceOntologyBindingSchema = z
61
+ .object({
62
+ actions: z.array(OntologyIdSchema).optional(),
63
+ primaryAction: OntologyIdSchema.optional(),
64
+ reads: z.array(OntologyIdSchema).optional(),
65
+ writes: z.array(OntologyIdSchema).optional(),
66
+ usesCatalogs: z.array(OntologyIdSchema).optional(),
67
+ emits: z.array(OntologyIdSchema).optional()
68
+ })
69
+ .superRefine((binding, ctx) => {
70
+ if (binding.primaryAction === undefined) return
71
+ if (binding.actions?.includes(binding.primaryAction)) return
72
+
73
+ ctx.addIssue({
74
+ code: z.ZodIssueCode.custom,
75
+ path: ['primaryAction'],
76
+ message: 'Resource ontology primaryAction must be included in actions'
77
+ })
78
+ })
79
+
80
+ type OntologyRefInput = string | { id: string }
81
+ type ResourceOntologyBindingInput = {
82
+ actions?: OntologyRefInput[]
83
+ primaryAction?: OntologyRefInput
84
+ reads?: OntologyRefInput[]
85
+ writes?: OntologyRefInput[]
86
+ usesCatalogs?: OntologyRefInput[]
87
+ emits?: OntologyRefInput[]
88
+ }
67
89
 
68
90
  export const CodeReferenceSchema = z.object({
69
91
  path: z
@@ -83,7 +105,11 @@ const ResourceEntryBaseSchema = z.object({
83
105
  /** Domain-map iteration order. Convention: multiples of 10 (10, 20, 30, ...) to allow easy insertion. */
84
106
  order: z.number().default(0),
85
107
  /** Required single System membership — value is a dot-separated system path (e.g. "sales.lead-gen"). */
86
- systemPath: SystemPathSchema.meta({ ref: 'system' }),
108
+ systemPath: SystemPathSchema.meta({ ref: 'system' }),
109
+ /** Executable display title owned by the OM Resource descriptor. */
110
+ title: LabelSchema.optional(),
111
+ /** Executable display description owned by the OM Resource descriptor. */
112
+ description: DescriptionSchema.optional(),
87
113
  /** Optional role responsible for maintaining this resource. */
88
114
  ownerRoleId: ModelIdSchema.meta({ ref: 'role' }).optional(),
89
115
  status: ResourceGovernanceStatusSchema,
@@ -97,12 +123,10 @@ const ResourceEntryBaseSchema = z.object({
97
123
  codeRefs: z.array(CodeReferenceSchema).default([])
98
124
  })
99
125
 
100
- export const WorkflowResourceEntrySchema = ResourceEntryBaseSchema.extend({
101
- kind: z.literal('workflow'),
102
- /** Mirrors WorkflowConfig.actionKey when the runtime workflow has one. */
103
- actionKey: z.string().trim().min(1).max(255).optional(),
104
- emits: z.array(EventEmissionDescriptorSchema).optional()
105
- })
126
+ export const WorkflowResourceEntrySchema = ResourceEntryBaseSchema.extend({
127
+ kind: z.literal('workflow'),
128
+ emits: z.array(EventEmissionDescriptorSchema).optional()
129
+ })
106
130
 
107
131
  export const AgentResourceEntrySchema = ResourceEntryBaseSchema.extend({
108
132
  kind: z.literal('agent'),
@@ -155,13 +179,32 @@ export function defineResource<const TResource extends ResourceEntry>(resource:
155
179
  return ResourceEntrySchema.parse(resource) as TResource
156
180
  }
157
181
 
158
- export function defineResources<const TResources extends Record<string, ResourceEntry>>(
159
- resources: TResources
160
- ): TResources {
182
+ export function defineResources<const TResources extends Record<string, ResourceEntry>>(
183
+ resources: TResources
184
+ ): TResources {
161
185
  return Object.fromEntries(
162
186
  Object.entries(resources).map(([key, resource]) => [key, ResourceEntrySchema.parse(resource)])
163
- ) as TResources
164
- }
187
+ ) as TResources
188
+ }
189
+
190
+ function ontologyIdFrom(input: OntologyRefInput): string {
191
+ return typeof input === 'string' ? input : input.id
192
+ }
193
+
194
+ function ontologyIdArrayFrom(input: OntologyRefInput[] | undefined): string[] | undefined {
195
+ return input?.map(ontologyIdFrom)
196
+ }
197
+
198
+ export function defineResourceOntology(input: ResourceOntologyBindingInput): ResourceOntologyBinding {
199
+ return ResourceOntologyBindingSchema.parse({
200
+ actions: ontologyIdArrayFrom(input.actions),
201
+ primaryAction: input.primaryAction === undefined ? undefined : ontologyIdFrom(input.primaryAction),
202
+ reads: ontologyIdArrayFrom(input.reads),
203
+ writes: ontologyIdArrayFrom(input.writes),
204
+ usesCatalogs: ontologyIdArrayFrom(input.usesCatalogs),
205
+ emits: ontologyIdArrayFrom(input.emits)
206
+ })
207
+ }
165
208
 
166
209
  export type ResourceId = z.infer<typeof ResourceIdSchema>
167
210
  export type ResourceKind = z.infer<typeof ResourceKindSchema>