UNPKG

business-as-code

Version:

Primitives for expressing business logic and processes as code

907 lines (755 loc) 20.6 kB
/** * Organization Structure - Flows to FGA/RBAC * * Defines the complete organizational hierarchy: * * Organization * └── Department * └── Team * └── Position (Role + Worker) * └── Permissions (FGA/RBAC) * * This structure enables: * - Hierarchical permission inheritance * - Role-based task assignment * - Approval chains based on org structure * - Resource access control based on department/team * * @packageDocumentation */ import type { BusinessRole, TaskRoutingRule, WorkflowRole } from './roles.js' // ============================================================================= // Organization Hierarchy // ============================================================================= /** * Organization - top-level business entity * * @example * ```ts * const acme: Organization = { * id: 'org_acme', * name: 'Acme Corp', * domain: 'acme.com', * industry: 'technology', * * // Hierarchy * departments: [engineering, sales, support], * * // Global settings * settings: { * defaultCurrency: 'USD', * timezone: 'America/Los_Angeles', * workWeek: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], * }, * * // Resource hierarchy for FGA * resourceHierarchy: { * organization: { children: ['department', 'project', 'repository'] }, * department: { parent: 'organization', children: ['team'] }, * team: { parent: 'department', children: ['position'] }, * }, * } * ``` */ export interface Organization { /** Unique identifier */ id: string /** Organization name */ name: string /** Primary domain */ domain?: string /** Legal name (if different) */ legalName?: string /** Industry/sector */ industry?: string /** Mission statement */ mission?: string /** Core values */ values?: string[] /** Founded date */ foundedAt?: Date /** Headquarters location */ headquarters?: Address /** Organization settings */ settings?: OrganizationSettings /** Departments */ departments?: Department[] /** Standalone teams (not in departments) */ teams?: Team[] /** Organization-wide roles */ roles?: BusinessRole[] /** Resource hierarchy for FGA */ resourceHierarchy?: ResourceHierarchy /** Global approval chains */ approvalChains?: ApprovalChain[] /** Task routing rules */ routingRules?: TaskRoutingRule[] /** Metadata */ metadata?: Record<string, unknown> } /** * Organization settings */ export interface OrganizationSettings { /** Default currency */ defaultCurrency?: string /** Default timezone */ timezone?: string /** Work week days */ workWeek?: string[] /** Business hours */ businessHours?: { start: string end: string timezone?: string } /** Fiscal year start month (1-12) */ fiscalYearStart?: number /** Default language */ language?: string /** Date format */ dateFormat?: string } /** * Address */ export interface Address { street?: string city?: string state?: string postalCode?: string country?: string } // ============================================================================= // Department // ============================================================================= /** * Department - major organizational division * * @example * ```ts * const engineering: Department = { * id: 'dept_eng', * name: 'Engineering', * code: 'ENG', * * // Leadership * head: { roleId: 'role_cto', positionId: 'pos_cto_jane' }, * * // Teams * teams: [platformTeam, productTeam, infraTeam], * * // Budget * budget: { * annual: 5000000, * currency: 'USD', * categories: { * salaries: 3500000, * tools: 200000, * infrastructure: 800000, * training: 100000, * }, * }, * * // FGA: Department-level permissions * defaultPermissions: { * repository: ['read'], * project: ['read'], * }, * } * ``` */ export interface Department { /** Unique identifier */ id: string /** Department name */ name: string /** Short code (e.g., 'ENG', 'SALES') */ code?: string /** Description */ description?: string /** Department head position */ head?: PositionRef /** Parent department (for sub-departments) */ parentId?: string /** Teams within department */ teams?: Team[] /** Department budget */ budget?: Budget /** Cost center code */ costCenter?: string /** Department goals */ goals?: string[] /** Default permissions for department members */ defaultPermissions?: Record<string, string[]> /** Department-specific roles */ roles?: BusinessRole[] /** Department-specific routing rules */ routingRules?: TaskRoutingRule[] /** Metadata */ metadata?: Record<string, unknown> } // ============================================================================= // Team // ============================================================================= /** * Team - working group within a department * * @example * ```ts * const platformTeam: Team = { * id: 'team_platform', * name: 'Platform Team', * departmentId: 'dept_eng', * * // Leadership * lead: { roleId: 'role_lead', positionId: 'pos_lead_bob' }, * * // Members * positions: [ * { id: 'pos_1', roleId: 'role_engineer', workerId: 'worker_alice' }, * { id: 'pos_2', roleId: 'role_engineer', workerId: 'worker_charlie' }, * { id: 'pos_3', roleId: 'role_engineer', workerId: null }, // Open position * ], * * // Team resources (for FGA scoping) * resources: { * repositories: ['platform-core', 'platform-api'], * projects: ['platform-v2'], * }, * * // Team-level permissions (inherited by members) * defaultPermissions: { * repository: ['read', 'edit', 'act:merge'], * project: ['read', 'edit'], * }, * } * ``` */ export interface Team { /** Unique identifier */ id: string /** Team name */ name: string /** Parent department */ departmentId?: string /** Description */ description?: string /** Team lead position */ lead?: PositionRef /** Team positions */ positions?: Position[] /** Team objectives */ objectives?: string[] /** Team resources (for FGA scoping) */ resources?: TeamResources /** Team budget (if separate from department) */ budget?: Budget /** Default permissions for team members */ defaultPermissions?: Record<string, string[]> /** Workflow roles for this team */ workflowRoles?: WorkflowRole[] /** Communication channels */ channels?: TeamChannels /** Metadata */ metadata?: Record<string, unknown> } /** * Team resources - scopes FGA permissions */ export interface TeamResources { /** Owned repositories */ repositories?: string[] /** Owned projects */ projects?: string[] /** Owned products */ products?: string[] /** Custom resource types */ [resourceType: string]: string[] | undefined } /** * Team communication channels */ export interface TeamChannels { slack?: string teams?: string email?: string discord?: string } // ============================================================================= // Position - Role + Worker Assignment // ============================================================================= /** * Position - a role filled by a worker * * The position is the link between: * - Business Role (responsibilities, permissions) * - Worker (human or AI agent) * - Location in org hierarchy (team → department → org) * * @example * ```ts * const seniorEngineerPosition: Position = { * id: 'pos_se_123', * title: 'Senior Software Engineer', * * // Role defines what this position can do * roleId: 'role_senior_engineer', * * // Worker currently filling this position * workerId: 'worker_alice', * workerType: 'human', * * // Hierarchy * teamId: 'team_platform', * reportsTo: 'pos_lead_bob', * * // Position-specific permissions (added to role permissions) * additionalPermissions: { * 'repository:platform-core': ['manage'], * }, * * // Employment details * startDate: new Date('2023-01-15'), * status: 'active', * } * ``` */ export interface Position { /** Unique identifier */ id: string /** Position title */ title: string /** Business role this position requires */ roleId: string /** Worker filling this position (null = open position) */ workerId?: string | null /** Type of worker (human, agent, or either) */ workerType?: 'human' | 'agent' | 'any' /** Team this position belongs to */ teamId?: string /** Position this reports to */ reportsTo?: string /** Direct reports (position IDs) */ directReports?: string[] /** Position-specific additional permissions */ additionalPermissions?: Record<string, string[]> /** Start date */ startDate?: Date /** End date (for contractors/temporary) */ endDate?: Date /** Position status */ status?: 'active' | 'open' | 'on-leave' | 'terminated' /** Full-time equivalent (1.0 = full-time) */ fte?: number /** Location */ location?: string /** Remote/hybrid/onsite */ workModel?: 'remote' | 'hybrid' | 'onsite' /** Compensation */ compensation?: Compensation /** Metadata */ metadata?: Record<string, unknown> } /** * Position reference */ export interface PositionRef { positionId?: string roleId?: string workerId?: string } /** * Compensation details */ export interface Compensation { /** Base salary */ baseSalary?: number /** Currency */ currency?: string /** Pay frequency */ frequency?: 'hourly' | 'weekly' | 'biweekly' | 'monthly' | 'annual' /** Bonus target percentage */ bonusTarget?: number /** Equity grants */ equity?: { type: 'options' | 'rsu' | 'shares' amount: number vestingSchedule?: string } /** Band/level */ band?: string } // ============================================================================= // Budget // ============================================================================= /** * Budget allocation */ export interface Budget { /** Total annual budget */ annual?: number /** Currency */ currency?: string /** Budget period */ period?: string /** Budget categories */ categories?: Record<string, number> /** Spent to date */ spent?: number /** Remaining */ remaining?: number } // ============================================================================= // Resource Hierarchy - FGA Integration // ============================================================================= /** * Resource hierarchy definition for FGA * * Defines how resources relate to each other for permission inheritance. * * @example * ```ts * const hierarchy: ResourceHierarchy = { * // Organization is root * organization: { * children: ['department', 'project', 'repository'], * }, * * // Department inherits from org * department: { * parent: 'organization', * children: ['team'], * }, * * // Team inherits from department * team: { * parent: 'department', * children: ['position'], * }, * * // Project can be org-level or team-level * project: { * parent: 'organization', * alternateParents: ['team'], * children: ['document', 'task'], * }, * * // Repository can be org-level or team-level * repository: { * parent: 'organization', * alternateParents: ['team'], * }, * } * ``` */ export interface ResourceHierarchy { [resourceType: string]: ResourceHierarchyNode } /** * Node in resource hierarchy */ export interface ResourceHierarchyNode { /** Primary parent resource type */ parent?: string /** Alternative parent types */ alternateParents?: string[] /** Child resource types */ children?: string[] /** Whether permissions cascade down */ inheritPermissions?: boolean /** Maximum depth (for nested resources) */ maxDepth?: number } // ============================================================================= // Approval Chains // ============================================================================= /** * Approval chain - defines who approves what * * @example * ```ts * const expenseApprovalChain: ApprovalChain = { * id: 'chain_expense', * name: 'Expense Approval', * type: 'expense', * * levels: [ * { * threshold: 1000, * approvers: [{ type: 'direct-manager' }], * }, * { * threshold: 5000, * approvers: [ * { type: 'direct-manager' }, * { type: 'role', roleId: 'director' }, * ], * }, * { * threshold: 25000, * approvers: [ * { type: 'direct-manager' }, * { type: 'role', roleId: 'vp' }, * { type: 'role', roleId: 'cfo' }, * ], * }, * ], * * // Escalation settings * escalation: { * afterHours: 24, * escalateTo: 'skip-level-manager', * }, * } * ``` */ export interface ApprovalChain { /** Chain identifier */ id: string /** Chain name */ name: string /** What type of requests this chain handles */ type: string /** Approval levels */ levels: ApprovalLevel[] /** Escalation rules */ escalation?: EscalationRule /** Active/inactive */ active?: boolean /** Metadata */ metadata?: Record<string, unknown> } /** * Approval level within a chain */ export interface ApprovalLevel { /** Threshold amount (requests above this need this level) */ threshold?: number /** Approvers at this level */ approvers: ApproverSpec[] /** How many approvers needed */ requiredApprovals?: number /** Whether approvals at this level are sequential or parallel */ approvalMode?: 'sequential' | 'parallel' | 'any' /** SLA for this level (hours) */ slaHours?: number } /** * Approver specification */ export interface ApproverSpec { /** Approver type */ type: 'direct-manager' | 'skip-level-manager' | 'role' | 'position' | 'worker' | 'team' /** Role ID (if type is 'role') */ roleId?: string /** Position ID (if type is 'position') */ positionId?: string /** Worker ID (if type is 'worker') */ workerId?: string /** Team ID (if type is 'team') */ teamId?: string } /** * Escalation rule */ export interface EscalationRule { /** Hours before escalation */ afterHours: number /** Who to escalate to */ escalateTo: 'skip-level-manager' | 'department-head' | 'role' | 'position' /** Role ID if escalating to role */ roleId?: string /** Position ID if escalating to position */ positionId?: string /** Maximum escalations */ maxEscalations?: number } // ============================================================================= // Permission Resolution // ============================================================================= /** * Resolved permissions for a worker * * Combines: * - Organization-level defaults * - Department-level permissions * - Team-level permissions * - Role permissions * - Position-specific permissions */ export interface ResolvedPermissions { /** Worker ID */ workerId: string /** Position ID */ positionId: string /** Effective permissions by resource type */ permissions: Record<string, string[]> /** Resource-specific permissions */ resourcePermissions: Record<string, Record<string, string[]>> /** Approval capabilities */ canApprove: string[] /** Task handling capabilities */ canHandle: string[] /** Inheritance chain (for debugging) */ inheritanceChain: string[] } /** * Resolve permissions for a position in the org hierarchy */ export function resolvePermissions( org: Organization, positionId: string ): ResolvedPermissions | null { // Find the position let position: Position | undefined let team: Team | undefined let department: Department | undefined // Search through hierarchy for (const dept of org.departments || []) { for (const t of dept.teams || []) { const pos = t.positions?.find((p) => p.id === positionId) if (pos) { position = pos team = t department = dept break } } if (position) break } // Also check standalone teams if (!position) { for (const t of org.teams || []) { const pos = t.positions?.find((p) => p.id === positionId) if (pos) { position = pos team = t break } } } if (!position) return null // Find the role const role = org.roles?.find((r) => r['id'] === position.roleId) // Build inheritance chain const inheritanceChain: string[] = [] const permissions: Record<string, string[]> = {} const resourcePermissions: Record<string, Record<string, string[]>> = {} const canApprove: string[] = [] const canHandle: string[] = [] // 1. Department defaults if (department?.defaultPermissions) { inheritanceChain.push(`department:${department.id}`) mergePermissions(permissions, department.defaultPermissions) } // 2. Team defaults if (team?.defaultPermissions) { inheritanceChain.push(`team:${team.id}`) mergePermissions(permissions, team.defaultPermissions) } // 3. Team resources (scoped permissions) if (team?.resources) { for (const [resourceType, resourceIds] of Object.entries(team.resources)) { if (resourceIds) { for (const resourceId of resourceIds) { const key = `${resourceType}:${resourceId}` resourcePermissions[key] = resourcePermissions[key] || {} mergePermissions(resourcePermissions[key], team.defaultPermissions || {}) } } } } // 4. Role permissions if (role?.permissions) { inheritanceChain.push(`role:${role['id']}`) mergePermissions(permissions, role.permissions) } // 5. Role capabilities if (role?.['canApprove']) { canApprove.push(...role['canApprove']) } if (role?.['canHandle']) { canHandle.push(...role['canHandle']) } // 6. Position-specific permissions if (position.additionalPermissions) { inheritanceChain.push(`position:${position.id}`) mergePermissions(permissions, position.additionalPermissions) // Handle resource-specific permissions for (const [key, perms] of Object.entries(position.additionalPermissions)) { if (key.includes(':')) { resourcePermissions[key] = resourcePermissions[key] || {} resourcePermissions[key] = { ...resourcePermissions[key], _direct: perms } } } } return { workerId: position.workerId || '', positionId: position.id, permissions, resourcePermissions, canApprove: [...new Set(canApprove)], canHandle: [...new Set(canHandle)], inheritanceChain, } } /** * Merge permissions into target */ function mergePermissions( target: Record<string, string[]>, source: Record<string, string[]> ): void { for (const [key, perms] of Object.entries(source)) { if (!target[key]) { target[key] = [] } for (const perm of perms) { if (!target[key].includes(perm)) { target[key].push(perm) } } } } /** * Get approval chain for a request */ export function getApprovalChainForRequest( org: Organization, requestType: string, amount?: number ): ApproverSpec[] { const chain = org.approvalChains?.find((c) => c.type === requestType && c.active !== false) if (!chain) return [] // Find the appropriate level based on amount const levels = [...chain.levels].sort((a, b) => (a.threshold || 0) - (b.threshold || 0)) for (const level of levels.reverse()) { if (amount === undefined || (level.threshold && amount <= level.threshold)) { return level.approvers } } // Return highest level if amount exceeds all thresholds return levels[levels.length - 1]?.approvers || [] } /** * Find manager for a position (follows reportsTo chain) */ export function findManager(org: Organization, positionId: string): Position | null { // Find the position for (const dept of org.departments || []) { for (const team of dept.teams || []) { const position = team.positions?.find((p) => p.id === positionId) if (position?.reportsTo) { // Find the manager position for (const d of org.departments || []) { for (const t of d.teams || []) { const manager = t.positions?.find((p) => p.id === position.reportsTo) if (manager) return manager } } } } } return null }