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