Skip to content

Grouped/Unit-Based Org Charts (issue #1754)

Real-world org charts (e.g. UTSA Academic Affairs) often aren’t strict personβ†’manager trees. They use category banners, unit boxes with an attached leader, and bulleted sub-unit lists. Before this feature the extraction schema only modeled people[] + relationships[], so the vision models silently flattened ~70% of such documents into direct reports of the root while reporting high confidence.

Extraction

workers/api/src/services/org-chart/vision-extractor.ts:

  • metadata.chart_style: strict-tree | grouped-units | mixed β€” the model classifies the source structure.
  • Optional groups[] (banners: temp id, name, order) and units[] (name, leader temp person id, sub_units[] labels, parent group/unit/person temp ids). Absent for plain charts β€” fully backward compatible.
  • Fidelity signal: the model self-reports metadata.text_blocks_seen (visible boxes/banners/bullets). assessExtractionFidelity compares that against captured entities (people + units + groups + sub-unit bullets) and writes metadata.fidelity_warning (also appended to notes) when < 60% was captured on a chart with β‰₯ 10 blocks. Surfaced in the extract route response as fidelityWarning.

Data model (migration 20260725000000_223_org_chart_units.sql)

  • org_charts.chart_style β€” nullable TEXT + CHECK.
  • org_chart_groups β€” id, org_chart_id (CASCADE), name, sort_order.
  • org_chart_units β€” id, org_chart_id (CASCADE), group_id (SET NULL), name, leader_person_id (SET NULL β€” units may be leaderless), parent_unit_id (SET NULL, self-parent CHECK), parent_person_id (SET NULL), sub_units JSONB string array, sort_order.
  • RLS: owner via EXISTS on org_charts + service-role-scoped bypass (never USING (TRUE); matches migrations 206/209).

Sub-units are label arrays, not rows β€” they carry no attributes in source charts. Promote to rows only if they ever grow attributes.

API

  • Extraction (extract.ts) persists groups β†’ units after people (extraction-units.ts maps temp ids β†’ UUIDs, drops dangling refs) and stores chart_style on the chart. /re-extract clears units/groups too.
  • GET/POST/PATCH/DELETE /api/orgcharts/:id/units and .../units/groups[/:groupId] β€” CRUD, camelCase at the boundary, '' β†’ NULL for cleared FK fields. Registered in index.ts, index-aws.ts, and server.ts (org-chart routes must exist on all three; route-parity test enforces it).

Rendering + routing

  • New template grouped-units (workers/api/src/templates/grouped-units.ts): banners β†’ semantic <section> + <h2>, unit boxes β†’ <article> with the unit name heading + leader line, sub-unit lists β†’ real <ul> (better for screen readers than the source’s visual bullets). Leadership section lists people not shown inside unit cards. Falls back to department grouping when a chart has no units. WCAG 2.1 AA verified by the validator on light + dark schemes; 12px font floor via max(12px, …).
  • recommendTemplate(people, relationships, chartStyle) returns grouped-units when chart_style = 'grouped-units'; the template also appears in the picker for every chart (picker is API-driven β€” no frontend change needed).
  • /generate and preview regeneration fetch units/groups lazily (only for the grouped-units template); /generate-previews always includes them in the cached render input manifest.

Version-snapshot gap

Version snapshots (org_chart_versions.snapshot) still cover only people + relationships; units/groups are not yet versioned/restored. Tracked as a known follow-up.