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.
Files changed (83) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/CHANGELOG.md +17 -0
  3. package/README.md +290 -106
  4. package/dist/actions.d.ts +95 -0
  5. package/dist/actions.d.ts.map +1 -0
  6. package/dist/actions.js +437 -0
  7. package/dist/actions.js.map +1 -0
  8. package/dist/approve.d.ts +49 -0
  9. package/dist/approve.d.ts.map +1 -0
  10. package/dist/approve.js +235 -0
  11. package/dist/approve.js.map +1 -0
  12. package/dist/ask.d.ts +42 -0
  13. package/dist/ask.d.ts.map +1 -0
  14. package/dist/ask.js +227 -0
  15. package/dist/ask.js.map +1 -0
  16. package/dist/decide.d.ts +62 -0
  17. package/dist/decide.d.ts.map +1 -0
  18. package/dist/decide.js +245 -0
  19. package/dist/decide.js.map +1 -0
  20. package/dist/do.d.ts +63 -0
  21. package/dist/do.d.ts.map +1 -0
  22. package/dist/do.js +228 -0
  23. package/dist/do.js.map +1 -0
  24. package/dist/generate.d.ts +61 -0
  25. package/dist/generate.d.ts.map +1 -0
  26. package/dist/generate.js +299 -0
  27. package/dist/generate.js.map +1 -0
  28. package/dist/goals.d.ts +89 -0
  29. package/dist/goals.d.ts.map +1 -0
  30. package/dist/goals.js +206 -0
  31. package/dist/goals.js.map +1 -0
  32. package/dist/index.d.ts +68 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +69 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/is.d.ts +54 -0
  37. package/dist/is.d.ts.map +1 -0
  38. package/dist/is.js +318 -0
  39. package/dist/is.js.map +1 -0
  40. package/dist/kpis.d.ts +103 -0
  41. package/dist/kpis.d.ts.map +1 -0
  42. package/dist/kpis.js +271 -0
  43. package/dist/kpis.js.map +1 -0
  44. package/dist/notify.d.ts +47 -0
  45. package/dist/notify.d.ts.map +1 -0
  46. package/dist/notify.js +220 -0
  47. package/dist/notify.js.map +1 -0
  48. package/dist/role.d.ts +53 -0
  49. package/dist/role.d.ts.map +1 -0
  50. package/dist/role.js +111 -0
  51. package/dist/role.js.map +1 -0
  52. package/dist/team.d.ts +61 -0
  53. package/dist/team.d.ts.map +1 -0
  54. package/dist/team.js +131 -0
  55. package/dist/team.js.map +1 -0
  56. package/dist/transports.d.ts +164 -0
  57. package/dist/transports.d.ts.map +1 -0
  58. package/dist/transports.js +358 -0
  59. package/dist/transports.js.map +1 -0
  60. package/dist/types.d.ts +693 -0
  61. package/dist/types.d.ts.map +1 -0
  62. package/dist/types.js +72 -0
  63. package/dist/types.js.map +1 -0
  64. package/package.json +27 -61
  65. package/src/actions.ts +615 -0
  66. package/src/approve.ts +317 -0
  67. package/src/ask.ts +304 -0
  68. package/src/decide.ts +295 -0
  69. package/src/do.ts +275 -0
  70. package/src/generate.ts +364 -0
  71. package/src/goals.ts +220 -0
  72. package/src/index.ts +118 -0
  73. package/src/is.ts +372 -0
  74. package/src/kpis.ts +348 -0
  75. package/src/notify.ts +303 -0
  76. package/src/role.ts +116 -0
  77. package/src/team.ts +142 -0
  78. package/src/transports.ts +504 -0
  79. package/src/types.ts +843 -0
  80. package/test/actions.test.ts +546 -0
  81. package/test/standalone.test.ts +299 -0
  82. package/test/types.test.ts +460 -0
  83. 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
+ }