UNPKG

business-as-code

Version:

Primitives for expressing business logic and processes as code

426 lines (373 loc) 12.6 kB
/** * Objectives and Key Results (OKRs) management * * Uses org.ai OKR types for standardized OKR definitions across the ecosystem. */ import type { OKRDefinition, KeyResult } from './types.js' import type { OKR as OrgOKR, KeyResult as OrgKeyResult, OKRStatus, KeyResultStatus } from 'org.ai' // Re-export org.ai OKR types for convenience export type { OrgOKR, OrgKeyResult, OKRStatus, KeyResultStatus } /** * Convert a business-as-code KeyResult to an org.ai KeyResult * * @param kr - Business key result * @param id - Optional identifier * @returns org.ai KeyResult object */ export function toOrgKeyResult(kr: KeyResult, id?: string): OrgKeyResult { const result: OrgKeyResult = { description: kr.description, } if (id !== undefined) result.id = id if (kr.description) result.name = kr.description if (kr.metric !== undefined) result.metric = kr.metric if (kr.startValue !== undefined) result.startValue = kr.startValue if (kr.targetValue !== undefined) { result.targetValue = kr.targetValue result.target = kr.targetValue } if (kr.currentValue !== undefined) { result.currentValue = kr.currentValue result.current = kr.currentValue } if (kr.unit !== undefined) result.unit = kr.unit if (kr.progress !== undefined) result.progress = kr.progress return result } /** * Convert an org.ai KeyResult to a business-as-code KeyResult * * @param kr - org.ai KeyResult object * @returns Business key result */ export function fromOrgKeyResult(kr: OrgKeyResult): KeyResult { const result: KeyResult = { description: kr.description || kr.name || '', metric: kr.metric || '', targetValue: kr.targetValue ?? kr.target ?? 0, } if (kr.startValue !== undefined) result.startValue = kr.startValue if (kr.currentValue !== undefined) result.currentValue = kr.currentValue else if (kr.current !== undefined) result.currentValue = kr.current if (kr.unit !== undefined) result.unit = kr.unit if (kr.progress !== undefined) result.progress = kr.progress return result } /** * Convert a business-as-code OKRDefinition to an org.ai OKR * * @param definition - Business OKR definition * @param id - Optional unique identifier * @returns org.ai OKR object */ export function toOrgOKR(definition: OKRDefinition, id?: string): OrgOKR { const result: OrgOKR = { objective: definition.objective, keyResults: definition.keyResults?.map((kr, i) => toOrgKeyResult(kr, `${id}_kr_${i}`)) || [], } if (id !== undefined) result.id = id if (definition.description !== undefined) result.description = definition.description if (definition.owner !== undefined) result.owner = definition.owner if (definition.period !== undefined) result.period = definition.period if (definition.status !== undefined) result.status = definition.status as OKRStatus result.progress = calculateOKRProgress(definition) if (definition.confidence !== undefined) result.confidence = definition.confidence if (definition.metadata !== undefined) result.metadata = definition.metadata return result } /** * Convert an org.ai OKR to a business-as-code OKRDefinition * * @param okr - org.ai OKR object * @returns Business OKR definition */ export function fromOrgOKR(okr: OrgOKR): OKRDefinition { const result: OKRDefinition = { objective: okr.objective, keyResults: okr.keyResults.map(fromOrgKeyResult), } if (okr.description !== undefined) result.description = okr.description if (okr.owner !== undefined) result.owner = okr.owner if (okr.period !== undefined) result.period = okr.period const st = okr.status if (st !== undefined) result.status = st as NonNullable<OKRDefinition['status']> if (okr.confidence !== undefined) result.confidence = okr.confidence if (okr.metadata !== undefined) result.metadata = okr.metadata return result } /** * Define Objectives and Key Results for goal tracking * * @example * ```ts * const quarterlyOKRs = okrs([ * { * objective: 'Achieve Product-Market Fit', * description: 'Validate that our product solves a real problem for customers', * period: 'Q2 2024', * owner: 'CEO', * keyResults: [ * { * description: 'Increase Net Promoter Score', * metric: 'NPS', * startValue: 40, * targetValue: 60, * currentValue: 52, * unit: 'score', * progress: 60, * }, * { * description: 'Reduce monthly churn rate', * metric: 'Churn Rate', * startValue: 8, * targetValue: 4, * currentValue: 5.5, * unit: 'percent', * progress: 62.5, * }, * { * description: 'Achieve customer retention', * metric: 'Customers with 3+ months', * startValue: 50, * targetValue: 200, * currentValue: 125, * unit: 'customers', * progress: 50, * }, * ], * status: 'on-track', * confidence: 75, * }, * ]) * ``` */ export function okrs(definitions: OKRDefinition[]): OKRDefinition[] { return definitions.map((okr) => validateAndNormalizeOKR(okr)) } /** * Define a single OKR */ export function okr(definition: OKRDefinition): OKRDefinition { return validateAndNormalizeOKR(definition) } /** * Validate and normalize an OKR definition */ function validateAndNormalizeOKR(okr: OKRDefinition): OKRDefinition { if (!okr.objective) { throw new Error('OKR objective is required') } // Calculate progress for key results if not set const keyResults = okr.keyResults?.map((kr) => ({ ...kr, progress: kr.progress ?? calculateKeyResultProgress(kr), })) const result: OKRDefinition = { ...okr, status: okr.status || 'not-started', confidence: okr.confidence ?? calculateConfidence(keyResults || []), metadata: okr.metadata || {}, } if (keyResults !== undefined) result.keyResults = keyResults return result } /** * Calculate key result progress */ export function calculateKeyResultProgress(kr: KeyResult): number { if (kr.currentValue === undefined || kr.startValue === undefined) return 0 const total = kr.targetValue - kr.startValue if (total === 0) return 100 const current = kr.currentValue - kr.startValue const progress = (current / total) * 100 return Math.max(0, Math.min(100, progress)) } /** * Calculate overall OKR progress */ export function calculateOKRProgress(okr: OKRDefinition): number { if (!okr.keyResults || okr.keyResults.length === 0) return 0 const totalProgress = okr.keyResults.reduce((sum, kr) => { return sum + (kr.progress ?? calculateKeyResultProgress(kr)) }, 0) return totalProgress / okr.keyResults.length } /** * Calculate confidence score based on key results */ export function calculateConfidence(keyResults: KeyResult[]): number { if (keyResults.length === 0) return 0 const totalProgress = keyResults.reduce((sum, kr) => { return sum + (kr.progress ?? calculateKeyResultProgress(kr)) }, 0) const avgProgress = totalProgress / keyResults.length // Confidence tends to be slightly lower than actual progress return Math.max(0, Math.min(100, avgProgress - 10)) } /** * Update key result current value */ export function updateKeyResult( okr: OKRDefinition, krDescription: string, currentValue: number ): OKRDefinition { const keyResults = okr.keyResults?.map((kr) => { if (kr.description === krDescription) { const updatedKR = { ...kr, currentValue } return { ...updatedKR, progress: calculateKeyResultProgress(updatedKR), } } return kr }) // Recalculate overall status and confidence const okrWithKeyResults: OKRDefinition = { ...okr } if (keyResults !== undefined) okrWithKeyResults.keyResults = keyResults const progress = calculateOKRProgress(okrWithKeyResults) const status = determineOKRStatus(progress, okr.confidence || 0) const result: OKRDefinition = { ...okr, confidence: calculateConfidence(keyResults || []), } if (keyResults !== undefined) result.keyResults = keyResults if (status !== undefined) result.status = status return result } /** * Determine OKR status based on progress and confidence */ function determineOKRStatus(progress: number, confidence: number): OKRDefinition['status'] { if (progress === 0) return 'not-started' if (progress === 100) return 'completed' if (confidence < 50 || progress < 30) return 'at-risk' return 'on-track' } /** * Check if key result is on track */ export function isKeyResultOnTrack(kr: KeyResult): boolean { const progress = kr.progress ?? calculateKeyResultProgress(kr) return progress >= 70 } /** * Check if OKR is on track */ export function isOKROnTrack(okr: OKRDefinition): boolean { const progress = calculateOKRProgress(okr) return progress >= 70 && (okr.confidence ?? 0) >= 60 } /** * Get key results that are on track */ export function getKeyResultsOnTrack(okr: OKRDefinition): KeyResult[] { return okr.keyResults?.filter(isKeyResultOnTrack) || [] } /** * Get key results that are at risk */ export function getKeyResultsAtRisk(okr: OKRDefinition): KeyResult[] { return okr.keyResults?.filter((kr) => !isKeyResultOnTrack(kr)) || [] } /** * Get OKRs by owner */ export function getOKRsByOwner(okrs: OKRDefinition[], owner: string): OKRDefinition[] { return okrs.filter((okr) => okr.owner === owner) } /** * Get OKRs by period */ export function getOKRsByPeriod(okrs: OKRDefinition[], period: string): OKRDefinition[] { return okrs.filter((okr) => okr.period === period) } /** * Get OKRs by status */ export function getOKRsByStatus( okrs: OKRDefinition[], status: OKRDefinition['status'] ): OKRDefinition[] { return okrs.filter((okr) => okr.status === status) } /** * Calculate success rate across all OKRs */ export function calculateSuccessRate(okrs: OKRDefinition[]): number { if (okrs.length === 0) return 0 const avgProgress = okrs.reduce((sum, okr) => { return sum + calculateOKRProgress(okr) }, 0) / okrs.length return avgProgress } /** * Format key result for display */ export function formatKeyResult(kr: KeyResult): string { const progress = kr.progress ?? calculateKeyResultProgress(kr) const current = kr.currentValue ?? kr.startValue ?? 0 const target = kr.targetValue return `${kr.description}: ${current}/${target} ${kr.unit || ''} (${progress.toFixed(0)}%)` } /** * Compare OKR performance between periods */ export function compareOKRPerformance( current: OKRDefinition, previous: OKRDefinition ): { progressDelta: number confidenceDelta: number improved: boolean } { const currentProgress = calculateOKRProgress(current) const previousProgress = calculateOKRProgress(previous) const progressDelta = currentProgress - previousProgress const currentConfidence = current.confidence ?? 0 const previousConfidence = previous.confidence ?? 0 const confidenceDelta = currentConfidence - previousConfidence const improved = progressDelta > 0 && confidenceDelta >= 0 return { progressDelta, confidenceDelta, improved } } /** * Validate OKR definitions */ export function validateOKRs(okrs: OKRDefinition[]): { valid: boolean; errors: string[] } { const errors: string[] = [] for (const okr of okrs) { if (!okr.objective) { errors.push('OKR objective is required') } if (okr.objective && okr.objective.length < 10) { errors.push(`OKR objective "${okr.objective}" should be at least 10 characters`) } if (okr.confidence !== undefined && (okr.confidence < 0 || okr.confidence > 100)) { errors.push(`OKR "${okr.objective}" confidence must be between 0 and 100`) } if (okr.keyResults) { if (okr.keyResults.length === 0) { errors.push(`OKR "${okr.objective}" must have at least one key result`) } if (okr.keyResults.length > 5) { errors.push(`OKR "${okr.objective}" should have no more than 5 key results`) } for (const kr of okr.keyResults) { if (!kr.description) { errors.push(`Key result in OKR "${okr.objective}" must have a description`) } if (!kr.metric) { errors.push(`Key result "${kr.description}" must have a metric`) } if (kr.progress !== undefined && (kr.progress < 0 || kr.progress > 100)) { errors.push(`Key result "${kr.description}" progress must be between 0 and 100`) } } } } return { valid: errors.length === 0, errors, } }