import type { LinearProjectMapping } from "../db/schema"; /** * Explicit Linear -> Scribe project resolution. * * Linear is an API source with no local folder, so its timeline events can't be * attributed to a project by path matching like git-backed sources. Instead the * user links a Linear project name to a Scribe project (`linear_project_mappings`), * and these pure helpers resolve that link at query time (see OPL-2675). * * Matching is case-insensitive on the Linear project name. These functions are * pure so the resolution logic is unit-testable without a database and is shared * by whichever surface consumes it (timeline, reports). */ function normalizeName(name: string): string { return name.trim().toLowerCase(); } /** * Resolve the Scribe project id an unassigned Linear event belongs to, given the * `projectName` from its `LinearMetadata` and the tenant's mappings. Returns the * mapped project id, or `null` when the event has no project name or no link * exists (it stays unassigned rather than being misattributed). */ export function resolveLinearProjectId( linearProjectName: string | null | undefined, mappings: readonly LinearProjectMapping[], ): string | null { if (!linearProjectName) return null; const target = normalizeName(linearProjectName); if (!target) return null; for (const mapping of mappings) { if (normalizeName(mapping.linearProjectName) === target) { return mapping.projectId; } } return null; } /** * Inverse of {@link resolveLinearProjectId}: the set of Linear project names * linked to any of `projectIds`. A project-filtered timeline/report uses this to * pull in the mapped Linear events that would otherwise drop out (their * `projectRootId` is null), without widening the query for unmapped sources. * Names are returned as stored (original case) for display/predicate building; * de-duplicated case-insensitively. */ export function linearProjectNamesForProjectIds( mappings: readonly LinearProjectMapping[], projectIds: readonly string[], ): string[] { const wanted = new Set(projectIds); const seen = new Set(); const names: string[] = []; for (const mapping of mappings) { if (!wanted.has(mapping.projectId)) continue; const key = normalizeName(mapping.linearProjectName); if (!key || seen.has(key)) continue; seen.add(key); names.push(mapping.linearProjectName); } return names; }