business-as-code
Version:
Primitives for expressing business logic and processes as code
391 lines • 12.2 kB
JavaScript
/**
* Objectives and Key Results (OKRs) management
*
* Uses org.ai OKR types for standardized OKR definitions across the ecosystem.
*/
/**
* 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, id) {
const result = {
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) {
const result = {
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, id) {
const result = {
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;
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) {
const result = {
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;
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) {
return definitions.map((okr) => validateAndNormalizeOKR(okr));
}
/**
* Define a single OKR
*/
export function okr(definition) {
return validateAndNormalizeOKR(definition);
}
/**
* Validate and normalize an OKR definition
*/
function validateAndNormalizeOKR(okr) {
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 = {
...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) {
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) {
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) {
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, krDescription, currentValue) {
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 = { ...okr };
if (keyResults !== undefined)
okrWithKeyResults.keyResults = keyResults;
const progress = calculateOKRProgress(okrWithKeyResults);
const status = determineOKRStatus(progress, okr.confidence || 0);
const result = {
...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, confidence) {
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) {
const progress = kr.progress ?? calculateKeyResultProgress(kr);
return progress >= 70;
}
/**
* Check if OKR is on track
*/
export function isOKROnTrack(okr) {
const progress = calculateOKRProgress(okr);
return progress >= 70 && (okr.confidence ?? 0) >= 60;
}
/**
* Get key results that are on track
*/
export function getKeyResultsOnTrack(okr) {
return okr.keyResults?.filter(isKeyResultOnTrack) || [];
}
/**
* Get key results that are at risk
*/
export function getKeyResultsAtRisk(okr) {
return okr.keyResults?.filter((kr) => !isKeyResultOnTrack(kr)) || [];
}
/**
* Get OKRs by owner
*/
export function getOKRsByOwner(okrs, owner) {
return okrs.filter((okr) => okr.owner === owner);
}
/**
* Get OKRs by period
*/
export function getOKRsByPeriod(okrs, period) {
return okrs.filter((okr) => okr.period === period);
}
/**
* Get OKRs by status
*/
export function getOKRsByStatus(okrs, status) {
return okrs.filter((okr) => okr.status === status);
}
/**
* Calculate success rate across all OKRs
*/
export function calculateSuccessRate(okrs) {
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) {
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, previous) {
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) {
const errors = [];
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,
};
}
//# sourceMappingURL=okrs.js.map