digital-workers 2.0.2 → 2.1.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.
package/src/is.js ADDED
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Type validation and checking functionality for digital workers
3
+ */
4
+ import { generateObject } from 'ai-functions';
5
+ import { schema as convertSchema } from 'ai-functions';
6
+ /**
7
+ * Check if a value matches an expected type or schema
8
+ *
9
+ * Uses AI-powered validation for complex types and schemas.
10
+ * Can also perform type coercion when enabled.
11
+ *
12
+ * @param value - The value to check
13
+ * @param type - Type name or schema to validate against
14
+ * @param options - Validation options
15
+ * @returns Promise resolving to validation result
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * // Simple type checking
20
+ * const result = await is('hello@example.com', 'email')
21
+ * console.log(result.valid) // true
22
+ * ```
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * // Schema validation
27
+ * const result = await is(
28
+ * { name: 'John', age: 30 },
29
+ * {
30
+ * name: 'Full name',
31
+ * age: 'Age in years (number)',
32
+ * email: 'Email address',
33
+ * }
34
+ * )
35
+ * console.log(result.valid) // false - missing email
36
+ * console.log(result.errors) // ['Missing required field: email']
37
+ * ```
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * // With coercion
42
+ * const result = await is('123', 'number', { coerce: true })
43
+ * console.log(result.valid) // true
44
+ * console.log(result.value) // 123 (as number)
45
+ * ```
46
+ */
47
+ export async function is(value, type, options = {}) {
48
+ const { coerce = false, strict = false } = options;
49
+ // Handle simple type strings
50
+ if (typeof type === 'string') {
51
+ return validateSimpleType(value, type, { coerce, strict });
52
+ }
53
+ // Handle schema validation
54
+ return validateSchema(value, type, { coerce, strict });
55
+ }
56
+ /**
57
+ * Validate against a simple type name
58
+ */
59
+ async function validateSimpleType(value, type, options) {
60
+ const { coerce, strict } = options;
61
+ // Built-in JavaScript types
62
+ const builtInTypes = {
63
+ string: (v) => typeof v === 'string',
64
+ number: (v) => typeof v === 'number' && !isNaN(v),
65
+ boolean: (v) => typeof v === 'boolean',
66
+ object: (v) => typeof v === 'object' && v !== null && !Array.isArray(v),
67
+ array: (v) => Array.isArray(v),
68
+ null: (v) => v === null,
69
+ undefined: (v) => v === undefined,
70
+ function: (v) => typeof v === 'function',
71
+ };
72
+ // Check built-in types first
73
+ if (type in builtInTypes) {
74
+ const isValid = builtInTypes[type](value);
75
+ if (!isValid && coerce) {
76
+ // Try to coerce the value
77
+ const coerced = coerceValue(value, type);
78
+ if (coerced.success) {
79
+ return {
80
+ valid: true,
81
+ value: coerced.value,
82
+ };
83
+ }
84
+ }
85
+ return {
86
+ valid: isValid,
87
+ value: isValid ? value : undefined,
88
+ errors: isValid ? undefined : [`Value is not a valid ${type}`],
89
+ };
90
+ }
91
+ // Use AI for complex type validation
92
+ const result = await generateObject({
93
+ model: 'sonnet',
94
+ schema: {
95
+ valid: 'Whether the value matches the expected type (boolean)',
96
+ errors: ['List of validation errors if invalid'],
97
+ coercedValue: coerce ? 'The value coerced to the expected type' : undefined,
98
+ },
99
+ system: `You are a type validation expert. Determine if a value matches an expected type.
100
+
101
+ ${coerce ? 'If the value can be coerced to the expected type, provide the coerced value.' : ''}
102
+ ${strict ? 'Be strict in your validation - require exact type matches.' : 'Be flexible - allow reasonable type conversions.'}`,
103
+ prompt: `Validate if this value matches the expected type:
104
+
105
+ Value: ${JSON.stringify(value)}
106
+ Type: ${type}
107
+
108
+ Determine if the value is valid for this type.`,
109
+ });
110
+ const validation = result.object;
111
+ return {
112
+ valid: validation.valid,
113
+ value: coerce && validation.coercedValue !== undefined ? validation.coercedValue : value,
114
+ errors: validation.valid ? undefined : validation.errors,
115
+ };
116
+ }
117
+ /**
118
+ * Validate against a schema
119
+ */
120
+ async function validateSchema(value, schema, options) {
121
+ const { coerce, strict } = options;
122
+ try {
123
+ // Convert SimpleSchema to Zod schema
124
+ const zodSchema = convertSchema(schema);
125
+ // Parse the value
126
+ const parsed = zodSchema.parse(value);
127
+ return {
128
+ valid: true,
129
+ value: parsed,
130
+ };
131
+ }
132
+ catch (error) {
133
+ if (strict) {
134
+ return {
135
+ valid: false,
136
+ errors: [error.message],
137
+ };
138
+ }
139
+ // Use AI for more flexible validation
140
+ const result = await generateObject({
141
+ model: 'sonnet',
142
+ schema: {
143
+ valid: 'Whether the value matches the schema (boolean)',
144
+ errors: ['List of validation errors'],
145
+ coercedValue: coerce ? 'The value with corrections/coercions applied' : undefined,
146
+ },
147
+ system: `You are a schema validation expert. Validate a value against a schema.
148
+
149
+ ${coerce ? 'Try to coerce the value to match the schema where reasonable.' : ''}
150
+ Be helpful - provide clear error messages.`,
151
+ prompt: `Validate this value against the schema:
152
+
153
+ Value:
154
+ ${JSON.stringify(value, null, 2)}
155
+
156
+ Schema:
157
+ ${JSON.stringify(schema, null, 2)}
158
+
159
+ Check if the value matches the schema structure and types.`,
160
+ });
161
+ const validation = result.object;
162
+ return {
163
+ valid: validation.valid,
164
+ value: coerce && validation.coercedValue !== undefined ? validation.coercedValue : value,
165
+ errors: validation.valid ? undefined : validation.errors,
166
+ };
167
+ }
168
+ }
169
+ /**
170
+ * Try to coerce a value to a specific type
171
+ */
172
+ function coerceValue(value, type) {
173
+ try {
174
+ switch (type) {
175
+ case 'string':
176
+ return { success: true, value: String(value) };
177
+ case 'number':
178
+ const num = Number(value);
179
+ return { success: !isNaN(num), value: num };
180
+ case 'boolean':
181
+ if (typeof value === 'string') {
182
+ const lower = value.toLowerCase();
183
+ if (lower === 'true' || lower === '1') {
184
+ return { success: true, value: true };
185
+ }
186
+ if (lower === 'false' || lower === '0') {
187
+ return { success: true, value: false };
188
+ }
189
+ }
190
+ return { success: true, value: Boolean(value) };
191
+ case 'array':
192
+ if (Array.isArray(value)) {
193
+ return { success: true, value };
194
+ }
195
+ return { success: true, value: [value] };
196
+ default:
197
+ return { success: false };
198
+ }
199
+ }
200
+ catch {
201
+ return { success: false };
202
+ }
203
+ }
204
+ /**
205
+ * Check if a value is valid email
206
+ *
207
+ * @param value - Value to check
208
+ * @returns Promise resolving to validation result
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * const result = await is.email('test@example.com')
213
+ * console.log(result.valid) // true
214
+ * ```
215
+ */
216
+ is.email = async (value) => {
217
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
218
+ const valid = typeof value === 'string' && emailRegex.test(value);
219
+ return {
220
+ valid,
221
+ value: valid ? value : undefined,
222
+ errors: valid ? undefined : ['Invalid email format'],
223
+ };
224
+ };
225
+ /**
226
+ * Check if a value is a valid URL
227
+ *
228
+ * @param value - Value to check
229
+ * @returns Promise resolving to validation result
230
+ */
231
+ is.url = async (value) => {
232
+ try {
233
+ if (typeof value !== 'string') {
234
+ return {
235
+ valid: false,
236
+ errors: ['Value must be a string'],
237
+ };
238
+ }
239
+ new URL(value);
240
+ return {
241
+ valid: true,
242
+ value,
243
+ };
244
+ }
245
+ catch {
246
+ return {
247
+ valid: false,
248
+ errors: ['Invalid URL format'],
249
+ };
250
+ }
251
+ };
252
+ /**
253
+ * Check if a value is a valid date
254
+ *
255
+ * @param value - Value to check
256
+ * @param options - Validation options
257
+ * @returns Promise resolving to validation result
258
+ */
259
+ is.date = async (value, options = {}) => {
260
+ const { coerce } = options;
261
+ if (value instanceof Date) {
262
+ return {
263
+ valid: !isNaN(value.getTime()),
264
+ value,
265
+ errors: isNaN(value.getTime()) ? ['Invalid date'] : undefined,
266
+ };
267
+ }
268
+ if (coerce) {
269
+ try {
270
+ const date = new Date(value);
271
+ if (!isNaN(date.getTime())) {
272
+ return {
273
+ valid: true,
274
+ value: date,
275
+ };
276
+ }
277
+ }
278
+ catch {
279
+ // Fall through to invalid
280
+ }
281
+ }
282
+ return {
283
+ valid: false,
284
+ errors: ['Invalid date'],
285
+ };
286
+ };
287
+ /**
288
+ * Check if a value matches a custom validation function
289
+ *
290
+ * @param value - Value to check
291
+ * @param validator - Validation function
292
+ * @returns Promise resolving to validation result
293
+ *
294
+ * @example
295
+ * ```ts
296
+ * const result = await is.custom(
297
+ * 42,
298
+ * (v) => typeof v === 'number' && v > 0 && v < 100
299
+ * )
300
+ * ```
301
+ */
302
+ is.custom = async (value, validator) => {
303
+ try {
304
+ const valid = await validator(value);
305
+ return {
306
+ valid,
307
+ value: valid ? value : undefined,
308
+ errors: valid ? undefined : ['Custom validation failed'],
309
+ };
310
+ }
311
+ catch (error) {
312
+ return {
313
+ valid: false,
314
+ errors: [error.message],
315
+ };
316
+ }
317
+ };
package/src/kpis.js ADDED
@@ -0,0 +1,270 @@
1
+ /**
2
+ * KPI and OKR tracking functionality for digital workers
3
+ */
4
+ export function kpis(definition) {
5
+ return definition;
6
+ }
7
+ /**
8
+ * Update a KPI's current value
9
+ *
10
+ * @param kpi - The KPI to update
11
+ * @param current - New current value
12
+ * @returns Updated KPI
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const updated = kpis.update(deploymentFrequency, 8)
17
+ * console.log(updated.current) // 8
18
+ * console.log(updated.trend) // 'up' (automatically determined)
19
+ * ```
20
+ */
21
+ kpis.update = (kpi, current) => {
22
+ // Determine trend
23
+ let trend = 'stable';
24
+ if (current > kpi.current) {
25
+ trend = 'up';
26
+ }
27
+ else if (current < kpi.current) {
28
+ trend = 'down';
29
+ }
30
+ return {
31
+ ...kpi,
32
+ current,
33
+ trend,
34
+ };
35
+ };
36
+ /**
37
+ * Calculate progress towards target (0-1)
38
+ *
39
+ * @param kpi - The KPI
40
+ * @returns Progress as a decimal (0-1)
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * const kpi = { current: 75, target: 100 }
45
+ * const progress = kpis.progress(kpi) // 0.75
46
+ * ```
47
+ */
48
+ kpis.progress = (kpi) => {
49
+ if (kpi.target === 0)
50
+ return 0;
51
+ return Math.min(1, Math.max(0, kpi.current / kpi.target));
52
+ };
53
+ /**
54
+ * Check if a KPI is on track
55
+ *
56
+ * @param kpi - The KPI
57
+ * @param threshold - Minimum progress to be "on track" (default: 0.8)
58
+ * @returns Whether the KPI is on track
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * const kpi = { current: 85, target: 100 }
63
+ * const onTrack = kpis.onTrack(kpi) // true (85% >= 80%)
64
+ * ```
65
+ */
66
+ kpis.onTrack = (kpi, threshold = 0.8) => {
67
+ return kpis.progress(kpi) >= threshold;
68
+ };
69
+ /**
70
+ * Get the gap to target
71
+ *
72
+ * @param kpi - The KPI
73
+ * @returns Difference between target and current
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * const kpi = { current: 75, target: 100 }
78
+ * const gap = kpis.gap(kpi) // 25
79
+ * ```
80
+ */
81
+ kpis.gap = (kpi) => {
82
+ return kpi.target - kpi.current;
83
+ };
84
+ /**
85
+ * Format a KPI for display
86
+ *
87
+ * @param kpi - The KPI
88
+ * @returns Formatted string
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * const kpi = {
93
+ * name: 'Deployment Frequency',
94
+ * current: 5,
95
+ * target: 10,
96
+ * unit: 'deploys/week',
97
+ * trend: 'up',
98
+ * }
99
+ * const formatted = kpis.format(kpi)
100
+ * // "Deployment Frequency: 5/10 deploys/week (50%, trending up)"
101
+ * ```
102
+ */
103
+ kpis.format = (kpi) => {
104
+ const progress = kpis.progress(kpi);
105
+ const progressPercent = Math.round(progress * 100);
106
+ const trendEmoji = kpi.trend === 'up' ? '↑' : kpi.trend === 'down' ? '↓' : '→';
107
+ return `${kpi.name}: ${kpi.current}/${kpi.target} ${kpi.unit} (${progressPercent}%, trending ${kpi.trend} ${trendEmoji})`;
108
+ };
109
+ /**
110
+ * Compare two KPI snapshots to see change over time
111
+ *
112
+ * @param previous - Previous KPI state
113
+ * @param current - Current KPI state
114
+ * @returns Change analysis
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * const change = kpis.compare(previousKPI, currentKPI)
119
+ * console.log(change.delta) // 5
120
+ * console.log(change.percentChange) // 10
121
+ * console.log(change.improved) // true
122
+ * ```
123
+ */
124
+ kpis.compare = (previous, current) => {
125
+ const delta = current.current - previous.current;
126
+ const percentChange = previous.current !== 0
127
+ ? (delta / previous.current) * 100
128
+ : 0;
129
+ // Improved if we got closer to the target
130
+ const previousGap = Math.abs(previous.target - previous.current);
131
+ const currentGap = Math.abs(current.target - current.current);
132
+ const improved = currentGap < previousGap;
133
+ return {
134
+ delta,
135
+ percentChange,
136
+ improved,
137
+ };
138
+ };
139
+ /**
140
+ * Define OKRs (Objectives and Key Results)
141
+ *
142
+ * @param definition - OKR definition
143
+ * @returns The defined OKR
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * const engineeringOKR = okrs({
148
+ * objective: 'Improve development velocity',
149
+ * keyResults: [
150
+ * {
151
+ * name: 'Deployment Frequency',
152
+ * current: 5,
153
+ * target: 10,
154
+ * unit: 'deploys/week',
155
+ * },
156
+ * {
157
+ * name: 'Lead Time',
158
+ * current: 48,
159
+ * target: 24,
160
+ * unit: 'hours',
161
+ * },
162
+ * {
163
+ * name: 'Change Failure Rate',
164
+ * current: 15,
165
+ * target: 5,
166
+ * unit: '%',
167
+ * },
168
+ * ],
169
+ * owner: 'engineering-team',
170
+ * dueDate: new Date('2024-03-31'),
171
+ * })
172
+ * ```
173
+ */
174
+ export function okrs(definition) {
175
+ return definition;
176
+ }
177
+ /**
178
+ * Calculate overall OKR progress
179
+ *
180
+ * @param okr - The OKR
181
+ * @returns Average progress across all key results (0-1)
182
+ *
183
+ * @example
184
+ * ```ts
185
+ * const progress = okrs.progress(engineeringOKR)
186
+ * console.log(progress) // 0.67 (67% complete)
187
+ * ```
188
+ */
189
+ okrs.progress = (okr) => {
190
+ if (okr.keyResults.length === 0)
191
+ return 0;
192
+ const totalProgress = okr.keyResults.reduce((sum, kr) => {
193
+ return sum + kpis.progress(kr);
194
+ }, 0);
195
+ return totalProgress / okr.keyResults.length;
196
+ };
197
+ /**
198
+ * Update a key result in an OKR
199
+ *
200
+ * @param okr - The OKR
201
+ * @param keyResultName - Name of key result to update
202
+ * @param current - New current value
203
+ * @returns Updated OKR
204
+ *
205
+ * @example
206
+ * ```ts
207
+ * const updated = okrs.updateKeyResult(
208
+ * engineeringOKR,
209
+ * 'Deployment Frequency',
210
+ * 8
211
+ * )
212
+ * ```
213
+ */
214
+ okrs.updateKeyResult = (okr, keyResultName, current) => {
215
+ return {
216
+ ...okr,
217
+ keyResults: okr.keyResults.map((kr) => kr.name === keyResultName ? { ...kr, current } : kr),
218
+ progress: undefined, // Will be recalculated
219
+ };
220
+ };
221
+ /**
222
+ * Check if OKR is on track
223
+ *
224
+ * @param okr - The OKR
225
+ * @param threshold - Minimum progress to be "on track" (default: 0.7)
226
+ * @returns Whether the OKR is on track
227
+ *
228
+ * @example
229
+ * ```ts
230
+ * const onTrack = okrs.onTrack(engineeringOKR)
231
+ * ```
232
+ */
233
+ okrs.onTrack = (okr, threshold = 0.7) => {
234
+ return okrs.progress(okr) >= threshold;
235
+ };
236
+ /**
237
+ * Format OKR for display
238
+ *
239
+ * @param okr - The OKR
240
+ * @returns Formatted string
241
+ *
242
+ * @example
243
+ * ```ts
244
+ * const formatted = okrs.format(engineeringOKR)
245
+ * console.log(formatted)
246
+ * // Improve development velocity (67% complete)
247
+ * // • Deployment Frequency: 5/10 deploys/week (50%)
248
+ * // • Lead Time: 48/24 hours (200%)
249
+ * // • Change Failure Rate: 15/5 % (300%)
250
+ * ```
251
+ */
252
+ okrs.format = (okr) => {
253
+ const progress = okrs.progress(okr);
254
+ const progressPercent = Math.round(progress * 100);
255
+ const lines = [
256
+ `${okr.objective} (${progressPercent}% complete)`,
257
+ ...okr.keyResults.map((kr) => {
258
+ const krProgress = kpis.progress(kr);
259
+ const krPercent = Math.round(krProgress * 100);
260
+ return ` • ${kr.name}: ${kr.current}/${kr.target} ${kr.unit} (${krPercent}%)`;
261
+ }),
262
+ ];
263
+ if (okr.owner) {
264
+ lines.push(` Owner: ${okr.owner}`);
265
+ }
266
+ if (okr.dueDate) {
267
+ lines.push(` Due: ${okr.dueDate.toLocaleDateString()}`);
268
+ }
269
+ return lines.join('\n');
270
+ };