@elevasis/core 0.27.0 → 0.28.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.
@@ -3,22 +3,23 @@ import { IconNameSchema, ModelIdSchema } from './shared'
3
3
  import { NodeIdStringSchema } from './systems'
4
4
  import { RoleIdSchema } from './roles'
5
5
  import { OntologyIdSchema } from '../ontology'
6
-
7
- // ---------------------------------------------------------------------------
8
- // KnowledgeLink — a typed graph link pointing to another OM node.
9
- // Uses NodeIdStringSchema (kind:dotted-path) to resolve to a graph node.
10
- // Phase 1 only emits 'governs' edges from knowledge nodes.
11
- // ---------------------------------------------------------------------------
12
-
13
- export const KnowledgeTargetKindSchema = z
14
- .enum([
15
- 'system',
16
- 'resource',
17
- 'knowledge',
18
- 'stage',
19
- 'action',
20
- 'role',
21
- 'goal',
6
+ import { defineDomainRecord } from '../helpers'
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // KnowledgeLink a typed graph link pointing to another OM node.
10
+ // Uses NodeIdStringSchema (kind:dotted-path) to resolve to a graph node.
11
+ // Phase 1 only emits 'governs' edges from knowledge nodes.
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export const KnowledgeTargetKindSchema = z
15
+ .enum([
16
+ 'system',
17
+ 'resource',
18
+ 'knowledge',
19
+ 'stage',
20
+ 'action',
21
+ 'role',
22
+ 'goal',
22
23
  'customer-segment',
23
24
  'offering',
24
25
  'ontology'
@@ -59,48 +60,48 @@ const LegacyKnowledgeLinkSchema = z
59
60
  })
60
61
  }
61
62
  })
62
-
63
- const CanonicalKnowledgeLinkSchema = z.object({
64
- target: KnowledgeTargetRefSchema
65
- })
66
-
67
- function nodeIdFromTarget(target: z.infer<typeof KnowledgeTargetRefSchema>): string {
68
- return `${target.kind}:${target.id}`
69
- }
70
-
71
- function targetFromNodeId(nodeId: string): z.infer<typeof KnowledgeTargetRefSchema> {
72
- const [kind, ...idParts] = nodeId.split(':')
73
- return {
74
- kind: KnowledgeTargetKindSchema.parse(kind),
75
- id: idParts.join(':')
76
- }
77
- }
78
-
79
- export const KnowledgeLinkSchema = z
80
- .union([CanonicalKnowledgeLinkSchema, LegacyKnowledgeLinkSchema])
81
- .transform((link) => {
82
- const target = 'target' in link ? link.target : targetFromNodeId(link.nodeId)
83
- return {
84
- target,
85
- nodeId: nodeIdFromTarget(target)
86
- }
87
- })
88
-
89
- // ---------------------------------------------------------------------------
90
- // OrgKnowledgeNode — single schema, `kind` discriminator drives presentation.
91
- // Phase 1: body is raw MDX string (lossless migration from operations/ docs).
92
- // Phase 2 (deferred): structured block format (Supabase-backed authoring).
93
- // ---------------------------------------------------------------------------
94
-
95
- export const OrgKnowledgeKindSchema = z
96
- .enum(['playbook', 'strategy', 'reference'])
97
- .meta({ label: 'Knowledge kind', color: 'grape' })
98
-
99
- export const OrgKnowledgeNodeSchema = z.object({
100
- id: ModelIdSchema,
101
- kind: OrgKnowledgeKindSchema,
102
- title: z.string().trim().min(1).max(200),
103
- summary: z.string().trim().min(1).max(1000),
63
+
64
+ const CanonicalKnowledgeLinkSchema = z.object({
65
+ target: KnowledgeTargetRefSchema
66
+ })
67
+
68
+ function nodeIdFromTarget(target: z.infer<typeof KnowledgeTargetRefSchema>): string {
69
+ return `${target.kind}:${target.id}`
70
+ }
71
+
72
+ function targetFromNodeId(nodeId: string): z.infer<typeof KnowledgeTargetRefSchema> {
73
+ const [kind, ...idParts] = nodeId.split(':')
74
+ return {
75
+ kind: KnowledgeTargetKindSchema.parse(kind),
76
+ id: idParts.join(':')
77
+ }
78
+ }
79
+
80
+ export const KnowledgeLinkSchema = z
81
+ .union([CanonicalKnowledgeLinkSchema, LegacyKnowledgeLinkSchema])
82
+ .transform((link) => {
83
+ const target = 'target' in link ? link.target : targetFromNodeId(link.nodeId)
84
+ return {
85
+ target,
86
+ nodeId: nodeIdFromTarget(target)
87
+ }
88
+ })
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // OrgKnowledgeNode — single schema, `kind` discriminator drives presentation.
92
+ // Phase 1: body is raw MDX string (lossless migration from operations/ docs).
93
+ // Phase 2 (deferred): structured block format (Supabase-backed authoring).
94
+ // ---------------------------------------------------------------------------
95
+
96
+ export const OrgKnowledgeKindSchema = z
97
+ .enum(['playbook', 'strategy', 'reference'])
98
+ .meta({ label: 'Knowledge kind', color: 'grape' })
99
+
100
+ export const OrgKnowledgeNodeSchema = z.object({
101
+ id: ModelIdSchema,
102
+ kind: OrgKnowledgeKindSchema,
103
+ title: z.string().trim().min(1).max(200),
104
+ summary: z.string().trim().min(1).max(1000),
104
105
  icon: IconNameSchema.optional(),
105
106
  /** Canonical documentation URL when body content is a local summary. */
106
107
  externalUrl: z.string().trim().url().max(500).optional(),
@@ -108,38 +109,50 @@ export const OrgKnowledgeNodeSchema = z.object({
108
109
  sourceFilePath: z.string().trim().min(1).max(500).optional(),
109
110
  /** Raw MDX string. Phase 2 will introduce a structured block format. */
110
111
  body: z.string().trim().min(1),
111
- /**
112
- * Graph links to other OM nodes this knowledge node governs.
113
- * Each link emits a `governs` edge: knowledge-node -> target node.
114
- */
115
- links: z.array(KnowledgeLinkSchema).default([]),
116
- /** Role identifiers that own this knowledge node. */
117
- ownerIds: z.array(RoleIdSchema.meta({ ref: 'role' })).default([]),
118
- /** ISO date string (YYYY-MM-DD or full ISO 8601) of last meaningful update. */
119
- updatedAt: z.string().trim().min(1).max(50)
120
- })
121
-
122
- // ---------------------------------------------------------------------------
123
- // Domain schema — flat Record<id, KnowledgeNode> (D3: flatten per Phase 4 cut)
124
- //
125
- // Wave 1 shape change: the old wrapper object { version, lastModified, nodes[] }
126
- // is replaced by a plain id-keyed map. version/lastModified move to
127
- // domainMetadata.knowledge (D7). The default is an empty map {}.
128
- //
129
- // Wave 2 (elevasis-core canonicalOrganizationModel) must produce a value of this
130
- // shape: Record<KnowledgeId, OrgKnowledgeNode> — i.e., an object keyed by
131
- // each node's id string, with OrgKnowledgeNode as the value. Example:
132
- // knowledge: {
133
- // 'playbook.crm.discovery': { id: 'playbook.crm.discovery', kind: 'playbook', ... },
134
- // }
135
- // ---------------------------------------------------------------------------
136
-
137
- export const KnowledgeDomainSchema = z.record(ModelIdSchema, OrgKnowledgeNodeSchema).default({})
138
-
139
- export type OrgKnowledgeNode = z.infer<typeof OrgKnowledgeNodeSchema>
140
- export type OrgKnowledgeNodeInput = z.input<typeof OrgKnowledgeNodeSchema>
141
- export type OrgKnowledgeKind = z.infer<typeof OrgKnowledgeKindSchema>
142
- export type KnowledgeTargetKind = z.infer<typeof KnowledgeTargetKindSchema>
143
- export type KnowledgeTargetRef = z.infer<typeof KnowledgeTargetRefSchema>
144
- export type KnowledgeLink = z.infer<typeof KnowledgeLinkSchema>
145
- export type KnowledgeDomain = z.infer<typeof KnowledgeDomainSchema>
112
+ /**
113
+ * Graph links to other OM nodes this knowledge node governs.
114
+ * Each link emits a `governs` edge: knowledge-node -> target node.
115
+ */
116
+ links: z.array(KnowledgeLinkSchema).default([]),
117
+ /** Role identifiers that own this knowledge node. */
118
+ ownerIds: z.array(RoleIdSchema.meta({ ref: 'role' })).default([]),
119
+ /** ISO date string (YYYY-MM-DD or full ISO 8601) of last meaningful update. */
120
+ updatedAt: z.string().trim().min(1).max(50)
121
+ })
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Domain schema — flat Record<id, KnowledgeNode> (D3: flatten per Phase 4 cut)
125
+ //
126
+ // Wave 1 shape change: the old wrapper object { version, lastModified, nodes[] }
127
+ // is replaced by a plain id-keyed map. version/lastModified move to
128
+ // domainMetadata.knowledge (D7). The default is an empty map {}.
129
+ //
130
+ // Wave 2 (elevasis-core canonicalOrganizationModel) must produce a value of this
131
+ // shape: Record<KnowledgeId, OrgKnowledgeNode> — i.e., an object keyed by
132
+ // each node's id string, with OrgKnowledgeNode as the value. Example:
133
+ // knowledge: {
134
+ // 'playbook.crm.discovery': { id: 'playbook.crm.discovery', kind: 'playbook', ... },
135
+ // }
136
+ // ---------------------------------------------------------------------------
137
+
138
+ export const KnowledgeDomainSchema = z.record(ModelIdSchema, OrgKnowledgeNodeSchema).default({})
139
+
140
+ /** Validate and return a single knowledge node entry. */
141
+ export function defineKnowledgeNode(entry: z.input<typeof OrgKnowledgeNodeSchema>): OrgKnowledgeNode {
142
+ return OrgKnowledgeNodeSchema.parse(entry)
143
+ }
144
+
145
+ /** Validate and return an id-keyed map of knowledge node entries. */
146
+ export function defineKnowledgeNodes(
147
+ entries: readonly z.input<typeof OrgKnowledgeNodeSchema>[]
148
+ ): Record<string, OrgKnowledgeNode> {
149
+ return defineDomainRecord(OrgKnowledgeNodeSchema, entries)
150
+ }
151
+
152
+ export type OrgKnowledgeNode = z.infer<typeof OrgKnowledgeNodeSchema>
153
+ export type OrgKnowledgeNodeInput = z.input<typeof OrgKnowledgeNodeSchema>
154
+ export type OrgKnowledgeKind = z.infer<typeof OrgKnowledgeKindSchema>
155
+ export type KnowledgeTargetKind = z.infer<typeof KnowledgeTargetKindSchema>
156
+ export type KnowledgeTargetRef = z.infer<typeof KnowledgeTargetRefSchema>
157
+ export type KnowledgeLink = z.infer<typeof KnowledgeLinkSchema>
158
+ export type KnowledgeDomain = z.infer<typeof KnowledgeDomainSchema>
@@ -1,71 +1,88 @@
1
- import { z } from 'zod'
2
-
3
- // ---------------------------------------------------------------------------
4
- // Pricing model — the four canonical pricing structures used in B2B SaaS and
5
- // professional services. "custom" covers bespoke / negotiated pricing.
6
- // ---------------------------------------------------------------------------
7
-
8
- export const PricingModelSchema = z
9
- .enum(['one-time', 'subscription', 'usage-based', 'custom'])
10
- .meta({ label: 'Pricing model', color: 'green' })
11
-
12
- // ---------------------------------------------------------------------------
13
- // Product schema — one entry per distinct offering (product or service).
14
- // Modeled after Business Model Canvas "Value Propositions" and company profile
15
- // product/service catalog language. Fields use plain English throughout.
16
- // ---------------------------------------------------------------------------
17
-
18
- export const ProductSchema = z.object({
19
- /** Stable unique identifier for the product (e.g. "product-starter-plan"). */
20
- id: z.string().trim().min(1).max(100),
21
- /** Domain-map iteration order. Convention: multiples of 10 (10, 20, 30, ...) to allow easy insertion. */
22
- order: z.number(),
23
- /** Human-readable name shown to agents and in UI (e.g. "Starter Plan"). */
24
- name: z.string().trim().max(200).default(''),
25
- /** One or two sentences describing what this product/service delivers. */
26
- description: z.string().trim().max(2000).default(''),
27
- /**
28
- * How this product is priced:
29
- * - "one-time" single purchase (setup fee, project fee)
30
- * - "subscription" — recurring (monthly/annual SaaS, retainer)
31
- * - "usage-based" — metered by consumption (API calls, seats)
32
- * - "custom" — negotiated or bespoke pricing
33
- */
34
- pricingModel: PricingModelSchema.default('custom'),
35
- /** Base price amount (≥ 0). Currency unit defined by `currency`. */
36
- price: z.number().min(0).default(0),
37
- /**
38
- * ISO 4217 currency code (e.g. "USD", "EUR", "GBP").
39
- * Free-form string to accommodate any currency; defaults to "USD".
40
- */
41
- currency: z.string().trim().max(10).default('USD'),
42
- /**
43
- * IDs of customer segments this product targets.
44
- * Each id must reference a declared `customers.segments[].id`.
45
- * Cross-reference enforced in `OrganizationModelSchema.superRefine()`.
46
- */
47
- targetSegmentIds: z.array(z.string().trim().min(1)).default([]),
48
- /**
49
- * Optional: ID of the platform system responsible for delivering this product.
50
- * When present, must reference a declared `systems.systems[].id`.
51
- * Cross-reference enforced in `OrganizationModelSchema.superRefine()`.
52
- */
53
- deliveryFeatureId: z.string().trim().min(1).optional()
54
- })
55
-
56
- // ---------------------------------------------------------------------------
57
- // Offerings domain schema — id-keyed map of products and services.
58
- // ---------------------------------------------------------------------------
59
-
60
- export const OfferingsDomainSchema = z
61
- .record(z.string(), ProductSchema)
62
- .refine((record) => Object.entries(record).every(([key, entry]) => entry.id === key), {
63
- message: 'Each product entry id must match its map key'
64
- })
65
- .default({})
66
-
67
- // ---------------------------------------------------------------------------
68
- // Seed — empty by default; adapters populate with real product definitions.
69
- // ---------------------------------------------------------------------------
70
-
71
- export const DEFAULT_ORGANIZATION_MODEL_OFFERINGS: z.infer<typeof OfferingsDomainSchema> = {}
1
+ import { z } from 'zod'
2
+ import { defineDomainRecord } from '../helpers'
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Pricing model the four canonical pricing structures used in B2B SaaS and
6
+ // professional services. "custom" covers bespoke / negotiated pricing.
7
+ // ---------------------------------------------------------------------------
8
+
9
+ export const PricingModelSchema = z
10
+ .enum(['one-time', 'subscription', 'usage-based', 'custom'])
11
+ .meta({ label: 'Pricing model', color: 'green' })
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Product schema one entry per distinct offering (product or service).
15
+ // Modeled after Business Model Canvas "Value Propositions" and company profile
16
+ // product/service catalog language. Fields use plain English throughout.
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export const ProductSchema = z.object({
20
+ /** Stable unique identifier for the product (e.g. "product-starter-plan"). */
21
+ id: z.string().trim().min(1).max(100),
22
+ /** Domain-map iteration order. Convention: multiples of 10 (10, 20, 30, ...) to allow easy insertion. */
23
+ order: z.number(),
24
+ /** Human-readable name shown to agents and in UI (e.g. "Starter Plan"). */
25
+ name: z.string().trim().max(200).default(''),
26
+ /** One or two sentences describing what this product/service delivers. */
27
+ description: z.string().trim().max(2000).default(''),
28
+ /**
29
+ * How this product is priced:
30
+ * - "one-time" — single purchase (setup fee, project fee)
31
+ * - "subscription" — recurring (monthly/annual SaaS, retainer)
32
+ * - "usage-based" — metered by consumption (API calls, seats)
33
+ * - "custom" — negotiated or bespoke pricing
34
+ */
35
+ pricingModel: PricingModelSchema.default('custom'),
36
+ /** Base price amount (0). Currency unit defined by `currency`. */
37
+ price: z.number().min(0).default(0),
38
+ /**
39
+ * ISO 4217 currency code (e.g. "USD", "EUR", "GBP").
40
+ * Free-form string to accommodate any currency; defaults to "USD".
41
+ */
42
+ currency: z.string().trim().max(10).default('USD'),
43
+ /**
44
+ * IDs of customer segments this product targets.
45
+ * Each id must reference a declared `customers.segments[].id`.
46
+ * Cross-reference enforced in `OrganizationModelSchema.superRefine()`.
47
+ */
48
+ targetSegmentIds: z.array(z.string().trim().min(1)).default([]),
49
+ /**
50
+ * Optional: ID of the platform system responsible for delivering this product.
51
+ * When present, must reference a declared `systems.systems[].id`.
52
+ * Cross-reference enforced in `OrganizationModelSchema.superRefine()`.
53
+ */
54
+ deliveryFeatureId: z.string().trim().min(1).optional()
55
+ })
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Offerings domain schema — id-keyed map of products and services.
59
+ // ---------------------------------------------------------------------------
60
+
61
+ export const OfferingsDomainSchema = z
62
+ .record(z.string(), ProductSchema)
63
+ .refine((record) => Object.entries(record).every(([key, entry]) => entry.id === key), {
64
+ message: 'Each product entry id must match its map key'
65
+ })
66
+ .default({})
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Seed — empty by default; adapters populate with real product definitions.
70
+ // ---------------------------------------------------------------------------
71
+
72
+ export const DEFAULT_ORGANIZATION_MODEL_OFFERINGS: z.infer<typeof OfferingsDomainSchema> = {}
73
+
74
+ /** Validate and return a single offering (product) entry. */
75
+ export function defineOffering(entry: z.input<typeof ProductSchema>): z.infer<typeof ProductSchema> {
76
+ return ProductSchema.parse(entry)
77
+ }
78
+
79
+ /** Validate and return an id-keyed map of offering entries. */
80
+ export function defineOfferings(
81
+ entries: readonly z.input<typeof ProductSchema>[]
82
+ ): Record<string, z.infer<typeof ProductSchema>> {
83
+ return defineDomainRecord(ProductSchema, entries)
84
+ }
85
+
86
+ export type Product = z.infer<typeof ProductSchema>
87
+ export type OfferingsDomain = z.infer<typeof OfferingsDomainSchema>
88
+ export type PricingModel = z.infer<typeof PricingModelSchema>
@@ -1,102 +1,115 @@
1
- import { z } from 'zod'
2
- import { DescriptionSchema, LabelSchema, ModelIdSchema } from './shared'
3
- import { EventIdSchema } from './resources'
4
-
5
- export const PolicyIdSchema = ModelIdSchema
6
-
7
- export const PolicyApplicabilitySchema = z.object({
8
- systemIds: z.array(ModelIdSchema.meta({ ref: 'system' })).default([]),
9
- actionIds: z.array(ModelIdSchema.meta({ ref: 'action' })).default([]),
10
- resourceIds: z.array(ModelIdSchema.meta({ ref: 'resource' })).default([]),
11
- roleIds: z.array(ModelIdSchema.meta({ ref: 'role' })).default([])
12
- })
13
-
14
- export const PolicyTriggerSchema = z.discriminatedUnion('kind', [
15
- z.object({
16
- kind: z.literal('event'),
17
- eventId: EventIdSchema.meta({ ref: 'event' })
18
- }),
19
- z.object({
20
- kind: z.literal('action-invocation'),
21
- actionId: ModelIdSchema.meta({ ref: 'action' })
22
- }),
23
- z.object({
24
- kind: z.literal('schedule'),
25
- cron: z.string().trim().min(1).max(120)
26
- }),
27
- z.object({
28
- kind: z.literal('manual')
29
- })
30
- ])
31
-
32
- export const PolicyPredicateSchema = z.discriminatedUnion('kind', [
33
- z.object({
34
- kind: z.literal('always')
35
- }),
36
- z.object({
37
- kind: z.literal('expression'),
38
- expression: z.string().trim().min(1).max(2000)
39
- }),
40
- z.object({
41
- kind: z.literal('threshold'),
42
- metric: ModelIdSchema,
43
- operator: z.enum(['lt', 'lte', 'eq', 'gte', 'gt']).meta({ label: 'Operator' }),
44
- value: z.number()
45
- })
46
- ])
47
-
48
- export const PolicyEffectSchema = z.discriminatedUnion('kind', [
49
- z.object({
50
- kind: z.literal('require-approval'),
51
- roleId: ModelIdSchema.meta({ ref: 'role' }).optional()
52
- }),
53
- z.object({
54
- kind: z.literal('invoke-action'),
55
- actionId: ModelIdSchema.meta({ ref: 'action' })
56
- }),
57
- z.object({
58
- kind: z.literal('notify-role'),
59
- roleId: ModelIdSchema.meta({ ref: 'role' })
60
- }),
61
- z.object({
62
- kind: z.literal('block')
63
- })
64
- ])
65
-
66
- export const PolicySchema = z.object({
67
- id: PolicyIdSchema,
68
- /** Domain-map iteration order. Convention: multiples of 10 (10, 20, 30, ...) to allow easy insertion. */
69
- order: z.number(),
70
- label: LabelSchema,
71
- description: DescriptionSchema.optional(),
72
- trigger: PolicyTriggerSchema,
73
- predicate: PolicyPredicateSchema.default({ kind: 'always' }),
74
- actions: z.array(PolicyEffectSchema).min(1),
75
- appliesTo: PolicyApplicabilitySchema.default({
76
- systemIds: [],
77
- actionIds: [],
78
- resourceIds: [],
79
- roleIds: []
80
- }),
81
- lifecycle: z
82
- .enum(['draft', 'beta', 'active', 'deprecated', 'archived'])
83
- .meta({ label: 'Lifecycle', color: 'teal' })
84
- .default('active')
85
- })
86
-
87
- export const PoliciesDomainSchema = z
88
- .record(z.string(), PolicySchema)
89
- .refine((record) => Object.entries(record).every(([key, entry]) => entry.id === key), {
90
- message: 'Each policy entry id must match its map key'
91
- })
92
- .default({})
93
-
94
- export const DEFAULT_ORGANIZATION_MODEL_POLICIES: z.infer<typeof PoliciesDomainSchema> = {}
95
-
96
- export type PolicyId = z.infer<typeof PolicyIdSchema>
97
- export type PolicyApplicability = z.infer<typeof PolicyApplicabilitySchema>
98
- export type PolicyTrigger = z.infer<typeof PolicyTriggerSchema>
99
- export type PolicyPredicate = z.infer<typeof PolicyPredicateSchema>
100
- export type PolicyEffect = z.infer<typeof PolicyEffectSchema>
101
- export type Policy = z.infer<typeof PolicySchema>
102
- export type PoliciesDomain = z.infer<typeof PoliciesDomainSchema>
1
+ import { z } from 'zod'
2
+ import { DescriptionSchema, LabelSchema, ModelIdSchema } from './shared'
3
+ import { EventIdSchema } from './resources'
4
+ import { defineDomainRecord } from '../helpers'
5
+
6
+ export const PolicyIdSchema = ModelIdSchema
7
+
8
+ export const PolicyApplicabilitySchema = z.object({
9
+ systemIds: z.array(ModelIdSchema.meta({ ref: 'system' })).default([]),
10
+ actionIds: z.array(ModelIdSchema.meta({ ref: 'action' })).default([]),
11
+ resourceIds: z.array(ModelIdSchema.meta({ ref: 'resource' })).default([]),
12
+ roleIds: z.array(ModelIdSchema.meta({ ref: 'role' })).default([])
13
+ })
14
+
15
+ export const PolicyTriggerSchema = z.discriminatedUnion('kind', [
16
+ z.object({
17
+ kind: z.literal('event'),
18
+ eventId: EventIdSchema.meta({ ref: 'event' })
19
+ }),
20
+ z.object({
21
+ kind: z.literal('action-invocation'),
22
+ actionId: ModelIdSchema.meta({ ref: 'action' })
23
+ }),
24
+ z.object({
25
+ kind: z.literal('schedule'),
26
+ cron: z.string().trim().min(1).max(120)
27
+ }),
28
+ z.object({
29
+ kind: z.literal('manual')
30
+ })
31
+ ])
32
+
33
+ export const PolicyPredicateSchema = z.discriminatedUnion('kind', [
34
+ z.object({
35
+ kind: z.literal('always')
36
+ }),
37
+ z.object({
38
+ kind: z.literal('expression'),
39
+ expression: z.string().trim().min(1).max(2000)
40
+ }),
41
+ z.object({
42
+ kind: z.literal('threshold'),
43
+ metric: ModelIdSchema,
44
+ operator: z.enum(['lt', 'lte', 'eq', 'gte', 'gt']).meta({ label: 'Operator' }),
45
+ value: z.number()
46
+ })
47
+ ])
48
+
49
+ export const PolicyEffectSchema = z.discriminatedUnion('kind', [
50
+ z.object({
51
+ kind: z.literal('require-approval'),
52
+ roleId: ModelIdSchema.meta({ ref: 'role' }).optional()
53
+ }),
54
+ z.object({
55
+ kind: z.literal('invoke-action'),
56
+ actionId: ModelIdSchema.meta({ ref: 'action' })
57
+ }),
58
+ z.object({
59
+ kind: z.literal('notify-role'),
60
+ roleId: ModelIdSchema.meta({ ref: 'role' })
61
+ }),
62
+ z.object({
63
+ kind: z.literal('block')
64
+ })
65
+ ])
66
+
67
+ export const PolicySchema = z.object({
68
+ id: PolicyIdSchema,
69
+ /** Domain-map iteration order. Convention: multiples of 10 (10, 20, 30, ...) to allow easy insertion. */
70
+ order: z.number(),
71
+ label: LabelSchema,
72
+ description: DescriptionSchema.optional(),
73
+ trigger: PolicyTriggerSchema,
74
+ predicate: PolicyPredicateSchema.default({ kind: 'always' }),
75
+ actions: z.array(PolicyEffectSchema).min(1),
76
+ appliesTo: PolicyApplicabilitySchema.default({
77
+ systemIds: [],
78
+ actionIds: [],
79
+ resourceIds: [],
80
+ roleIds: []
81
+ }),
82
+ lifecycle: z
83
+ .enum(['draft', 'beta', 'active', 'deprecated', 'archived'])
84
+ .meta({ label: 'Lifecycle', color: 'teal' })
85
+ .default('active')
86
+ })
87
+
88
+ export const PoliciesDomainSchema = z
89
+ .record(z.string(), PolicySchema)
90
+ .refine((record) => Object.entries(record).every(([key, entry]) => entry.id === key), {
91
+ message: 'Each policy entry id must match its map key'
92
+ })
93
+ .default({})
94
+
95
+ export const DEFAULT_ORGANIZATION_MODEL_POLICIES: z.infer<typeof PoliciesDomainSchema> = {}
96
+
97
+ /** Validate and return a single policy entry. */
98
+ export function definePolicy(entry: z.input<typeof PolicySchema>): z.infer<typeof PolicySchema> {
99
+ return PolicySchema.parse(entry)
100
+ }
101
+
102
+ /** Validate and return an id-keyed map of policy entries. */
103
+ export function definePolicies(
104
+ entries: readonly z.input<typeof PolicySchema>[]
105
+ ): Record<string, z.infer<typeof PolicySchema>> {
106
+ return defineDomainRecord(PolicySchema, entries)
107
+ }
108
+
109
+ export type PolicyId = z.infer<typeof PolicyIdSchema>
110
+ export type PolicyApplicability = z.infer<typeof PolicyApplicabilitySchema>
111
+ export type PolicyTrigger = z.infer<typeof PolicyTriggerSchema>
112
+ export type PolicyPredicate = z.infer<typeof PolicyPredicateSchema>
113
+ export type PolicyEffect = z.infer<typeof PolicyEffectSchema>
114
+ export type Policy = z.infer<typeof PolicySchema>
115
+ export type PoliciesDomain = z.infer<typeof PoliciesDomainSchema>