Business Logic Services
The foundation/src/services/ directory contains the business logic layer. Resolvers delegate to services for data access and domain operations. Services never directly receive { payload, context } — they accept typed parameters.
Table of Contents
Section titled “Table of Contents”- hierarchy-engine.ts
- cache-manager.ts
- permission-service.ts
- view-service.ts
- sync-agent-service.ts
- jira-sync.ts
- lens-service.ts
- realtime.ts
- rate-limiter.ts
- license-service.ts
- ai-service.ts
- auto-sync.ts
- Resource Management Services
hierarchy-engine.ts
Section titled “hierarchy-engine.ts”Tree manipulation engine for the lens hierarchy. All write operations use a KVS lease (hierarchy:write:${lensId}) to prevent concurrent modifications.
Core Types
Section titled “Core Types”interface HierarchyNode { id: string; lens_id: string; parent_id: string | null; position: number; node_type: 'issue' | 'flex' | 'generator_group' | 'milestone'; jira_issue_id: string | null; jira_issue_key: string | null; flex_name: string | null; flex_description: string | null; flex_icon: string | null; depth: number; hidden?: boolean; generator_id?: string; milestone_date?: string | null; scheduling_mode?: string; leveling_delay_days?: number; created_by: string | null;}Position Gap Strategy
Section titled “Position Gap Strategy”Nodes within a parent are positioned using a gap=100 strategy. New nodes get MAX(position) + 100. Moving a node between two siblings computes the midpoint. If gaps become too small (adjacent positions), a re-gap operation spreads all siblings evenly.
Key Functions
Section titled “Key Functions”| Function | Description |
|---|---|
getTreeForLens(lensId) | Fetch all nodes for a lens, ordered by depth ASC, position ASC |
addIssueNode(input) | Add a Jira issue node. Returns the new node. Respects idx_lens_issue UNIQUE constraint. |
addIssueNodesBatch(lensId, inputs, options?) | Bulk-add issue nodes with optimized batch insert |
addFlexItem(input) | Add a flex (grouping) item |
addMilestone(input) | Add a milestone with a target date |
addSyncAgentGroup(input) | Add a sync agent group node |
moveNode(lensId, nodeId, newParentId, position?, beforeNodeId?, afterNodeId?) | Move a node to a new parent/position. Updates depth for entire subtree. |
deleteNode(lensId, nodeId, deleteChildren?) | Delete a node. If deleteChildren=true, deletes entire subtree. Otherwise, re-parents children to the deleted node’s parent. |
setNodeHidden(nodeId, hidden) | Toggle the hidden flag (used by filter sync agents) |
clearAllHidden(lensId) | Clear all hidden flags for a lens (before sync re-execution) |
deleteSyncAgentGroups(lensId, generatorId) | Delete all group nodes created by a specific sync agent |
Concurrency Control
Section titled “Concurrency Control”All mutating operations are wrapped in withHierarchyWriteLease(lensId, fn) which acquires a KVS-backed distributed lock with a 5-second wait timeout. This prevents race conditions from concurrent edits or sync agent executions targeting the same lens.
cache-manager.ts
Section titled “cache-manager.ts”Issue cache CRUD operations. The cache is the backend’s local copy of Jira issue data, stored in the issue_cache table.
Core Type
Section titled “Core Type”interface CachedIssue { jira_issue_id: string; jira_issue_key: string; project_id: string; project_key: string; // ... 40+ fields matching issue_cache columns custom_fields: string | null; // JSON blob for non-standard fields links_json: string | null; // JSON array of issue links}Column Definition System
Section titled “Column Definition System”The ISSUE_CACHE_COLUMNS array is the single source of truth for all issue_cache SQL operations. Each entry defines the column name and an extract function. This eliminates the 4-way duplication that previously existed between INSERT, UPDATE, batch operations, and type definitions.
Key Functions
Section titled “Key Functions”| Function | Description |
|---|---|
upsertIssue(issue) | Insert or update a single issue. Uses INSERT ... ON DUPLICATE KEY UPDATE. |
batchUpsertIssues(issues) | Upsert multiple issues in a single transaction (used by sync agents) |
getIssueByKey(key) | Fetch a cached issue by Jira issue key |
getIssuesByKeys(keys) | Fetch multiple cached issues by keys |
getIssueById(id) | Fetch a cached issue by Jira issue ID |
deleteIssue(jiraIssueId) | Delete a single cache entry |
reconcileDeletedIssues(liveIds, projectKey, lensId?) | Delete cache entries for issues no longer in Jira. Compares liveIds against current cache, deletes the difference. Also removes orphaned hierarchy nodes. |
Upsert Pattern
Section titled “Upsert Pattern”INSERT INTO issue_cache (jira_issue_id, jira_issue_key, ...)VALUES (?, ?, ...)ON DUPLICATE KEY UPDATE jira_issue_key = VALUES(jira_issue_key), summary = VALUES(summary), ... cached_at = CURRENT_TIMESTAMPpermission-service.ts
Section titled “permission-service.ts”Permission checking and ACL management. Implements a layered permission model.
Permission Levels
Section titled “Permission Levels”| Level | Rank | Capabilities |
|---|---|---|
view | 1 | Read lens data, list views |
edit | 2 | Modify nodes, fields, views, sync agents |
control | 3 | Manage permissions, delete lens, transfer ownership |
Owner of a lens has implicit control access.
Permission Resolution
Section titled “Permission Resolution”checkPermission(lensId, accountId, requiredLevel) resolves permissions in this order:
- Ownership check: If the user owns the lens, they have
control - Direct grants:
grantee_type = 'user'with matchinggrantee_id - Everyone grants:
grantee_type = 'everyone'(applies to all users) - Group grants:
grantee_type = 'group'— resolved via Jira Groups API (/rest/api/3/user?expand=groups) - Role grants:
grantee_type = 'role'— resolved via Jira Application Roles API
The highest matching permission level is compared against the required level.
Key Functions
Section titled “Key Functions”| Function | Description |
|---|---|
checkPermission(lensId, accountId, level) | Returns true if user has at least the specified level |
getMaxPermissionLevel(lensId, accountId) | Returns the user’s highest permission level, or null if no access |
getPermissions(lensId) | List all grants for a lens |
grantPermission(input) | Create or upsert a permission grant (uses INSERT … ON DUPLICATE KEY UPDATE) |
revokePermission(permissionId, lensId) | Delete a permission grant |
requireAdmin(accountId) | Check if user is a Jira site admin (via /rest/api/3/myself?expand=applicationRoles) |
denyAccess() | Returns { error: 'Permission denied' } |
Issue-Level Permissions
Section titled “Issue-Level Permissions”Beyond lens-level ACLs, individual issues are filtered by Jira BROWSE permission. The getVisibleIssueKeys() utility (in utils/visible-issues.ts) batch-checks BROWSE permissions using a JQL query scoped to the issue keys. Results are cached in KVS per project+user with a 30-minute TTL.
view-service.ts
Section titled “view-service.ts”View persistence and management.
Key Functions
Section titled “Key Functions”| Function | Description |
|---|---|
listViews(lensId) | List all views for a lens |
getView(viewId) | Get a single view by ID |
createView(input) | Create a new view |
createDefaultView(lensId, ownerAccountId) | Create the initial default view with standard columns |
updateView(viewId, updates) | Update view configuration (columns, settings, Gantt, formatting) |
deleteView(viewId) | Delete a view |
setDefaultView(lensId, viewId) | Set the default view (clears is_default on all other views for the lens) |
serializeView(view) | Serialize a view for API response (parse JSON columns) |
View Settings
Section titled “View Settings”The settings_json column stores a JSON object with all non-column view settings:
density— comfortable, standard, compactwrapRows— booleanformattingRules— conditional formatting rules arrayassigneeDisplayMode— name, initials, avatarcolumnsFrozen— number of frozen columnsganttEnabled,ganttWidth,ganttScale,ganttStartField,ganttEndField,ganttColorBy— Gantt configurationbarLabels,barIcon,ganttFormatting,ganttAssigneeDisplayMode— Gantt bar stylingcriticalPathEnabled,resourcePanelEnabled,autoScheduleEnabled— Timeline featuressortConfig— sort configurationpipeline— pipeline view settings
sync-agent-service.ts
Section titled “sync-agent-service.ts”Sync agent (generator) persistence and execution management.
Generator Types
Section titled “Generator Types”| Type | Description |
|---|---|
jql_insert | Insert issues matching a JQL query |
child_extend | Extend a parent’s children based on Jira parent-child relationships |
hierarchy_builder | Build a full hierarchy from a project |
jpd_links | Import from Jira Product Discovery links |
auto_project | Automatically add all issues from a project |
Key Functions
Section titled “Key Functions”| Function | Description |
|---|---|
getSyncAgents(lensId) | List all sync agents for a lens, ordered by position |
createSyncAgent(input) | Create a new sync agent |
updateSyncAgent(lensId, generatorId, updates) | Update sync agent config |
deleteSyncAgent(lensId, generatorId) | Delete a sync agent and its group nodes |
reorderSyncAgents(lensId, orderedIds) | Reorder sync agents by updating positions |
updateExecutionStatus(generatorId, status, message?, total?) | Update execution status after sync |
Execution Flow
Section titled “Execution Flow”- User triggers
executeSyncAgentsresolver - Resolver pushes a message to the
generator-queue(Forge async events) - The
sync-agent-executorconsumer picks up the message (15-min timeout) - Executor fetches all sync agents for the lens, runs them sequentially
- Each agent executes its JQL/logic, upserts issues to cache, adds/updates hierarchy nodes
- Execution status is persisted to the
generatorstable
jira-sync.ts
Section titled “jira-sync.ts”Jira REST API integration. Handles issue fetching, field parsing, and rate limit tracking.
Key Functions
Section titled “Key Functions”| Function | Description |
|---|---|
fetchIssueByKey(key) | Fetch a single issue by key. Returns CachedIssue or null. Costs 2 API points. |
fetchIssueById(id) | Fetch a single issue by ID. Costs 2 API points. |
fetchIssuesByJql(jql, maxItems?, options?) | Fetch issues matching a JQL query. Paginates automatically. Costs 1 + N/100 points per page. Hard max: 2,000 items. |
fetchIssueLinks(issueKey) | Fetch issue links for a single issue. Costs 2 points. |
parseJiraIssue(raw) | Transform a raw Jira API response into a CachedIssue |
Field Parsing (parseJiraIssue)
Section titled “Field Parsing (parseJiraIssue)”The parser handles:
- Standard fields mapped to dedicated
issue_cachecolumns customfield_10015(Start Date) promoted to thestart_datecolumncustomfield_10016(Story Points) extracted tostory_points- Sprint data extracted from
customfield_10020array - All remaining custom fields dumped into the
custom_fieldsJSON blob - Description ADF (Atlassian Document Format) converted to plain text (max 500 chars)
- Issue links serialized to
links_json - Time tracking fields (original/remaining/spent estimates)
Rate Limit Integration
Section titled “Rate Limit Integration”Every Jira API call goes through the rate limiter:
if (!(await canAfford(pointCost))) { // Budget exhausted -- abort or return partial results}await trackPoints(pointCost);Failed calls refund their points via refundPoints().
lens-service.ts
Section titled “lens-service.ts”Lens persistence layer.
| Function | Description |
|---|---|
listLenses(accountId) | List lenses the user can access (owner + grants) |
getLens(lensId) | Get a single lens |
getLensByNumericId(numericId) | Look up by friendly URL numeric ID |
createLens(input) | Create a new lens with auto-assigned numeric_id |
updateLens(lensId, updates) | Update name/description |
deleteLens(lensId) | Delete a lens (sets mode to ‘deleting’, cascades to child tables) |
transferOwnership(lensId, newOwnerAccountId) | Transfer lens ownership |
realtime.ts
Section titled “realtime.ts”Forge Realtime pub/sub integration.
| Function | Description |
|---|---|
buildChannel(lensId) | Build the Realtime channel name: foundation:lens:${lensId} |
publishEvent(event) | Publish an event to the lens channel. Fire-and-forget (errors logged, not thrown). |
Event Types
Section titled “Event Types”| Event Type | Trigger | Payload |
|---|---|---|
hierarchy_changed | Issue created/updated/deleted, sync agent execution | { issueKey, action, actorId } |
field_updated | Inline field edit | { issueKey, field, newValue, actorId } |
analysis_complete | AI analysis finished | { insightCount } |
rate-limiter.ts
Section titled “rate-limiter.ts”KVS-backed rate limit tracking for Jira API calls. Located in foundation/src/utils/rate-limiter.ts.
Architecture
Section titled “Architecture”- Sharded counters: 8 KVS shards per hour slot, chosen randomly on each write. Reads sum all shards. This reduces contention on high-traffic installations.
- Hourly windows: Counter keys include a UTC hour identifier (e.g.,
rate_limit:hourly:2026-2-25-14) - KVS leases: Each shard write acquires a short-lived lease to prevent lost-update races
- Safety margin: 10,000 points deducted from the effective limit to account for lease timeouts and concurrent cold-starts
Key Functions
Section titled “Key Functions”| Function | Description |
|---|---|
trackPoints(points) | Record point consumption |
canAfford(points) | Check if budget allows the specified points |
refundPoints(points) | Refund points on failed API calls |
getBudgetStatus() | Get remaining budget, used, limit, reset time |
getRemainingBudget() | Get remaining points |
getUsagePercent() | Get usage as a percentage |
getTierBudget() | Get the current tier budget (65K for Tier 1, admin-configurable for Tier 2) |
setTierBudget(budget) | Set the tier budget override |
getHourlyUsageHistory(hours) | Get usage history for the last N hours |
createPointAccumulator() | Create a local accumulator for batching point tracking |
Tier Configuration
Section titled “Tier Configuration”| Tier | Hourly Limit | Scope |
|---|---|---|
| Tier 1 | 65,000 points | Global (all installations share the pool) |
| Tier 2 | 100,000-500,000 points | Per-tenant (requires Atlassian approval) |
The effective limit is tierBudget - SAFETY_MARGIN_POINTS (10,000).
license-service.ts
Section titled “license-service.ts”Forge licensing integration.
| Function | Description |
|---|---|
checkLicenseActive(context) | Returns null if license is active, or { error: 'license_inactive', message } if expired/missing. Passes silently if licensing field not present (dev environments). |
ai-service.ts
Section titled “ai-service.ts”Portfolio intelligence analysis engine.
| Function | Description |
|---|---|
analyzeLens(lensId) | Run heuristic analysis to detect issues: overdue items, unassigned work, bottlenecks, empty epics, etc. Returns an array of AiInsight objects. |
analyzeSectionGapsInLens(lensId, anchor, maxItems?) | Analyze a specific section for missing items compared to similar sections. |
Insights are rule-based heuristics, not LLM-based. They examine issue metadata patterns (status distribution, assignment gaps, date health).
auto-sync.ts
Section titled “auto-sync.ts”Automatic sync agent trigger system.
| Function | Description |
|---|---|
checkAndTriggerStaleness(lensId) | Check if any sync agents are stale based on per-project event timestamps. If stale, queue async re-execution. Called non-blocking from getLensView. |
triggerAutoSync(lensId, projectKeys) | Queue sync agent execution for stale projects |
suppressAutoSyncForLens(lensId) | Temporarily suppress auto-sync (e.g., during manual edits) |
Staleness Detection
Section titled “Staleness Detection”- Issue events write timestamps to KVS:
last-issue-event-ts:${projectKey} - On lens view load, the timestamp is compared against
generators.last_executed_at - If the event timestamp is newer, the generator’s data may be stale
- An async sync is queued via the
generator-queue
Resource Management Services
Section titled “Resource Management Services”team-service.ts
Section titled “team-service.ts”Team CRUD persistence. Teams can be global (accessible across all lenses) or lens-scoped.
holiday-service.ts
Section titled “holiday-service.ts”Holiday calendar and date management. Supports region-specific calendars with named holidays.
absence-service.ts
Section titled “absence-service.ts”Resource absence tracking (vacation, sick leave, etc.). Supports half-day absences.
baseline-service.ts
Section titled “baseline-service.ts”Point-in-time snapshots of node start/end dates. Used for schedule variance analysis.
skills-service.ts
Section titled “skills-service.ts”Skill taxonomy with categories and proficiency levels (1-5 scale).
resource-workload.ts
Section titled “resource-workload.ts”Calculates workload distribution for resources within a lens. Considers story points, time estimates, and capacity.
cross-lens-workload.ts
Section titled “cross-lens-workload.ts”Aggregates workload data across multiple lenses to detect over-allocation.
resource-leveling.ts
Section titled “resource-leveling.ts”Implements a resource leveling algorithm that adjusts task schedules to resolve over-allocation. Runs as an async job via the leveling-queue.
resource-matcher.ts
Section titled “resource-matcher.ts”Scores and ranks resources for task assignment based on skill match, availability, and current workload.
capacity-calculator.ts
Section titled “capacity-calculator.ts”Computes available capacity for a resource over a date range, accounting for work schedules, holidays, and absences.
timeline-scheduler.ts
Section titled “timeline-scheduler.ts”Auto-scheduling engine that computes start/end dates based on dependencies, resource availability, and constraints.
Cross-References
Section titled “Cross-References”- API Reference — resolver endpoint documentation
- Database Schema — table definitions and migration system
- Events & Async Actions — event handlers that use these services