Import Mapping Layer
Ingests the full Jira field set — promotes the Agile/planning fields to typed columns, captures the long tail in rawFields (JSONB) · resolves custom-field IDs per instance · resolves phases · tags dataSource. If a plan's ticket limit is ever reached, any un-imported work is disclosed honestly — the exact skipped count surfaces on the Dashboard, Reports, the daily briefing, and the API (meta.coverageComplete), never hidden as if the import were complete. Per-source freshness is disclosed the same way: each connected source (Jira, Azure DevOps, Asana) carries its last successful-sync age, so a stale, silently-failed, or never-synced source is surfaced (Dashboard, Reports, briefing, and the API meta.sources) rather than pooled into one green "on track" number. Custom capacity fields (planned effort, story points, release) are never guessed at: the Data & Fields page (/admin/fields; RULE 24, lib/dataRequirements.ts) is a thin source-switched orchestrator (FieldMappingPage.tsx) over two views that share the same underlying discovery/proposal/confirm machinery. For JIRA orgs (phase 1) it opens on the v5 signal-first view, "Required fields to get Avium Signals" (SignalFieldsView.tsx ← GET /admin/field-mapping/requirements, lib/signalRequirements.ts, Brian's drawing, hi-fi-approved 2026-07-17): a framing box states the live count computed off the Signal registry ("Avium ships N Signals — only the M decisions below need you"), and one block per real decision — Estimated effort per work item, Story points, Releases/milestones — each led by the Signals it unlocks, showing Avium's own detected native-field proposal ("Import detected field = Original estimate") with a Yes/No, or a searchable dropdown of alternatives. Time actually spent and Blocked render as plain "read automatically" rows, not decisions; Blocked additionally keeps its own real opt-in for a custom flag field, and a confirmed Blocked mapping now actually projects into detection (new JiraTicket.flaggedMapped column, re-run on every sync and every confirm/removal, detection reads flagged OR flaggedMapped — previously this control saved a mapping nothing read, a silent no-op). The three pop-ups Brian kept carry over unchanged: the epic-rollup question (does the epic's own estimate already cover its children?), the three-step release flow (field → date source → preview), and the decline acknowledgment naming the exact Signals a decline darkens before it takes effect. For non-JIRA sources (a spreadsheet today; Azure DevOps and Asana keep this page unchanged until their own pass, Law 4) the page opens on the untouched receipt/question/kept view (ReceiptMapperView.tsx, AVM-284, lib/fieldDiscovery.ts + lib/fieldReceipt.ts), not an interview. It opens on a walkable RECEIPT of every native concept Avium already reads without asking — Original Estimate, Remaining Estimate, the dated Work log, the Time Spent rollup, Story Points, Linked issues (dependency blocking), the impediment flag, and Sprints & Fix Versions — each row stating its coverage (the shared 300-ticket sample, or a real-time full-scope count where the concept demands it), real sample values, and, where a real filter exists, a "See the rows →" link straight into /pipeline scoped to the exact tickets behind it (never an invented route). A question appears only where one genuinely exists: a REQUIRED concept Avium can't resolve on its own (today: the estimated-effort field, story points) renders an amber "Needs an answer" card with a coverage-ranked candidate rail, evidence (sample values), a full-registry name/id search, and "We don't use this" as a reversible decline — one that carries real weight when it does: declining a concept with darkensSignals opens an acknowledgment modal naming the exact Signals going dark, drawn from the coverage matrix server-side (never hand-maintained copy), before the decline takes effect. An ENRICHMENT concept already covered by a native feed — today, Blocked/impediment, since Jira's own flag and issue links already power blocker detection — asks quietly as an optional extra with no amber badge and no acknowledgment: nothing goes dark either way. A source with a structural gap (Excel and Asana carry no change history) shows a one-time, plain-language acknowledgment of exactly which history-driven Signals can't fire on that source, collapsing to a quiet reopenable line once acknowledged. A dashed "We kept your other N fields" box lists the org's remaining custom fields, verbatim from the discovery scan. An org with more than one connected source (Jira and a spreadsheet in v1) gets a Source selector rendered by the orchestrator above either view, so the receipt (or the v5 blocks), the questions, and every coverage count scope to ONE source at a time — never a blended count across sources. The underlying capability-contract computation (GET /api/capability-contract, readable by every role, RULE 16; changing it stays admin-only) still drives the post-sync banner and the Dashboard's provenance strip the same way it always has — both views now consume it internally to decide which questions are still open, rather than surfacing it as four visual cards. File import writes each row's own columns into rawFields just like the Jira path, so a spreadsheet's real column headers surface in the receipt and question cards with evidence, mappable exactly like a Jira custom field; lib/excelFieldBridge.ts reconciles the file-import wizard's mapping with this page's requirement vocabulary so a column mapped on either surface closes the matching question on the other, never disagreeing. Resolution precedence is explicit and never silent: the org's CONFIRMED mapping (lib/confirmedFields.ts) outranks the instance's own field metadata. Story points carry NO further fallback beyond that (Brian's decision, 2026-07-16, AVM-289 — "Don't assume — ask"): an instance whose custom-field id Avium can't resolve any other way stays honestly dark rather than guessing the historical customfield_10016 default, which a differently-numbered instance could silently get wrong. (Sprint and Epic Link keep their own disclosed last-resort defaults — AVM-289's scope was story points only.) A confirmed story-points mapping resolves at the very next sync AND backfills immediately, so the numbers move the moment an admin answers, not at some unknowable future sync. A confirmed planned-effort mapping projects into DepartmentAllocation (lib/mappedEffort.ts, source='mapping', re-run on every sync) so it flows into capacity, velocity and utilization exactly like a hand-entered allocation — hand-entered rows are never touched. The mapper proposes, never polices, for VALUE-shape doubts (RULE 24): a field whose sample values look off (Fibonacci-looking hours, >100 "points") warns loudly on its candidate chip via shapeWarning and goes through on the admin's own call, because there the customer may genuinely know something Avium doesn't. A STRUCTURAL type mismatch is a different class and is REFUSED, not warned (Brian, 2026-07-17, Data & Fields v5 — reverses the prior "loud warning, never a refused drop" rule after the Issue-Type-proposed-as-releases incident): typeGate in lib/dataRequirements.ts is the one authority, enforced in three places — discovery's candidate lists, the mapping page's dropdown, and POST /field-mapping/confirm, which answers 422 if a structurally impossible field (a user field as releases, an option field as hours) is forced through anyway. An UNKNOWN type (no registry row, uninferrable samples — a sparse Excel column) still passes with a warning: the gate blocks known incompatibility, never missing metadata. Splitting an estimate across roles ("Dev Est" → Engineering, "QA Est" → QA) is no longer part of this page's main flow — the old per-field "whose time is this?" attribution interview is deferred; a link is shown for that case but the dedicated per-role flow isn't built yet, and every requirement here confirms as a single owner in the meantime. Release is itself a mapped concept (lib/mappedReleases.ts): any option, text, or label field can name an org's releases — each distinct value projects into a FixVersion row (source 'mapping') with its items linked, feeding the release filter, the milestone forecast, and the delivery-date signals on the org's own vocabulary — confirmed through a three-step flow (field picker with evidence → an explicit date-source step → a dry-run preview of exactly what confirming would create), ported behavior-for-behavior into the rebuilt page. The release date is the org's own answer, never Avium's guess: the latest date among an item's own confirmed date field (recomputed on every sync), a date typed directly in Avium (never overwritten by sync, never written back to the source), or honestly none — grouping and the filter still work, the date-risk signals stay off and say why. A ticket already linked by the source tool's native fixVersions is never re-linked, and confirming the standard fixVersions field simply hands authority back to native ingest. Above the ticket level, the Effort Rollup Engine (lib/effortRollup.ts) answers a different question — how much effort sits under a given epic/feature/story, at any depth: an item's OWN effort and its ROLLED-UP effort (the sum of every descendant's own effort) are two facts that coexist and ADD, never persisted, always derived at read time (RULE 26) — and lib/sourceLink.ts gives every contributing item a link straight back to its own row in Jira, Azure DevOps or Asana, so a rolled-up total is never a bare claim: it decomposes, click by click, to the customer's own tickets. The dedicated Rollup page (/rollup, client pages/RollupPage.tsx) puts this engine on its own top-level surface: GET /api/rollup/tree (lib/rollupTreeService.ts → computeRollupTree(), optional ?rootId= to scope one subtree) walks a parent's full Epic→Story→Sub-task tree and returns three rolled columns per row — Logged (Σ TimeLog.hours), Committed (Σ DepartmentAllocation.plannedHours), and Estimated (the item's own estimate; hours or, in points mode, story points, with the hours columns dropped rather than fabricated) — each carrying per-column coverage ("6 of 8 items" / "not planned") and a click-through walk to its exact contributing tickets. It's an app endpoint, not a /v1 shape — the frozen GET /v1/work-items/:id/effort-rollup single-item walk below is unaffected.
excel_import
jira_import