@emkodev/emkore 1.0.3

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 (48) hide show
  1. package/CHANGELOG.md +269 -0
  2. package/DEVELOPER_GUIDE.md +227 -0
  3. package/LICENSE +21 -0
  4. package/README.md +126 -0
  5. package/bun.lock +22 -0
  6. package/example/README.md +200 -0
  7. package/example/create-user.interactor.ts +88 -0
  8. package/example/dto/user.dto.ts +34 -0
  9. package/example/entity/user.entity.ts +54 -0
  10. package/example/index.ts +18 -0
  11. package/example/interface/create-user.usecase.ts +93 -0
  12. package/example/interface/user.repository.ts +23 -0
  13. package/mod.ts +1 -0
  14. package/package.json +32 -0
  15. package/src/common/abstract.actor.ts +59 -0
  16. package/src/common/abstract.entity.ts +59 -0
  17. package/src/common/abstract.interceptor.ts +17 -0
  18. package/src/common/abstract.repository.ts +162 -0
  19. package/src/common/abstract.usecase.ts +113 -0
  20. package/src/common/config/config-registry.ts +190 -0
  21. package/src/common/config/config-section.ts +106 -0
  22. package/src/common/exception/authorization-exception.ts +28 -0
  23. package/src/common/exception/repository-exception.ts +46 -0
  24. package/src/common/interceptor/audit-log.interceptor.ts +181 -0
  25. package/src/common/interceptor/authorization.interceptor.ts +252 -0
  26. package/src/common/interceptor/performance.interceptor.ts +101 -0
  27. package/src/common/llm/api-definition.type.ts +185 -0
  28. package/src/common/pattern/unit-of-work.ts +78 -0
  29. package/src/common/platform/env.ts +38 -0
  30. package/src/common/registry/usecase-registry.ts +80 -0
  31. package/src/common/type/interceptor-context.type.ts +25 -0
  32. package/src/common/type/json-schema.type.ts +80 -0
  33. package/src/common/type/json.type.ts +5 -0
  34. package/src/common/type/lowercase.type.ts +48 -0
  35. package/src/common/type/metadata.type.ts +5 -0
  36. package/src/common/type/money.class.ts +384 -0
  37. package/src/common/type/permission.type.ts +43 -0
  38. package/src/common/validation/validation-result.ts +52 -0
  39. package/src/common/validation/validators.ts +441 -0
  40. package/src/index.ts +95 -0
  41. package/test/unit/abstract-actor.test.ts +608 -0
  42. package/test/unit/actor.test.ts +89 -0
  43. package/test/unit/api-definition.test.ts +628 -0
  44. package/test/unit/authorization.test.ts +101 -0
  45. package/test/unit/entity.test.ts +95 -0
  46. package/test/unit/money.test.ts +480 -0
  47. package/test/unit/validation.test.ts +138 -0
  48. package/tsconfig.json +18 -0
@@ -0,0 +1,181 @@
1
+ import type { Interceptor } from "../abstract.interceptor.ts";
2
+ import type { InterceptorContext } from "../type/interceptor-context.type.ts";
3
+
4
+ /**
5
+ * Audit log entry structure for compliance and tracking
6
+ */
7
+ export interface AuditLogEntry {
8
+ timestamp: string;
9
+ actorId: string;
10
+ businessId: string;
11
+ action: string;
12
+ resource: string;
13
+ status: "started" | "completed" | "failed";
14
+ duration?: number;
15
+ error?: string;
16
+ metadata?: Record<string, unknown>;
17
+ requiredPermissions?: string[];
18
+ }
19
+
20
+ /**
21
+ * Audit log interceptor for compliance and security tracking.
22
+ * Creates detailed audit trails of all usecase executions.
23
+ */
24
+ export class AuditLogInterceptor<Input, Output>
25
+ implements Interceptor<Input, Output> {
26
+ readonly name = "AuditLogInterceptor";
27
+
28
+ private readonly startTimes = new Map<string, number>();
29
+
30
+ constructor(
31
+ private readonly logHandler: (
32
+ entry: AuditLogEntry,
33
+ ) => void | Promise<void> = (entry) =>
34
+ console.log("[Audit]", JSON.stringify(entry)),
35
+ ) {}
36
+
37
+ private getExecutionKey(context: InterceptorContext): string {
38
+ return `${context.actor.id}_${context.usecaseName.join("/")}`;
39
+ }
40
+
41
+ async beforeExecute(
42
+ input: Input,
43
+ context: InterceptorContext,
44
+ ): Promise<Input> {
45
+ const key = this.getExecutionKey(context);
46
+ this.startTimes.set(key, Date.now());
47
+
48
+ const entry: AuditLogEntry = {
49
+ timestamp: new Date().toISOString(),
50
+ actorId: context.actor.id,
51
+ businessId: context.actor.businessId,
52
+ action: context.usecaseName[0],
53
+ resource: context.usecaseName[1],
54
+ status: "started",
55
+ ...(context.requiredPermissions.length > 0 && {
56
+ requiredPermissions: context.requiredPermissions.map((p) =>
57
+ `${p.action}:${p.resource}`
58
+ ),
59
+ }),
60
+ ...(Object.keys(context.metadata).length > 0 && {
61
+ metadata: context.metadata,
62
+ }),
63
+ };
64
+
65
+ await this.logHandler(entry);
66
+
67
+ return input;
68
+ }
69
+
70
+ async afterExecute(
71
+ output: Output,
72
+ context: InterceptorContext,
73
+ ): Promise<Output> {
74
+ const key = this.getExecutionKey(context);
75
+ const startTime = this.startTimes.get(key);
76
+ this.startTimes.delete(key);
77
+
78
+ const entry: AuditLogEntry = {
79
+ timestamp: new Date().toISOString(),
80
+ actorId: context.actor.id,
81
+ businessId: context.actor.businessId,
82
+ action: context.usecaseName[0],
83
+ resource: context.usecaseName[1],
84
+ status: "completed",
85
+ ...(startTime !== undefined && {
86
+ duration: Date.now() - startTime,
87
+ }),
88
+ };
89
+
90
+ await this.logHandler(entry);
91
+
92
+ return output;
93
+ }
94
+
95
+ async onError(
96
+ error: Error,
97
+ context: InterceptorContext,
98
+ ): Promise<void> {
99
+ const key = this.getExecutionKey(context);
100
+ const startTime = this.startTimes.get(key);
101
+ this.startTimes.delete(key);
102
+
103
+ const entry: AuditLogEntry = {
104
+ timestamp: new Date().toISOString(),
105
+ actorId: context.actor.id,
106
+ businessId: context.actor.businessId,
107
+ action: context.usecaseName[0],
108
+ resource: context.usecaseName[1],
109
+ status: "failed",
110
+ error: `${error.name}: ${error.message}`,
111
+ ...(startTime !== undefined && {
112
+ duration: Date.now() - startTime,
113
+ }),
114
+ };
115
+
116
+ await this.logHandler(entry);
117
+ }
118
+ }
119
+
120
+ /**
121
+ * File-based audit logger that writes to a file
122
+ * This is an example implementation - in production you'd likely use a database or log aggregation service
123
+ */
124
+ export class FileAuditLogger {
125
+ private entries: AuditLogEntry[] = [];
126
+
127
+ constructor(
128
+ private readonly maxEntries: number = 10000,
129
+ private readonly rotateCallback?: (
130
+ entries: AuditLogEntry[],
131
+ ) => void | Promise<void>,
132
+ ) {}
133
+
134
+ async log(entry: AuditLogEntry): Promise<void> {
135
+ this.entries.push(entry);
136
+
137
+ // Rotate log when it gets too large
138
+ if (this.entries.length >= this.maxEntries) {
139
+ const entriesToRotate = this.entries.splice(0, this.maxEntries / 2);
140
+ if (this.rotateCallback) {
141
+ await this.rotateCallback(entriesToRotate);
142
+ }
143
+ }
144
+ }
145
+
146
+ getEntries(
147
+ filter?: {
148
+ actorId?: string;
149
+ businessId?: string;
150
+ action?: string;
151
+ resource?: string;
152
+ status?: AuditLogEntry["status"];
153
+ from?: Date;
154
+ to?: Date;
155
+ },
156
+ ): AuditLogEntry[] {
157
+ if (!filter) {
158
+ return [...this.entries];
159
+ }
160
+
161
+ return this.entries.filter((entry) => {
162
+ if (filter.actorId && entry.actorId !== filter.actorId) return false;
163
+ if (filter.businessId && entry.businessId !== filter.businessId) {
164
+ return false;
165
+ }
166
+ if (filter.action && entry.action !== filter.action) return false;
167
+ if (filter.resource && entry.resource !== filter.resource) return false;
168
+ if (filter.status && entry.status !== filter.status) return false;
169
+
170
+ const entryTime = new Date(entry.timestamp);
171
+ if (filter.from && entryTime < filter.from) return false;
172
+ if (filter.to && entryTime > filter.to) return false;
173
+
174
+ return true;
175
+ });
176
+ }
177
+
178
+ clear(): void {
179
+ this.entries = [];
180
+ }
181
+ }
@@ -0,0 +1,252 @@
1
+ import type { Interceptor } from "../abstract.interceptor.ts";
2
+ import type { InterceptorContext } from "../type/interceptor-context.type.ts";
3
+ import {
4
+ type Permission,
5
+ type ResourceConstraints,
6
+ ResourceScope,
7
+ } from "../type/permission.type.ts";
8
+ import { AuthorizationException } from "../exception/authorization-exception.ts";
9
+
10
+ export class AuthorizationInterceptor<Input, Output>
11
+ implements Interceptor<Input, Output> {
12
+ readonly name = "AuthorizationInterceptor";
13
+
14
+ // Pre-computed scope satisfaction matrix for O(1) lookups
15
+ // PROJECT and TEAM are parallel scopes under BUSINESS
16
+ private static readonly SCOPE_SATISFIES = new Map<
17
+ ResourceScope,
18
+ Set<ResourceScope>
19
+ >([
20
+ [
21
+ ResourceScope.ALL,
22
+ new Set([
23
+ ResourceScope.ALL,
24
+ ResourceScope.BUSINESS,
25
+ ResourceScope.PROJECT,
26
+ ResourceScope.TEAM,
27
+ ResourceScope.OWNED,
28
+ ]),
29
+ ],
30
+ [
31
+ ResourceScope.BUSINESS,
32
+ new Set([
33
+ ResourceScope.BUSINESS,
34
+ ResourceScope.PROJECT,
35
+ ResourceScope.TEAM,
36
+ ResourceScope.OWNED,
37
+ ]),
38
+ ],
39
+ [
40
+ ResourceScope.PROJECT,
41
+ new Set([ResourceScope.PROJECT, ResourceScope.OWNED]),
42
+ ],
43
+ [ResourceScope.TEAM, new Set([ResourceScope.TEAM, ResourceScope.OWNED])],
44
+ [ResourceScope.OWNED, new Set([ResourceScope.OWNED])],
45
+ ]);
46
+
47
+ beforeExecute(
48
+ input: Input,
49
+ context: InterceptorContext,
50
+ ): Input {
51
+ this._checkAuthorization(context);
52
+ return input;
53
+ }
54
+
55
+ private _checkAuthorization(context: InterceptorContext): void {
56
+ const requiredPerms = context.requiredPermissions;
57
+
58
+ if (requiredPerms.length === 0) {
59
+ return;
60
+ }
61
+
62
+ // Index actor permissions for O(1) lookups
63
+ const actorPermIndex = this._indexPermissions(context.actor.permissions);
64
+
65
+ if (!this._hasPermissionsIndexed(actorPermIndex, requiredPerms)) {
66
+ const missingPermissions = this._findMissingPermissions(
67
+ context.actor.permissions,
68
+ requiredPerms,
69
+ );
70
+
71
+ throw new AuthorizationException(
72
+ `Insufficient permissions for actor ${context.actor.id}`,
73
+ {
74
+ actorId: context.actor.id,
75
+ resource: context.usecaseName[1],
76
+ action: context.usecaseName[0],
77
+ missingPermissions,
78
+ },
79
+ );
80
+ }
81
+ }
82
+
83
+ private _indexPermissions(
84
+ permissions: Permission[],
85
+ ): Map<string, Permission[]> {
86
+ const index = new Map<string, Permission[]>();
87
+
88
+ for (const perm of permissions) {
89
+ const key = `${perm.resource}:${perm.action}`;
90
+ if (!index.has(key)) {
91
+ index.set(key, []);
92
+ }
93
+ index.get(key)!.push(perm);
94
+ }
95
+
96
+ return index;
97
+ }
98
+
99
+ private _hasPermissionsIndexed(
100
+ actorPermIndex: Map<string, Permission[]>,
101
+ requiredPermissions: Permission[],
102
+ ): boolean {
103
+ if (requiredPermissions.length === 0) {
104
+ return true;
105
+ }
106
+
107
+ for (const required of requiredPermissions) {
108
+ if (!this._hasPermissionIndexed(actorPermIndex, required)) {
109
+ return false;
110
+ }
111
+ }
112
+
113
+ return true;
114
+ }
115
+
116
+ private _hasPermissionIndexed(
117
+ actorPermIndex: Map<string, Permission[]>,
118
+ required: Permission,
119
+ ): boolean {
120
+ const key = `${required.resource}:${required.action}`;
121
+ const candidates = actorPermIndex.get(key) || [];
122
+
123
+ // Only check constraints for exact resource:action matches (O(1) lookup)
124
+ for (const permission of candidates) {
125
+ if (
126
+ this._constraintsSatisfied(permission.constraints, required.constraints)
127
+ ) {
128
+ return true;
129
+ }
130
+ }
131
+ return false;
132
+ }
133
+
134
+ private _hasPermission(
135
+ actorPermissions: Permission[],
136
+ required: Permission,
137
+ ): boolean {
138
+ for (const permission of actorPermissions) {
139
+ if (this._permissionMatches(permission, required)) {
140
+ return true;
141
+ }
142
+ }
143
+ return false;
144
+ }
145
+
146
+ private _permissionMatches(
147
+ granted: Permission,
148
+ required: Permission,
149
+ ): boolean {
150
+ if (
151
+ granted.resource === required.resource &&
152
+ granted.action === required.action
153
+ ) {
154
+ return this._constraintsSatisfied(
155
+ granted.constraints,
156
+ required.constraints,
157
+ );
158
+ }
159
+ return false;
160
+ }
161
+
162
+ private _constraintsSatisfied(
163
+ granted?: ResourceConstraints,
164
+ required?: ResourceConstraints,
165
+ ): boolean {
166
+ if (!required) {
167
+ return true;
168
+ }
169
+
170
+ if (!granted) {
171
+ return false;
172
+ }
173
+
174
+ // Check scope constraints
175
+ if (required.scope) {
176
+ if (!granted.scope) return false;
177
+ if (!this._scopeSatisfied(granted.scope, required.scope)) return false;
178
+ }
179
+
180
+ // Check status constraints
181
+ if (required.statuses) {
182
+ if (!granted.statuses) return false;
183
+ const requiredSet = new Set(required.statuses);
184
+ const grantedSet = new Set(granted.statuses);
185
+ for (const status of requiredSet) {
186
+ if (!grantedSet.has(status)) return false;
187
+ }
188
+ }
189
+
190
+ // Check time constraints
191
+ if (required.timeRestriction) {
192
+ if (!granted.timeRestriction) return false;
193
+ const now = new Date();
194
+ if (
195
+ now < granted.timeRestriction.from ||
196
+ now > granted.timeRestriction.to
197
+ ) {
198
+ return false;
199
+ }
200
+ }
201
+
202
+ // Check businessId constraints
203
+ if (required.businessId) {
204
+ if (!granted.businessId) return false;
205
+ if (granted.businessId !== required.businessId) return false;
206
+ }
207
+
208
+ // Check projectId constraints
209
+ if (required.projectId) {
210
+ if (!granted.projectId) return false;
211
+ if (granted.projectId !== required.projectId) return false;
212
+ }
213
+
214
+ // Check team constraints
215
+ if (required.teamId) {
216
+ if (!granted.teamId) return false;
217
+ if (granted.teamId !== required.teamId) return false;
218
+ }
219
+
220
+ // Check resourceId constraints
221
+ if (required.resourceId) {
222
+ if (!granted.resourceId) return false;
223
+ if (granted.resourceId !== required.resourceId) return false;
224
+ }
225
+
226
+ return true;
227
+ }
228
+
229
+ private _scopeSatisfied(
230
+ granted: ResourceScope,
231
+ required: ResourceScope,
232
+ ): boolean {
233
+ return AuthorizationInterceptor.SCOPE_SATISFIES.get(granted)?.has(
234
+ required,
235
+ ) ?? false;
236
+ }
237
+
238
+ private _findMissingPermissions(
239
+ actorPermissions: Permission[],
240
+ requiredPermissions: Permission[],
241
+ ): string[] {
242
+ const missing: string[] = [];
243
+
244
+ for (const required of requiredPermissions) {
245
+ if (!this._hasPermission(actorPermissions, required)) {
246
+ missing.push(`${required.action}:${required.resource}`);
247
+ }
248
+ }
249
+
250
+ return missing;
251
+ }
252
+ }
@@ -0,0 +1,101 @@
1
+ import type { Interceptor } from "../abstract.interceptor.ts";
2
+ import type { InterceptorContext } from "../type/interceptor-context.type.ts";
3
+
4
+ /**
5
+ * Performance interceptor that measures usecase execution time.
6
+ * Tracks slow operations and warns when they exceed thresholds.
7
+ */
8
+ export class PerformanceInterceptor<Input, Output>
9
+ implements Interceptor<Input, Output> {
10
+ readonly name = "PerformanceInterceptor";
11
+
12
+ private readonly startTimes = new Map<string, number>();
13
+
14
+ constructor(
15
+ private readonly slowThresholdMs: number = 1000,
16
+ private readonly logger: (message: string) => void = console.log,
17
+ ) {}
18
+
19
+ private getExecutionKey(context: InterceptorContext): string {
20
+ return `${context.actor.id}_${context.usecaseName.join("/")}`;
21
+ }
22
+
23
+ beforeExecute(
24
+ input: Input,
25
+ context: InterceptorContext,
26
+ ): Input {
27
+ const key = this.getExecutionKey(context);
28
+ this.startTimes.set(key, performance.now());
29
+
30
+ return input;
31
+ }
32
+
33
+ afterExecute(
34
+ output: Output,
35
+ context: InterceptorContext,
36
+ ): Output {
37
+ const key = this.getExecutionKey(context);
38
+ const startTime = this.startTimes.get(key);
39
+ this.startTimes.delete(key);
40
+
41
+ if (startTime !== undefined) {
42
+ const duration = performance.now() - startTime;
43
+ const action = context.usecaseName[0];
44
+ const resource = context.usecaseName[1];
45
+
46
+ this.logger(
47
+ `[Performance] ${action}/${resource} executed in ${
48
+ duration.toFixed(2)
49
+ }ms`,
50
+ );
51
+
52
+ // Warn if execution was slow
53
+ if (duration > this.slowThresholdMs) {
54
+ this.logger(
55
+ `[Performance WARNING] ${action}/${resource} exceeded threshold ` +
56
+ `(${duration.toFixed(2)}ms > ${this.slowThresholdMs}ms)`,
57
+ );
58
+ }
59
+ }
60
+
61
+ return output;
62
+ }
63
+
64
+ onError(
65
+ _error: Error,
66
+ context: InterceptorContext,
67
+ ): void {
68
+ const key = this.getExecutionKey(context);
69
+ const startTime = this.startTimes.get(key);
70
+ this.startTimes.delete(key);
71
+
72
+ if (startTime !== undefined) {
73
+ const duration = performance.now() - startTime;
74
+ const action = context.usecaseName[0];
75
+ const resource = context.usecaseName[1];
76
+
77
+ this.logger(
78
+ `[Performance] ${action}/${resource} failed after ${
79
+ duration.toFixed(2)
80
+ }ms`,
81
+ );
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Get current performance metrics (useful for monitoring)
87
+ */
88
+ getMetrics(): { activeExecutions: number; keys: string[] } {
89
+ return {
90
+ activeExecutions: this.startTimes.size,
91
+ keys: Array.from(this.startTimes.keys()),
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Clear all tracked executions (useful for cleanup)
97
+ */
98
+ clear(): void {
99
+ this.startTimes.clear();
100
+ }
101
+ }
@@ -0,0 +1,185 @@
1
+ import type {
2
+ JsonSchema,
3
+ MutableJsonSchema,
4
+ } from "../type/json-schema.type.ts";
5
+
6
+ // Base interface for all parameters
7
+ interface LlmBaseParameter {
8
+ readonly name: string;
9
+ readonly description: string;
10
+ readonly isRequired?: boolean;
11
+ }
12
+
13
+ // Primitive type parameters
14
+ interface LlmStringParameter extends LlmBaseParameter {
15
+ readonly type: "string";
16
+ }
17
+
18
+ interface LlmNumberParameter extends LlmBaseParameter {
19
+ readonly type: "number" | "integer";
20
+ }
21
+
22
+ interface LlmBooleanParameter extends LlmBaseParameter {
23
+ readonly type: "boolean";
24
+ }
25
+
26
+ interface LlmNullParameter extends LlmBaseParameter {
27
+ readonly type: "null";
28
+ }
29
+
30
+ // Complex type parameters
31
+ interface LlmArrayParameter extends LlmBaseParameter {
32
+ readonly type: "array";
33
+ readonly items: {
34
+ readonly type: string;
35
+ readonly description?: string;
36
+ readonly properties?: JsonSchema;
37
+ };
38
+ }
39
+
40
+ interface LlmObjectParameter extends LlmBaseParameter {
41
+ readonly type: "object";
42
+ readonly properties?: Record<string, JsonSchema>;
43
+ readonly required?: readonly string[];
44
+ }
45
+
46
+ // Discriminated union for parameters
47
+ export type LlmParameter =
48
+ | LlmStringParameter
49
+ | LlmNumberParameter
50
+ | LlmBooleanParameter
51
+ | LlmNullParameter
52
+ | LlmArrayParameter
53
+ | LlmObjectParameter;
54
+
55
+ // Base interface for all return values
56
+ interface LlmBaseReturnValue {
57
+ readonly description: string;
58
+ }
59
+
60
+ // Primitive type returns
61
+ interface LlmStringReturnValue extends LlmBaseReturnValue {
62
+ readonly type: "string";
63
+ }
64
+
65
+ interface LlmNumberReturnValue extends LlmBaseReturnValue {
66
+ readonly type: "number" | "integer";
67
+ }
68
+
69
+ interface LlmBooleanReturnValue extends LlmBaseReturnValue {
70
+ readonly type: "boolean";
71
+ }
72
+
73
+ interface LlmNullReturnValue extends LlmBaseReturnValue {
74
+ readonly type: "null";
75
+ }
76
+
77
+ // Complex type returns
78
+ interface LlmArrayReturnValue extends LlmBaseReturnValue {
79
+ readonly type: "array";
80
+ readonly items: {
81
+ readonly type: string;
82
+ readonly description?: string;
83
+ readonly properties?: JsonSchema;
84
+ };
85
+ }
86
+
87
+ interface LlmObjectReturnValue extends LlmBaseReturnValue {
88
+ readonly type: "object";
89
+ readonly properties?: Record<string, JsonSchema>;
90
+ readonly required?: readonly string[];
91
+ }
92
+
93
+ // Discriminated union for return values
94
+ export type LlmReturnValue =
95
+ | LlmStringReturnValue
96
+ | LlmNumberReturnValue
97
+ | LlmBooleanReturnValue
98
+ | LlmNullReturnValue
99
+ | LlmArrayReturnValue
100
+ | LlmObjectReturnValue;
101
+
102
+ export interface ApiDefinition {
103
+ readonly name: string;
104
+ readonly description: string;
105
+ readonly parameters: LlmParameter[];
106
+ readonly returns: LlmReturnValue;
107
+ }
108
+
109
+ export function apiDefinitionToJsonSchema(
110
+ definition: ApiDefinition,
111
+ ): JsonSchema {
112
+ const properties: Record<string, MutableJsonSchema> = {};
113
+ const required: string[] = [];
114
+
115
+ for (const param of definition.parameters) {
116
+ const paramSchema: MutableJsonSchema = {
117
+ type: param.type,
118
+ description: param.description,
119
+ };
120
+
121
+ // Add items for array types (JSON Schema 2020-12 compliance)
122
+ if (param.type === "array" && "items" in param) {
123
+ const arrayParam = param as LlmArrayParameter;
124
+ paramSchema.items = arrayParam.items as MutableJsonSchema;
125
+ }
126
+
127
+ // Add properties and required for object types (JSON Schema 2020-12 compliance)
128
+ if (param.type === "object" && "properties" in param) {
129
+ const objectParam = param as LlmObjectParameter;
130
+ if (objectParam.properties) {
131
+ paramSchema.properties = objectParam.properties as Record<
132
+ string,
133
+ MutableJsonSchema
134
+ >;
135
+ }
136
+ if (objectParam.required) {
137
+ paramSchema.required = [...objectParam.required];
138
+ }
139
+ }
140
+
141
+ properties[param.name] = paramSchema;
142
+
143
+ if (param.isRequired !== false) {
144
+ required.push(param.name);
145
+ }
146
+ }
147
+
148
+ const returnsSchema: MutableJsonSchema = {
149
+ type: definition.returns.type,
150
+ description: definition.returns.description,
151
+ };
152
+
153
+ // Add items for array return types (JSON Schema 2020-12 compliance)
154
+ if (definition.returns.type === "array" && "items" in definition.returns) {
155
+ const arrayReturn = definition.returns as LlmArrayReturnValue;
156
+ returnsSchema.items = arrayReturn.items as MutableJsonSchema;
157
+ }
158
+
159
+ // Add properties and required for object return types (JSON Schema 2020-12 compliance)
160
+ if (
161
+ definition.returns.type === "object" && "properties" in definition.returns
162
+ ) {
163
+ const objectReturn = definition.returns as LlmObjectReturnValue;
164
+ if (objectReturn.properties) {
165
+ returnsSchema.properties = objectReturn.properties as Record<
166
+ string,
167
+ MutableJsonSchema
168
+ >;
169
+ }
170
+ if (objectReturn.required) {
171
+ returnsSchema.required = [...objectReturn.required];
172
+ }
173
+ }
174
+
175
+ return {
176
+ name: definition.name,
177
+ description: definition.description,
178
+ parameters: {
179
+ type: "object",
180
+ properties,
181
+ required,
182
+ },
183
+ returns: returnsSchema,
184
+ } as JsonSchema;
185
+ }