business-as-code
Version:
Primitives for expressing business logic and processes as code
317 lines (276 loc) • 8.06 kB
text/typescript
/**
* Business goals definition and tracking
*
* Uses org.ai Goal types for standardized goal definitions across the ecosystem.
*/
import type { GoalDefinition } from './types.js'
import type {
Goal as OrgGoal,
Goals as OrgGoals,
GoalStatus,
GoalCategory,
GoalPriority,
} from 'org.ai'
// Re-export org.ai goal types for convenience
export type { OrgGoal, OrgGoals, GoalStatus, GoalCategory, GoalPriority }
/**
* Convert a business-as-code GoalDefinition to an org.ai Goal
*
* @param definition - Business goal definition
* @param id - Unique identifier for the goal
* @returns org.ai Goal object
*/
export function toOrgGoal(definition: GoalDefinition, id: string): OrgGoal {
const result: OrgGoal = {
id,
name: definition.name,
description: definition.description || definition.name,
target: 100,
progress: definition.progress || 0,
status: definition.status || 'not-started',
}
if (definition.targetDate) {
result.targetDate = definition.targetDate
result.deadline = definition.targetDate
}
if (definition.owner) result.owner = definition.owner
if (definition.category) result.category = definition.category
if (definition.metrics) result.metrics = definition.metrics
if (definition.dependencies) result.dependencies = definition.dependencies
if (definition.metadata) result.metadata = definition.metadata
return result
}
/**
* Convert an org.ai Goal to a business-as-code GoalDefinition
*
* @param goal - org.ai Goal object
* @returns Business goal definition
*/
export function fromOrgGoal(goal: OrgGoal): GoalDefinition {
const result: GoalDefinition = {
name: goal.name,
progress: typeof goal.progress === 'number' ? goal.progress : 0,
}
if (goal.description) result.description = goal.description
const cat = goal.category
if (cat) result.category = cat as NonNullable<GoalDefinition['category']>
if (goal.targetDate) result.targetDate = goal.targetDate
else if (goal.deadline) result.targetDate = goal.deadline
if (goal.owner) result.owner = goal.owner
if (goal.metrics) result.metrics = goal.metrics
const st = goal.status
if (st) result.status = st as NonNullable<GoalDefinition['status']>
if (goal.dependencies) result.dependencies = goal.dependencies
if (goal.metadata) result.metadata = goal.metadata
return result
}
/**
* Define a business goal with metrics and tracking
*
* @example
* ```ts
* const goals = Goals([
* {
* name: 'Launch MVP',
* description: 'Ship minimum viable product to early customers',
* category: 'strategic',
* targetDate: new Date('2024-06-30'),
* owner: 'Product Team',
* metrics: ['User signups', 'Feature completion rate'],
* status: 'in-progress',
* progress: 65,
* },
* {
* name: 'Achieve Product-Market Fit',
* description: 'Validate product value with target customers',
* category: 'strategic',
* targetDate: new Date('2024-12-31'),
* owner: 'CEO',
* metrics: ['NPS > 50', 'Churn < 5%', '100+ paying customers'],
* status: 'in-progress',
* progress: 30,
* dependencies: ['Launch MVP'],
* },
* ])
* ```
*/
export function Goals(definitions: GoalDefinition[]): GoalDefinition[] {
return definitions.map((goal) => validateAndNormalizeGoal(goal))
}
/**
* Define a single goal
*/
export function Goal(definition: GoalDefinition): GoalDefinition {
return validateAndNormalizeGoal(definition)
}
/**
* Validate and normalize a goal definition
*/
function validateAndNormalizeGoal(goal: GoalDefinition): GoalDefinition {
if (!goal.name) {
throw new Error('Goal name is required')
}
return {
...goal,
category: goal.category || 'operational',
status: goal.status || 'not-started',
progress: goal.progress || 0,
metrics: goal.metrics || [],
dependencies: goal.dependencies || [],
metadata: goal.metadata || {},
}
}
/**
* Update goal progress
*/
export function updateProgress(goal: GoalDefinition, progress: number): GoalDefinition {
if (progress < 0 || progress > 100) {
throw new Error('Progress must be between 0 and 100')
}
// Auto-update status based on progress
let status: GoalDefinition['status']
if (progress === 0) {
status = 'not-started'
} else if (progress === 100) {
status = 'completed'
} else {
status = 'in-progress'
}
const result: GoalDefinition = {
...goal,
progress,
}
if (status !== undefined) result.status = status
return result
}
/**
* Mark goal as at-risk
*/
export function markAtRisk(goal: GoalDefinition): GoalDefinition {
return {
...goal,
status: 'at-risk',
}
}
/**
* Mark goal as completed
*/
export function complete(goal: GoalDefinition): GoalDefinition {
return {
...goal,
status: 'completed',
progress: 100,
}
}
/**
* Check if goal is overdue
*/
export function isOverdue(goal: GoalDefinition): boolean {
if (!goal.targetDate) return false
return new Date() > goal.targetDate && goal.status !== 'completed'
}
/**
* Get goals by category
*/
export function getGoalsByCategory(
goals: GoalDefinition[],
category: GoalDefinition['category']
): GoalDefinition[] {
return goals.filter((g) => g.category === category)
}
/**
* Get goals by status
*/
export function getGoalsByStatus(
goals: GoalDefinition[],
status: GoalDefinition['status']
): GoalDefinition[] {
return goals.filter((g) => g.status === status)
}
/**
* Get goals by owner
*/
export function getGoalsByOwner(goals: GoalDefinition[], owner: string): GoalDefinition[] {
return goals.filter((g) => g.owner === owner)
}
/**
* Calculate overall progress across all goals
*/
export function calculateOverallProgress(goals: GoalDefinition[]): number {
if (goals.length === 0) return 0
const totalProgress = goals.reduce((sum, goal) => sum + (goal.progress || 0), 0)
return totalProgress / goals.length
}
/**
* Check for circular dependencies
*/
export function hasCircularDependencies(goals: GoalDefinition[]): boolean {
const goalMap = new Map(goals.map((g) => [g.name, g]))
function checkCircular(goalName: string, visited = new Set<string>()): boolean {
if (visited.has(goalName)) return true
const goal = goalMap.get(goalName)
if (!goal || !goal.dependencies) return false
visited.add(goalName)
for (const dep of goal.dependencies) {
if (checkCircular(dep, new Set(visited))) {
return true
}
}
return false
}
return goals.some((goal) => checkCircular(goal.name))
}
/**
* Get goals in dependency order
*/
export function sortByDependencies(goals: GoalDefinition[]): GoalDefinition[] {
const goalMap = new Map(goals.map((g) => [g.name, g]))
const sorted: GoalDefinition[] = []
const visited = new Set<string>()
function visit(goalName: string) {
if (visited.has(goalName)) return
const goal = goalMap.get(goalName)
if (!goal) return
visited.add(goalName)
// Visit dependencies first
if (goal.dependencies) {
for (const dep of goal.dependencies) {
visit(dep)
}
}
sorted.push(goal)
}
for (const goal of goals) {
visit(goal.name)
}
return sorted
}
/**
* Validate goals
*/
export function validateGoals(goals: GoalDefinition[]): { valid: boolean; errors: string[] } {
const errors: string[] = []
for (const goal of goals) {
if (!goal.name) {
errors.push('Goal name is required')
}
if (goal.progress && (goal.progress < 0 || goal.progress > 100)) {
errors.push(`Goal ${goal.name} progress must be between 0 and 100`)
}
if (goal.dependencies) {
const goalNames = new Set(goals.map((g) => g.name))
for (const dep of goal.dependencies) {
if (!goalNames.has(dep)) {
errors.push(`Goal ${goal.name} depends on unknown goal: ${dep}`)
}
}
}
}
if (hasCircularDependencies(goals)) {
errors.push('Circular dependencies detected in goals')
}
return {
valid: errors.length === 0,
errors,
}
}