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) andunits[](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).assessExtractionFidelitycompares that against captured entities (people + units + groups + sub-unit bullets) and writesmetadata.fidelity_warning(also appended tonotes) when < 60% was captured on a chart with β₯ 10 blocks. Surfaced in the extract route response asfidelityWarning.
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_unitsJSONB string array, sort_order.- RLS: owner via
EXISTSonorg_charts+ service-role-scoped bypass (neverUSING (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.tsmaps temp ids β UUIDs, drops dangling refs) and storeschart_styleon the chart./re-extractclears units/groups too. GET/POST/PATCH/DELETE /api/orgcharts/:id/unitsand.../units/groups[/:groupId]β CRUD, camelCase at the boundary,''β NULL for cleared FK fields. Registered inindex.ts,index-aws.ts, andserver.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 viamax(12px, β¦). recommendTemplate(people, relationships, chartStyle)returnsgrouped-unitswhenchart_style = 'grouped-units'; the template also appears in the picker for every chart (picker is API-driven β no frontend change needed)./generateand preview regeneration fetch units/groups lazily (only for the grouped-units template);/generate-previewsalways 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.