Skip to content

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.



Tree manipulation engine for the lens hierarchy. All write operations use a KVS lease (hierarchy:write:${lensId}) to prevent concurrent modifications.

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;
}

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.

FunctionDescription
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

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.


Issue cache CRUD operations. The cache is the backend’s local copy of Jira issue data, stored in the issue_cache table.

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
}

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.

FunctionDescription
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.
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_TIMESTAMP

Permission checking and ACL management. Implements a layered permission model.

LevelRankCapabilities
view1Read lens data, list views
edit2Modify nodes, fields, views, sync agents
control3Manage permissions, delete lens, transfer ownership

Owner of a lens has implicit control access.

checkPermission(lensId, accountId, requiredLevel) resolves permissions in this order:

  1. Ownership check: If the user owns the lens, they have control
  2. Direct grants: grantee_type = 'user' with matching grantee_id
  3. Everyone grants: grantee_type = 'everyone' (applies to all users)
  4. Group grants: grantee_type = 'group' — resolved via Jira Groups API (/rest/api/3/user?expand=groups)
  5. Role grants: grantee_type = 'role' — resolved via Jira Application Roles API

The highest matching permission level is compared against the required level.

FunctionDescription
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' }

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 persistence and management.

FunctionDescription
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)

The settings_json column stores a JSON object with all non-column view settings:

  • density — comfortable, standard, compact
  • wrapRows — boolean
  • formattingRules — conditional formatting rules array
  • assigneeDisplayMode — name, initials, avatar
  • columnsFrozen — number of frozen columns
  • ganttEnabled, ganttWidth, ganttScale, ganttStartField, ganttEndField, ganttColorBy — Gantt configuration
  • barLabels, barIcon, ganttFormatting, ganttAssigneeDisplayMode — Gantt bar styling
  • criticalPathEnabled, resourcePanelEnabled, autoScheduleEnabled — Timeline features
  • sortConfig — sort configuration
  • pipeline — pipeline view settings

Sync agent (generator) persistence and execution management.

TypeDescription
jql_insertInsert issues matching a JQL query
child_extendExtend a parent’s children based on Jira parent-child relationships
hierarchy_builderBuild a full hierarchy from a project
jpd_linksImport from Jira Product Discovery links
auto_projectAutomatically add all issues from a project
FunctionDescription
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
  1. User triggers executeSyncAgents resolver
  2. Resolver pushes a message to the generator-queue (Forge async events)
  3. The sync-agent-executor consumer picks up the message (15-min timeout)
  4. Executor fetches all sync agents for the lens, runs them sequentially
  5. Each agent executes its JQL/logic, upserts issues to cache, adds/updates hierarchy nodes
  6. Execution status is persisted to the generators table

Jira REST API integration. Handles issue fetching, field parsing, and rate limit tracking.

FunctionDescription
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

The parser handles:

  • Standard fields mapped to dedicated issue_cache columns
  • customfield_10015 (Start Date) promoted to the start_date column
  • customfield_10016 (Story Points) extracted to story_points
  • Sprint data extracted from customfield_10020 array
  • All remaining custom fields dumped into the custom_fields JSON 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)

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 persistence layer.

FunctionDescription
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

Forge Realtime pub/sub integration.

FunctionDescription
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 TypeTriggerPayload
hierarchy_changedIssue created/updated/deleted, sync agent execution{ issueKey, action, actorId }
field_updatedInline field edit{ issueKey, field, newValue, actorId }
analysis_completeAI analysis finished{ insightCount }

KVS-backed rate limit tracking for Jira API calls. Located in foundation/src/utils/rate-limiter.ts.

  • 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
FunctionDescription
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
TierHourly LimitScope
Tier 165,000 pointsGlobal (all installations share the pool)
Tier 2100,000-500,000 pointsPer-tenant (requires Atlassian approval)

The effective limit is tierBudget - SAFETY_MARGIN_POINTS (10,000).


Forge licensing integration.

FunctionDescription
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).

Portfolio intelligence analysis engine.

FunctionDescription
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).


Automatic sync agent trigger system.

FunctionDescription
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)
  1. Issue events write timestamps to KVS: last-issue-event-ts:${projectKey}
  2. On lens view load, the timestamp is compared against generators.last_executed_at
  3. If the event timestamp is newer, the generator’s data may be stale
  4. An async sync is queued via the generator-queue

Team CRUD persistence. Teams can be global (accessible across all lenses) or lens-scoped.

Holiday calendar and date management. Supports region-specific calendars with named holidays.

Resource absence tracking (vacation, sick leave, etc.). Supports half-day absences.

Point-in-time snapshots of node start/end dates. Used for schedule variance analysis.

Skill taxonomy with categories and proficiency levels (1-5 scale).

Calculates workload distribution for resources within a lens. Considers story points, time estimates, and capacity.

Aggregates workload data across multiple lenses to detect over-allocation.

Implements a resource leveling algorithm that adjusts task schedules to resolve over-allocation. Runs as an async job via the leveling-queue.

Scores and ranks resources for task assignment based on skill match, availability, and current workload.

Computes available capacity for a resource over a date range, accounting for work schedules, holidays, and absences.

Auto-scheduling engine that computes start/end dates based on dependencies, resource availability, and constraints.