Transformation Engine
Transformation Engine: Unified Mapping & Normalisation Service
Section titled “Transformation Engine: Unified Mapping & Normalisation Service”Problem Statement
Section titled “Problem Statement”GuideMode normalises data from four integration providers (GitHub, Jira, Linear, Notion) into a unified internal model. This normalisation involves four types of mappings:
- Labels (external labels to internal issue types)
- Statuses (external workflow states to internal sub-statuses)
- Environments (deployment environment names to production/staging/etc.)
- Link types (external relationship types to internal link types)
While the core inference logic is already shared (mapLabelsToTypeAndBlocked, mapProviderStatusToSubStatus), the plumbing around it is duplicated across providers: loading mappings from the database, filtering by scope, applying fallbacks, and reprocessing when mappings change. AI-powered suggestions exist only for labels. The UX is organised per-provider rather than per-tenant, making it harder for teams that use multiple providers.
- Eliminate duplication by extracting a single
transformation-engineservice that all providers and webhooks use - Extend AI suggestions to all mapping types (statuses, environments, link types)
- Shift the UX from per-provider to per-tenant with optional provider/repository overrides
- Enable parallel execution – each phase is independently deployable
Current Architecture (What Exists)
Section titled “Current Architecture (What Exists)”Already shared (good)
Section titled “Already shared (good)”| Module | Path | Used by |
|---|---|---|
| Label inference | utils/label-type-inference.ts |
All 4 providers |
| Status inference | utils/status-sub-status-inference.ts |
All 4 providers |
| Link detection | services/link-detection/ |
Generic service |
| Mapping preloader | workflows/utils/mapping-loader.ts |
Workflow steps only |
Duplicated (to be consolidated)
Section titled “Duplicated (to be consolidated)”| What | Where | Count |
|---|---|---|
| DB mapping queries | GitHub issues, Linear issues, Linear webhook, Notion transform, label-reprocessing, status-reprocessing, mapping-loader | 7 |
Environment matching (isProductionEnvironment, matchesPattern) |
services/github/deployments.ts + webhooks/handlers/github/deployments.ts |
2 (identical copies) |
| Label-to-type wrappers | GitHub issues, GitHub graphql/issues, Linear issues | 3 |
| Reprocessing engines | services/label-reprocessing.ts + services/status-reprocessing.ts |
2 (structurally identical) |
Notion’s unique approach
Section titled “Notion’s unique approach”Notion uses notion_database_mappings with a JSONB mappingConfig for column-level property mapping. This is architecturally different – it maps Notion database properties to issue fields, not values to values. However, Notion already falls through to the global label_mappings and status_mappings tables for value-level normalisation. The transformation engine handles this via bridge, not replacement.
Target Architecture
Section titled “Target Architecture”services/transformation-engine/ index.ts -- Barrel exports types.ts -- Shared interfaces preloader.ts -- Unified mapping loader from DB label-mapper.ts -- Thin facade over utils/label-type-inference.ts status-mapper.ts -- Thin facade over utils/status-sub-status-inference.ts environment-mapper.ts -- Extracted from github/deployments.ts link-type-mapper.ts -- Extracted from jira/issues/transform.ts reprocessor.ts -- Unified reprocessing (replaces 2 services) ai-suggestions.ts -- Generalised AI suggestions (Phase 2)Design principles
Section titled “Design principles”-
Thin facades, not rewrites – The existing
utils/label-type-inference.tsandutils/status-sub-status-inference.tsremain untouched. The engine wraps them with consistent scope filtering. -
Notion bridged, not forced – Notion’s embedded
valueMappingsare checked first as a Notion-specific optimisation. If not found, fall through to global mapping tables via the engine. -
Preload for batch, fresh for webhooks – Sync workflows call
preloadAllMappings()once. Webhook handlers call individual loaders per event. -
Provider callbacks for fallback – Jira falls back to issue type inference, GitHub uses
stateReason. The facades accept optionalproviderFallbackcallbacks rather than embedding provider logic. -
Tenant-first scoping – The new model treats mappings as tenant-level defaults with optional provider, repository, and project overrides.
Phase 1: Extract and Unify
Section titled “Phase 1: Extract and Unify”Goal: Consolidate all duplicated mapping logic into services/transformation-engine/ without changing any behaviour.
Can run in parallel with: Nothing (foundation phase) Prerequisite for: Phase 2, Phase 3, Phase 4
Step 1.1: Types and barrel
Section titled “Step 1.1: Types and barrel”Create the type definitions that all other modules in the engine will use.
Create: services/transformation-engine/types.ts
Provider = 'github' | 'jira' | 'linear' | 'notion'
MappingScope { repositoryId?: string projectId?: string // GitHub Projects v2}
LabelMapping { externalLabel: string internalType: IssueType | null mapsToBlocked: boolean repositoryId: string | null}
StatusMapping { externalStatus: string internalSubStatus: SubStatus projectId: string | null repositoryId: string | null}
EnvironmentMapping { environmentPattern: string isProduction: boolean repositoryId: string | null}
LinkTypeMapping { externalLinkType: string internalLinkType: InternalLinkType requiresSourceType: IssueType | null requiresTargetType: IssueType | null repositoryId: string | null}
PreloadedMappings { labels: LabelMapping[] statuses: StatusMapping[] environments: EnvironmentMapping[] linkTypes: LinkTypeMapping[]}
ReprocessResult<TChange> { issuesProcessed: number issuesUpdated: number changes: TChange[] factTablesRefreshed: boolean}Create: services/transformation-engine/index.ts – barrel exports
Step 1.2: Unified preloader
Section titled “Step 1.2: Unified preloader”Consolidate all 7 duplicate mapping-loading patterns into a single module.
Create: services/transformation-engine/preloader.ts
Functions to implement:
preloadLabelMappings(db, tenantId, provider)– querylabel_mappingstable filtered by tenant+providerpreloadStatusMappings(db, tenantId, provider)– querystatus_mappingstable filtered by tenant+providerpreloadEnvironmentMappings(db, tenantId, provider)– queryenvironment_mappingstable filtered by tenant+provider (new – currently only queried inline ingithub/deployments.ts)preloadLinkTypeMappings(db, tenantId, provider)– querylink_type_mappingstable filtered by tenant+provider (new – currently only queried inline in Jira transform)preloadAllMappings(db, tenantId, provider)– loads all 4 types in parallel viaPromise.allgetLabelMappingsForScope(mappings, scope)– filters preloaded labels by repository (repo-specific + global)getStatusMappingsForScope(mappings, scope)– filters preloaded statuses by project/repository (most-specific-wins priority)getEnvironmentMappingsForScope(mappings, scope)– filters preloaded environments by repositorygetLinkTypeMappingsForScope(mappings, scope)– filters preloaded link types by repository
The scope-filtering functions preserve the existing priority hierarchy:
- Project + Repository specific (most specific)
- Project specific
- Repository specific
- Global (least specific)
Modify: workflows/utils/mapping-loader.ts – make it a thin re-export shim:
export { preloadLabelMappings, preloadStatusMappings, preloadAllMappings, ... } from '../../services/transformation-engine/preloader.js'This ensures all existing workflow code continues working without import changes.
Source code to consolidate from:
workflows/utils/mapping-loader.tslines 60-230 (primary source)services/label-reprocessing.tslines 42-70 (loadMappingsWithDefault)services/status-reprocessing.tslines 46-67 (loadStatusMappings)services/github/deployments.tslines 63-78 (inline env mapping query)services/linear/issues.tslines 45-73 (getLinearLabelMappings)webhooks/handlers/linear/issue.tslines 30-97 (duplicate of Linear sync loaders)services/notion/issues/transform.tslines 186-204 (inline label mapping query)
Step 1.3: Environment mapper
Section titled “Step 1.3: Environment mapper”Extract the duplicated environment matching logic into a standalone module.
Create: services/transformation-engine/environment-mapper.ts
Functions to implement:
matchesPattern(value, pattern)– wildcard matching (*= any,?= single char). Currently atservices/github/deployments.ts:106-109isDefaultProductionEnvironment(environmentName)– hardcoded fallback heuristic (prod, production, prod-*). Currently atservices/github/deployments.ts:101-104isProductionEnvironment(environmentName, mappings, scope)– pure function (no DB access). Takes pre-loaded environment mappings, filters by scope, applies pattern matching, falls back to default heuristic. Currently atservices/github/deployments.ts:63-99but does its own DB query – the new version is pure.mapEnvironmentCategory(environmentName)– maps to production/staging/development/qa/preview/other. Currently atservices/github/deployments.ts:111-133
Modify: services/github/deployments.ts
- Remove the 4 local functions (
isProductionEnvironment,isDefaultProductionEnvironment,matchesPattern,mapEnvironment) - Import from transformation engine
- Update
upsertGitHubDeploymentto preload environment mappings via the engine preloader, then call the pureisProductionEnvironment
Modify: webhooks/handlers/github/deployments.ts
- Remove the identical 4 local functions
- Import from transformation engine
Step 1.4: Label and status mapper facades
Section titled “Step 1.4: Label and status mapper facades”Create thin facade functions that handle scope filtering before delegating to the existing shared utilities.
Create: services/transformation-engine/label-mapper.ts
mapLabels( labels: string[], mappings: PreloadedMappings, scope: MappingScope, provider: Provider, options?: { defaultType?: IssueType providerTypeInference?: (labels: string[]) => IssueType | null }): LabelMappingResult
// Internally:// 1. Filter mappings.labels by scope (repo-specific + global)// 2. If providerTypeInference provided (Jira), try it as additional fallback// 3. Delegate to mapLabelsToTypeAndBlocked() from utils/label-type-inference.tsCreate: services/transformation-engine/status-mapper.ts
mapStatus( provider: Provider, nativeStatus: string, state: IssueState, mappings: PreloadedMappings, scope: MappingScope): SubStatusResult
// Internally:// 1. Filter mappings.statuses by scope (project+repo priority)// 2. Delegate to mapProviderStatusToSubStatus() from utils/status-sub-status-inference.tsThese facades eliminate the 3+ duplicate wrapper functions in GitHub issues, GitHub graphql/issues, and Linear issues.
Modify: services/github/issues.ts – remove mapLabelsToTypeAndBlockedSync(), import from engine
Modify: services/github/graphql/issues.ts – remove mapLabelsToTypeAndBlockedSync(), import from engine
Modify: services/linear/issues.ts – remove mapLinearLabelsToTypeAndBlocked(), import from engine
Step 1.5: Link type mapper
Section titled “Step 1.5: Link type mapper”Extract the Jira link type mapping into a provider-agnostic module.
Create: services/transformation-engine/link-type-mapper.ts
mapLinkType( externalLinkType: string, direction: 'outward' | 'inward', sourceType: IssueType, targetType: IssueType, mappings: PreloadedMappings, scope: MappingScope): InternalLinkType | null
// Internally:// 1. Filter mappings.linkTypes by scope// 2. Check custom mappings (with conditional type requirements)// 3. Check keyword-based inference (validates, blocks, duplicates, etc.)// 4. Issue type inference (discovery + feature = validates)// 5. Standard defaultsThis is a direct extraction of mapJiraLinkTypeToInternal() from services/jira/issues/transform.ts:294-345, generalised to work with any provider.
Modify: services/jira/issues/transform.ts – remove mapJiraLinkTypeToInternal(), import from engine
Step 1.6: Unified reprocessor
Section titled “Step 1.6: Unified reprocessor”Merge the two structurally identical reprocessing services into a single generic engine.
Create: services/transformation-engine/reprocessor.ts
The two existing reprocessors follow the same pattern:
- Load mappings from DB
- Load all issues for tenant+provider
- For each issue, compute new values using current mappings
- Diff against existing values
- Batch update changed issues (500 per batch)
- Refresh fact tables
The unified version:
reprocessLabels(db, tenantId, provider, options?: { dryRun?, limit? }): ReprocessResult<LabelChange>reprocessStatuses(db, tenantId, provider, options?: { dryRun?, limit? }): ReprocessResult<StatusChange>Both functions:
- Use the engine preloader to load mappings
- Query issues with the relevant data (labels or native statuses)
- Apply the engine’s mapper facades to compute new values
- Diff and batch update
- Refresh fact tables via
refreshFactTablesForTenant()
The status reprocessor currently only handles GitHub (line 171: if (provider === 'github')). The unified version extends this to all providers by using mapProviderStatusToSubStatus() which already supports all 4 providers.
Modify: routes/settings/work-tracking.ts – update reprocessing endpoints to call unified reprocessor
Delete: services/label-reprocessing.ts
Delete: services/status-reprocessing.ts
Step 1.7: Migrate providers
Section titled “Step 1.7: Migrate providers”Update each provider to import from the transformation engine instead of maintaining local copies.
GitHub (services/github/issues.ts, services/github/graphql/issues.ts):
- Remove local
preloadLabelMappings(),preloadStatusMappings()calls - Remove local
mapLabelsToTypeAndBlockedSync()wrapper - Import
preloadAllMappings,mapLabels,mapStatusfrom engine - Pass preloaded mappings through to upsert functions
Jira (services/jira/issues/sync.ts, services/jira/issues/transform.ts):
- Remove inline status mapping query
- Remove
mapJiraLinkTypeToInternal() - Import from engine
- Pass preloaded mappings and Jira-specific
providerTypeInferencecallback (forinferTypeFromJiraIssueType)
Linear (services/linear/issues.ts, webhooks/handlers/linear/issue.ts):
- Remove
getLinearLabelMappings(),getLinearStatusMappings() - Remove
mapLinearLabelsToTypeAndBlocked() - Import from engine
Notion (services/notion/issues/transform.ts):
- Remove inline label mapping DB query (lines 186-204)
- Import
preloadLabelMappingsfrom engine - Keep Notion’s embedded
valueMappingscheck as-is (bridge pattern)
Workflows (all 4 workflow files):
- No changes needed – they import from
workflows/utils/mapping-loader.tswhich becomes a re-export shim
Step 1.8: Tests
Section titled “Step 1.8: Tests”- Unit tests for
preloader.tsscope-filtering functions (mock DB) - Unit tests for
environment-mapper.ts(wildcard matching, priority, fallback) - Unit tests for
link-type-mapper.ts(custom mappings, conditional types, defaults) - Verify existing tests still pass:
npm run test - Typecheck:
npm run typecheck - Lint:
npm run lint - Build:
npm run build
Files summary (Phase 1)
Section titled “Files summary (Phase 1)”| File | Action |
|---|---|
services/transformation-engine/types.ts |
Create |
services/transformation-engine/index.ts |
Create |
services/transformation-engine/preloader.ts |
Create |
services/transformation-engine/environment-mapper.ts |
Create |
services/transformation-engine/label-mapper.ts |
Create |
services/transformation-engine/status-mapper.ts |
Create |
services/transformation-engine/link-type-mapper.ts |
Create |
services/transformation-engine/reprocessor.ts |
Create |
workflows/utils/mapping-loader.ts |
Simplify to re-export shim |
services/github/deployments.ts |
Remove env functions, import from engine |
services/github/issues.ts |
Remove preload/wrapper, import from engine |
services/github/graphql/issues.ts |
Remove label wrapper, import from engine |
services/linear/issues.ts |
Remove preload/wrapper, import from engine |
services/jira/issues/transform.ts |
Remove link mapper, import from engine |
services/jira/issues/sync.ts |
Use engine preloader |
services/notion/issues/transform.ts |
Use engine preloader for global mappings |
webhooks/handlers/github/deployments.ts |
Import env matching from engine |
webhooks/handlers/linear/issue.ts |
Remove preload functions, import from engine |
routes/settings/work-tracking.ts |
Use unified reprocessor |
services/label-reprocessing.ts |
Delete |
services/status-reprocessing.ts |
Delete |
Phase 2: Extend AI Suggestions
Section titled “Phase 2: Extend AI Suggestions”Goal: Generalise the AI-powered suggestion system from labels-only to all mapping types.
Can run in parallel with: Phase 3 (UX shift) Prerequisite: Phase 1 (needs the unified preloader and types)
Step 2.1: Generalise AI client
Section titled “Step 2.1: Generalise AI client”Create: services/transformation-engine/ai-suggestions.ts
Refactor from services/label-mapping-ai.ts into a generalised suggestion engine.
Design: A single suggestMappings() function that accepts a mapping type and a list of external values:
MappingType = 'labels' | 'statuses' | 'environments' | 'link_types'
suggestMappings( type: MappingType, externalValues: string[], config: AIConfig, context?: { provider?: Provider existingMappings?: PreloadedMappings // So AI knows what's already mapped }): Promise<SuggestionResult>
SuggestionResult { suggestions: MappingSuggestion[] usage: { promptTokens, completionTokens, totalTokens }}
MappingSuggestion { externalValue: string suggestedMapping: string // The internal type/subStatus/etc. suggestsBlocked?: boolean // For labels only confidence: 'high' | 'medium' | 'low' reasoning: string}Step 2.2: Type-specific system prompts
Section titled “Step 2.2: Type-specific system prompts”Each mapping type needs its own section in the system prompt, describing:
- Available internal values and their meanings
- Default heuristic patterns (so the AI focuses on non-obvious mappings)
- Examples of good mappings
Label suggestions (exists today in label-mapping-ai.ts):
- 7 issue types with descriptions and examples
- Blocked status detection
- Default heuristic patterns to skip
Status suggestions (new):
- 9 sub-statuses with descriptions: backlog, ready, discovery, delivery, review, blocked, parked, done, canceled
- Per-provider heuristic patterns (from
status-sub-status-inference.ts) so the AI knows what’s already handled - Context: the AI sees the provider name to understand the status vocabulary
Environment suggestions (new):
- Binary classification: production vs non-production
- Plus environment category: production, staging, development, qa, preview, other
- Default patterns to skip: prod, production, staging, dev, etc.
- Focus on non-obvious patterns:
release-*,canary, custom naming conventions
Link type suggestions (new):
- 8 internal link types with descriptions
- Direction awareness (outward vs inward)
- Conditional type requirements explanation
Step 2.3: API endpoints
Section titled “Step 2.3: API endpoints”Modify: routes/settings/work-tracking.ts
Existing endpoint:
POST /label-mappings/suggestions– keep but delegate to generalised engine
New endpoints:
POST /status-mappings/suggestions– AI suggestions for unmapped statusesPOST /environment-mappings/suggestions– AI suggestions for environment patternsPOST /link-mappings/suggestions– AI suggestions for link types
Each endpoint:
- Loads existing mappings via the engine preloader
- Filters to unmapped values
- Calls
suggestMappings()with the appropriate type - Returns suggestions with confidence and reasoning
Step 2.4: Bulk apply from suggestions
Section titled “Step 2.4: Bulk apply from suggestions”Add an endpoint to apply AI suggestions in bulk:
POST /mappings/apply-suggestions– accepts an array of suggestions, creates mappings
This enables a “review and apply” workflow: AI suggests, user reviews, user applies selected suggestions.
Files summary (Phase 2)
Section titled “Files summary (Phase 2)”| File | Action |
|---|---|
services/transformation-engine/ai-suggestions.ts |
Create |
routes/settings/work-tracking.ts |
Add 3 new suggestion endpoints |
services/label-mapping-ai.ts |
Delete (logic moved to ai-suggestions.ts) |
Phase 3: Tenant-First UX — New “Mappings” Settings Section
Section titled “Phase 3: Tenant-First UX — New “Mappings” Settings Section”Goal: Create a new top-level “Mappings” settings section with one dedicated page per mapping type. Shift the mental model from “configure per provider” to “configure for your team, with optional provider/repository overrides”. This is a critical user-facing feature — usability is paramount.
Can run in parallel with: Phase 2 (AI suggestions) Prerequisite: Phase 1 (needs the unified preloader and types)
Why this matters
Section titled “Why this matters”Today, mapping configuration is buried inside each provider’s settings page as tabs. A user who uses GitHub, Jira, and Linear must navigate to three different settings pages to configure what is conceptually the same thing. Worse, link mappings live in a completely separate page. There is no way to set a tenant-wide default — everything must be configured per-provider.
This creates three problems:
- Discoverability — new users don’t know mapping configuration exists until they notice wrong issue types
- Duplication — the same “bug” → bug mapping must be created in GitHub, Jira, and Linear separately
- Cognitive load — the user must understand the provider settings before understanding the mapping concept
Current UX model
Section titled “Current UX model”Mappings are scattered across provider settings and one orphaned page:
Settings sidebar: Integrations: ├── GitHub → tabs: Setup | Org Sync | Repo Sync | Labels | Statuses | Environments ├── Jira → tabs: Setup | Org Sync | Repo Sync | Labels | Statuses ├── Linear → tabs: Setup | Org Sync | Repo Sync | Labels | Statuses ├── Notion → tabs: Setup | Database Mappings (column-level, unique) └── Link Mappings (separate page, all providers)- Label, status, and environment mappings are tabs inside each provider
- Link mappings are a standalone page under Integrations
- No tenant-level defaults exist
- No way to see all mappings across providers at once
Target UX model
Section titled “Target UX model”A new “Mappings” section in settings with four dedicated pages — one per mapping type:
Settings sidebar: Integrations: ├── GitHub → tabs: Setup | Org Sync | Repo Sync (mappings tabs REMOVED) ├── Jira → tabs: Setup | Org Sync | Repo Sync ├── Linear → tabs: Setup | Org Sync | Repo Sync └── Notion → tabs: Setup | Database Mappings (stays — it's column-level config)
Mappings: ← NEW SECTION ├── Labels ← dedicated page ├── Statuses ← dedicated page ├── Environments ← dedicated page └── Relationships ← dedicated page (renamed from "Link Mappings")Each page shows mappings across all providers with scope-based filtering and the ability to create tenant-wide defaults.
Step 3.1: Schema evolution
Section titled “Step 3.1: Schema evolution”Modify: db/schema/work-tracking-config.ts
Make provider nullable on all four mapping tables:
labelMappings.provider—varchar(50).notNull()becomesvarchar(50)(nullable)statusMappings.provider— sameenvironmentMappings.provider— samelinkTypeMappings.provider— same
When provider is NULL, the mapping applies to all providers (tenant-level default).
Generate migration: npm run db:generate
This is backward compatible — existing mappings retain their provider value and continue to work identically. New mappings can omit provider to apply globally.
Resolution hierarchy (most specific wins):
- Provider + repository/project specific
- Provider + global (no repo/project)
- Tenant default + repository specific (
provider = NULL) - Tenant default + global (
provider = NULL,repositoryId = NULL)
Step 3.2: Preloader update
Section titled “Step 3.2: Preloader update”Modify: services/transformation-engine/preloader.ts
Update the preloader to support the new scoping:
preloadLabelMappings(db, tenantId, provider?) — If provider specified: load both provider-specific AND tenant defaults (provider=NULL) — If provider not specified: load all mappings for tenantThe scope-filtering functions gain a provider dimension:
- Provider + repository specific
- Provider + global
- Tenant default (provider=NULL) + repository specific
- Tenant default (provider=NULL) + global
Step 3.3: Navigation — new “Mappings” section
Section titled “Step 3.3: Navigation — new “Mappings” section”Modify: ui/pages/Settings/components/SettingsNav.tsx
Add a new nav section between “Organisation” and “Integrations”:
{ label: 'Mappings', items: [ { path: '/settings/mappings/labels', label: 'Labels', icon: '...' }, { path: '/settings/mappings/statuses', label: 'Statuses', icon: '...' }, { path: '/settings/mappings/environments', label: 'Environments', icon: '...' }, { path: '/settings/mappings/relationships', label: 'Relationships', icon: '...' }, ]}Remove: The Link Mappings item from the Integrations section (replaced by Relationships).
Modify: ui/pages/Settings/index.tsx
Add routes for the four new pages:
/settings/mappings/labels → LabelMappingsPage/settings/mappings/statuses → StatusMappingsPage/settings/mappings/environments → EnvironmentMappingsPage/settings/mappings/relationships → RelationshipMappingsPageAdd redirects from old URLs:
/settings/link-mappings → /settings/mappings/relationships/settings/github?tab=label-mappings → /settings/mappings/labels?provider=githubRemove mapping tabs from provider settings pages (GitHubSettings, JiraSettings, LinearSettings) but keep Notion’s database mapping tabs (they are column-level config, not value-level mapping).
Step 3.4: Page design — Label Mappings page
Section titled “Step 3.4: Page design — Label Mappings page”Create: ui/pages/Settings/mappings/LabelMappingsPage.tsx
This is the reference design that the other three pages follow.
Layout
Section titled “Layout”┌─────────────────────────────────────────────────────────────────┐│ Labels ││ Map external labels to internal issue types. Tenant-wide ││ defaults apply to all providers unless overridden. │├─────────────────────────────────────────────────────────────────┤│ ││ ┌─ Filters ──────────────────────────────────────────────────┐ ││ │ Provider: [All Providers ▾] Repository: [All ▾] [Reset] │ ││ └────────────────────────────────────────────────────────────┘ ││ ││ ┌─ Actions ──────────────────────────────────────────────────┐ ││ │ [+ Add Mapping] [AI Suggest] [Re-process Issues] │ ││ └────────────────────────────────────────────────────────────┘ ││ ││ ┌─ Create Form (appears when Add clicked) ───────────────────┐ ││ │ Label Pattern: [________] Maps To: [Feature ▾] │ ││ │ Sets Blocked: [ ] Scope: [All Providers ▾] [All Repos ▾] │ ││ │ [Create] [Cancel] │ ││ └────────────────────────────────────────────────────────────┘ ││ ││ ┌─ Unmapped Labels Panel (collapsible) ──────────────────────┐ ││ │ 12 unmapped labels from your synced issues │ ││ │ [priority] [design] [ux] [frontend] ... [Map Selected] │ ││ └────────────────────────────────────────────────────────────┘ ││ ││ ┌─ Active Mappings ──────────────────────────────────────────┐ ││ │ │ ││ │ ── Tenant Defaults ───────────────────────────────────── │ ││ │ │ Pattern │ Maps To │ Blocked │ Actions │ ││ │ │ bug │ Bug │ │ [Edit] [Delete] │ ││ │ │ enhancement │ Feature │ │ [Edit] [Delete] │ ││ │ │ blocked │ — │ ✓ │ [Edit] [Delete] │ ││ │ │ ││ │ ── GitHub Overrides ──────────────────────────────────── │ ││ │ │ good first │ Chore │ │ [Edit] [Delete] │ ││ │ │ ││ │ ── Jira Overrides ────────────────────────────────────── │ ││ │ │ (none — using tenant defaults) │ ││ │ │ ││ └────────────────────────────────────────────────────────────┘ ││ ││ ┌─ Default Heuristics (collapsible reference) ───────────────┐ ││ │ These patterns are detected automatically when no custom │ ││ │ mapping exists. Create a mapping to override them. │ ││ │ Pattern │ Auto-detected as │ ││ │ *bug*, *defect* │ Bug │ ││ │ *feature*, ... │ Feature │ ││ └────────────────────────────────────────────────────────────┘ │└─────────────────────────────────────────────────────────────────┘Key UX decisions
Section titled “Key UX decisions”Grouped by scope, not flat table: Mappings are visually grouped into “Tenant Defaults”, “GitHub Overrides”, “Jira Overrides”, etc. This makes the inheritance model immediately visible. Empty provider groups show “(none — using tenant defaults)” to reinforce that defaults cascade.
Scope selector in the create form: When adding a new mapping, the user chooses scope via two dropdowns:
- Provider: “All Providers” (tenant default) | GitHub | Jira | Linear | Notion
- Repository: “All Repositories” | specific repo (only shown if provider selected)
“All Providers” is the default and recommended choice — this nudges users toward tenant defaults.
Unmapped labels panel: Shows labels from synced issues that have no custom mapping. These are the “quick wins” — the user can click a label to pre-fill the create form. When AI suggestions are available (Phase 2), the “AI Suggest” button analyses these unmapped labels.
Filter bar: Provider and repository filters narrow the view. When filtering to a specific provider, the “Tenant Defaults” section still shows (dimmed) to indicate what the provider inherits. This prevents the confusing situation where a user filters to “GitHub”, creates a mapping, and doesn’t understand why it only applies to GitHub.
Inline editing: Clicking “Edit” on a mapping row opens inline editing (same pattern as the current LabelMappingsTable). Changes save immediately with optimistic UI.
Reprocess with preview: “Re-process Issues” button opens a confirmation modal showing impact count from a dry-run. E.g., “This will update 142 issues across 3 providers. Proceed?”
Step 3.5: Page design — Status Mappings page
Section titled “Step 3.5: Page design — Status Mappings page”Create: ui/pages/Settings/mappings/StatusMappingsPage.tsx
Same layout pattern as labels but with status-specific features:
- Maps To dropdown shows the 9 sub-statuses: backlog, ready, discovery, delivery, review, blocked, parked, done, canceled
- WIP badge: Each mapping shows whether the sub-status excludes from WIP (work in progress) as a subtle badge
- Project scope: For GitHub, an additional “Project” scope dropdown appears (for GitHub Projects v2 custom status fields)
- Available statuses panel: Shows unique
nativeStatusvalues from synced issues with counts, similar to the existingAvailableStatusesPanel - Grouped by scope: Same tenant defaults → provider overrides → repo overrides grouping
Unique to status mappings
Section titled “Unique to status mappings”Status mappings have a richer “how it works” reference section:
┌─ How Status Mapping Works ─────────────────────────────────┐│ ││ External Status → Sub-Status → State (derived) ││ ││ "In Progress" → delivery → in_progress ││ "Done" → done → closed ││ "Backlog" → backlog → open ││ ││ Sub-statuses that exclude from WIP: ││ [backlog] [ready] [blocked] [parked] [done] [canceled] ││ ││ Active WIP sub-statuses: ││ [discovery] [delivery] [review] │└─────────────────────────────────────────────────────────────┘Step 3.6: Page design — Environment Mappings page
Section titled “Step 3.6: Page design — Environment Mappings page”Create: ui/pages/Settings/mappings/EnvironmentMappingsPage.tsx
Simpler than labels/statuses since environment mappings are binary (production or not).
- Pattern field: Supports wildcards (
*and?). Input shows hint: “e.g., prod*, staging, release-*” - Is Production toggle: Simple on/off toggle
- Repository scope: Optional per-repository override
- Provider scope: Optional per-provider override (though today only GitHub has deployments, this supports future Jira/Linear deployment integration)
- No “unmapped” panel: Instead, show a “Detected environments” panel listing unique environment names from synced deployments with their current classification (auto-detected vs custom mapped)
Unique to environment mappings
Section titled “Unique to environment mappings”┌─ Detected Environments ────────────────────────────────────┐│ Environment │ Deployments │ Classification ││ production │ 847 │ ✓ Production (auto) ││ staging │ 312 │ Staging (auto) ││ preview-pr-* │ 1,204 │ Preview (auto) ││ canary │ 56 │ ⚠ Other (unmapped) ││ release-candidate │ 23 │ ⚠ Other (unmapped) ││ ││ ⚠ 2 environments may need mapping [Map them →] │└─────────────────────────────────────────────────────────────┘Step 3.7: Page design — Relationship Mappings page
Section titled “Step 3.7: Page design — Relationship Mappings page”Create: ui/pages/Settings/mappings/RelationshipMappingsPage.tsx
Renamed from “Link Mappings” to “Relationships” for clarity. Uses a card-based layout (matching the existing LinkMappingsSettings pattern) rather than a table, because relationship mappings have richer structure (conditional source/target type).
- External type: Text input for the provider’s relationship name (e.g., “Blocks”, “Relates”, “Implements”)
- Internal type: Dropdown of 8 internal types: validates, invalidates, implements, blocks, blocked_by, relates_to, duplicates, duplicated_by
- Conditional filters (optional): “Only when source is [type]” and “Only when target is [type]”
- Provider scope: Required (since relationship names are provider-specific by nature). But “All Providers” option exists for generic names like “blocks”.
Unique to relationship mappings
Section titled “Unique to relationship mappings”┌─ Relationship Mapping Card ────────────────────────────────┐│ ││ [Jira] "Relates" ──→ validates ││ Only when: source is Discovery AND target is Feature ││ [Edit] [×] ││ ││ [All] "Blocks" ──→ blocks ││ (applies to all providers) ││ [Edit] [×] │└─────────────────────────────────────────────────────────────┘The card layout handles the conditional logic more gracefully than a table row — conditions are shown as a secondary line beneath the mapping.
Step 3.8: Shared components
Section titled “Step 3.8: Shared components”Create: ui/components/mappings/MappingScopeSelector.tsx
Reusable scope selector used by all four pages:
[Provider ▾: All Providers] [Repository ▾: All Repositories] [Project ▾: All Projects]- Provider dropdown: “All Providers” + connected providers (from synced data)
- Repository dropdown: shown only when a specific provider is selected. Lists repositories for that provider.
- Project dropdown: shown only for GitHub when status mapping. Lists GitHub Projects v2.
- Each dropdown shows counts: “GitHub (142 issues)” to give context
Create: ui/components/mappings/MappingScopeGroupedList.tsx
Generic component that renders mappings grouped by scope:
── Tenant Defaults ─────────────────────────── (mapping rows)
── GitHub Overrides ────────────────────────── (mapping rows or "using tenant defaults")
── Jira Overrides ──────────────────────────── (mapping rows or "using tenant defaults")Accepts renderRow prop for type-specific rendering (labels show blocked toggle, statuses show WIP badge, etc.)
Create: ui/components/mappings/UnmappedValuesPanel.tsx
Generic “unmapped values” panel used by labels and statuses pages. Shows values from synced issues that have no custom mapping, with quick-add actions.
Create: ui/components/mappings/HeuristicsReference.tsx
Collapsible reference section showing default heuristic patterns. Currently duplicated across LabelMappingsTable and StatusMappingsTable — extract into a shared component that accepts a heuristics array.
Step 3.9: API updates
Section titled “Step 3.9: API updates”Modify: routes/settings/work-tracking.ts
Update all CRUD endpoints to support provider = null:
POST /label-mappings—providerbecomes optional in the request body. When omitted, creates a tenant-level default.- Same for status, environment, and link type mappings.
GETendpoints gain ascopequery param:?scope=all(default),?scope=global(tenant defaults only),?scope=provider:github
New endpoint: GET /mappings/summary — returns a summary of all mapping counts by type and scope, used by the nav section to show badge counts (e.g., “Labels (12)” or a warning dot for unmapped values).
Step 3.10: Remove mapping tabs from provider pages
Section titled “Step 3.10: Remove mapping tabs from provider pages”Modify: ui/pages/Settings/GitHubSettings.tsx
- Remove tabs:
label-mappings,status-mappings,environment-mappings - Keep tabs:
setup,org-sync,repository-sync - Add a “Configure Mappings” link card pointing to
/settings/mappings/labels?provider=github
Modify: ui/pages/Settings/JiraSettings.tsx
- Remove mapping tabs, add “Configure Mappings” link
Modify: ui/pages/Settings/LinearSettings.tsx
- Remove mapping tabs, add “Configure Mappings” link
Keep unchanged: ui/pages/Settings/NotionSettings.tsx
- Notion’s “Database Mappings” tab stays — it’s column-level property-to-field mapping, not value-level transformation
Step 3.11: Migration wizard
Section titled “Step 3.11: Migration wizard”Create: ui/components/mappings/MigrationWizard.tsx
A one-time tool for existing tenants to consolidate their per-provider mappings.
Shown as a dismissible banner at the top of any mapping page when duplicates are detected:
┌─ Consolidate Mappings ──────────────────────────────────────┐│ ℹ We detected 8 mappings that are identical across multiple ││ providers. Consolidate them into tenant defaults? ││ [Review & Consolidate] [×]│└──────────────────────────────────────────────────────────────┘Clicking “Review & Consolidate” opens a modal:
┌─ Consolidate Duplicate Mappings ────────────────────────────┐│ ││ These mappings are identical across all your providers: ││ ││ ☑ "bug" → Bug ││ Currently in: GitHub, Jira, Linear ││ → Will become: Tenant default ││ ││ ☑ "enhancement" → Feature ││ Currently in: GitHub, Linear ││ → Will become: Tenant default ││ ││ ☐ "In Progress" → delivery ││ Currently in: GitHub, Jira ││ Note: Linear uses "Started" for the same thing ││ → Will become: Tenant default ││ ││ [Consolidate Selected (6)] [Cancel] │└──────────────────────────────────────────────────────────────┘The wizard:
- Detects duplicate mappings across providers (same external value, same internal mapping)
- Shows them grouped with provider badges
- User selects which to consolidate
- On confirm: creates tenant defaults, deletes the per-provider duplicates
- Dismisses the banner (stored in user settings so it doesn’t reappear)
Step 3.12: Deep-link support from provider pages
Section titled “Step 3.12: Deep-link support from provider pages”When a user lands on a provider settings page (e.g., GitHub), the removed mapping tabs should gracefully redirect:
/settings/github?tab=label-mappings→/settings/mappings/labels?provider=github/settings/github?tab=status-mappings→/settings/mappings/statuses?provider=github/settings/github?tab=environment-mappings→/settings/mappings/environments?provider=github/settings/link-mappings→/settings/mappings/relationships
The provider query param pre-selects the provider filter on the mapping page, so the user sees their provider’s mappings immediately.
Step 3.13: Hooks refactor
Section titled “Step 3.13: Hooks refactor”Modify: ui/hooks/useWorkTracking.ts
Update the existing hooks to support the new scoping:
useLabelMappings(filters?: { provider?, scope? })— add optional scope filteruseCreateLabelMapping()— mutation body now accepts optionalprovider(null = tenant default)- Same pattern for status, environment mappings
Modify: ui/hooks/useLinkMappings.ts
- Same updates: optional provider in create, scope-aware listing
New hook: useMappingSummary() — fetches /mappings/summary for nav badges and the migration wizard duplicate detection.
Files summary (Phase 3)
Section titled “Files summary (Phase 3)”| File | Action |
|---|---|
| Schema & API | |
db/schema/work-tracking-config.ts |
Make provider nullable on all mapping tables |
db/migrations/XXXX_tenant_default_mappings.ts |
Generated migration |
services/transformation-engine/preloader.ts |
Update to handle provider=NULL |
services/transformation-engine/types.ts |
Update MappingScope to include optional provider |
routes/settings/work-tracking.ts |
Update CRUD for optional provider, add /mappings/summary |
| New Pages | |
ui/pages/Settings/mappings/LabelMappingsPage.tsx |
Create — primary reference page |
ui/pages/Settings/mappings/StatusMappingsPage.tsx |
Create — with WIP badges and project scope |
ui/pages/Settings/mappings/EnvironmentMappingsPage.tsx |
Create — with detected environments panel |
ui/pages/Settings/mappings/RelationshipMappingsPage.tsx |
Create — card layout with conditional filters |
| Shared Components | |
ui/components/mappings/MappingScopeSelector.tsx |
Create — reusable provider/repo/project scope picker |
ui/components/mappings/MappingScopeGroupedList.tsx |
Create — grouped-by-scope rendering |
ui/components/mappings/UnmappedValuesPanel.tsx |
Create — quick-add for unmapped labels/statuses |
ui/components/mappings/HeuristicsReference.tsx |
Create — collapsible default patterns reference |
ui/components/mappings/MigrationWizard.tsx |
Create — consolidation wizard with duplicate detection |
| Navigation & Routing | |
ui/pages/Settings/components/SettingsNav.tsx |
Add “Mappings” section, remove Link Mappings from Integrations |
ui/pages/Settings/index.tsx |
Add 4 new routes, add redirects from old URLs |
| Provider Pages | |
ui/pages/Settings/GitHubSettings.tsx |
Remove mapping tabs, add “Configure Mappings” link |
ui/pages/Settings/JiraSettings.tsx |
Remove mapping tabs, add “Configure Mappings” link |
ui/pages/Settings/LinearSettings.tsx |
Remove mapping tabs, add “Configure Mappings” link |
ui/pages/Settings/LinkMappingsSettings.tsx |
Delete (replaced by RelationshipMappingsPage) |
| Hooks | |
ui/hooks/useWorkTracking.ts |
Update for optional provider scoping |
ui/hooks/useLinkMappings.ts |
Update for optional provider scoping |
ui/hooks/useMappingSummary.ts |
Create — summary counts and duplicate detection |
| Legacy Components (can be deleted after migration) | |
ui/components/work-tracking/LabelMappingsTable.tsx |
Delete (logic moved to LabelMappingsPage) |
ui/components/work-tracking/StatusMappingsTable.tsx |
Delete (logic moved to StatusMappingsPage) |
ui/components/work-tracking/EnvironmentMappingsTable.tsx |
Delete (logic moved to EnvironmentMappingsPage) |
Phase 4: Advanced Features
Section titled “Phase 4: Advanced Features”Goal: Build on the unified engine for power-user features.
Can run in parallel with: Nothing directly (depends on Phase 1-3) Prerequisite: Phase 1, Phase 2, Phase 3
Step 4.1: Mapping templates
Section titled “Step 4.1: Mapping templates”Pre-built mapping sets for common configurations:
- “Standard GitHub” – maps common GitHub label conventions
- “Standard Jira” – maps common Jira workflow statuses
- “DORA Metrics” – optimised environment and status mappings for DORA tracking
- “Custom” – blank slate
Templates stored as JSON, applied via a “Start from template” action in the UI.
Step 4.2: Mapping validation and preview
Section titled “Step 4.2: Mapping validation and preview”Before applying mappings (or reprocessing), show a preview:
Preview: 142 issues would change 87 issues: type "other" → "feature" (label "enhancement") 34 issues: type "other" → "chore" (label "maintenance") 21 issues: subStatus null → "review" (status "In Review")This uses the reprocessor’s dry-run mode (already exists) with a richer UI.
Step 4.3: Cross-provider mapping intelligence
Section titled “Step 4.3: Cross-provider mapping intelligence”The AI suggestion engine learns from mappings across all providers in a tenant:
- “You mapped ‘In Progress’ → delivery for GitHub. Jira has ‘In Development’ which is unmapped. Suggest: delivery?”
- “Linear’s ‘Started’ maps to delivery in 90% of tenants. Suggest the same?”
This requires tenant-level aggregation but no schema changes.
Step 4.4: Bulk import/export
Section titled “Step 4.4: Bulk import/export”Export all mappings as JSON/CSV. Import to replicate configuration across tenants.
GET /api/settings/mappings/export?format=jsonPOST /api/settings/mappings/importFiles summary (Phase 4)
Section titled “Files summary (Phase 4)”| File | Action |
|---|---|
services/transformation-engine/templates.ts |
Create template definitions |
ui/components/mappings/MappingPreview.tsx |
Create preview component |
ui/components/mappings/TemplateSelector.tsx |
Create template selector |
routes/settings/work-tracking.ts |
Add export/import endpoints |
Phase Dependency Graph
Section titled “Phase Dependency Graph”Phase 1 (Extract & Unify) │ ├──► Phase 2 (AI Suggestions) ◄──── can run in parallel ────► Phase 3 (Tenant-First UX) │ │ │ │ └──────────────────► Phase 4 (Advanced) ◄─────────────────┘- Phase 1 is the foundation – must complete first
- Phase 2 and Phase 3 are independent and can run in parallel
- Phase 4 depends on all three previous phases
Risk Mitigation
Section titled “Risk Mitigation”| Risk | Impact | Mitigation |
|---|---|---|
| Breaking existing sync during Phase 1 migration | High | Migrate one provider at a time. Keep old imports as re-export shims. Run full test suite after each provider. |
| Notion mapping incompatibility | Medium | Bridge pattern: Notion checks embedded config first, falls through to engine. No changes to Notion’s column-mapping concept. |
| Schema migration for nullable provider (Phase 3) | Medium | Backward compatible: existing rows keep their provider value. New rows can omit it. No data migration needed. |
| AI suggestion quality for statuses/environments | Low | Include existing heuristic patterns in system prompt so AI skips obvious ones. Require user review before applying. |
| Performance regression from extra abstraction | Low | Facades are thin (zero overhead). Preloader already does parallel loading. |
Verification Plan
Section titled “Verification Plan”Phase 1
Section titled “Phase 1”npm run typecheck– zero errorsnpm run lint– zero errorsnpm run build– succeedsnpm run test– all existing tests pass- Manual: trigger GitHub sync – label/status mappings applied correctly
- Manual: trigger Jira/Linear/Notion sync – mappings applied correctly
- Manual: change a label mapping, trigger reprocessing – issues updated correctly
- Manual: GitHub deployment webhook – environment mapping resolves correctly
Phase 2
Section titled “Phase 2”- All Phase 1 checks pass
- Manual: request AI suggestions for unmapped statuses – reasonable results
- Manual: request AI suggestions for environment patterns – reasonable results
- Manual: bulk-apply suggestions – mappings created correctly
Phase 3
Section titled “Phase 3”- All Phase 1 checks pass
- Navigation: “Mappings” section appears in settings sidebar with 4 sub-pages
- Deep links:
/settings/github?tab=label-mappingsredirects to/settings/mappings/labels?provider=github - Scope grouping: mappings are visually grouped into Tenant Defaults / Provider Overrides
- Create tenant default: adding a mapping with “All Providers” scope creates a
provider=NULLrow - Create provider override: adding a mapping with a specific provider only applies to that provider
- Inheritance: filtering to a single provider shows both tenant defaults (dimmed) and provider overrides
- Unmapped panel: labels/statuses from synced issues that lack mappings are shown with quick-add actions
- Migration wizard: duplicate mappings across providers are detected and consolidation is offered
- Old tabs: label/status/environment tabs removed from GitHub/Jira/Linear settings pages
- Old URLs:
/settings/link-mappingsredirects to/settings/mappings/relationships
Phase 4
Section titled “Phase 4”- All previous checks pass
- Manual: apply a template – creates expected mappings
- Manual: preview reprocessing – shows accurate diff
- Manual: export and re-import mappings – round-trip successful
