UNPKG

@fusedashlabs/ui9000-workspace

Version:

UI9000 workspace MCP. One tool, show_workspace, draws a chart from your table. Set TYPESAFE_API_KEY to let Jev choose the chart.

90 lines (89 loc) • 3.81 kB
/** * Table columns → the closed DataProfile. * * The profiler reports facts; the catalog judges them. There is no threshold * here — `categoryCardinality` is a count, and rules like bar-chart's * `categoryCardinality <= 30` live in metadata, not in this file. * * Output carries all 15 DATA_PROFILE_KEYS, in the frozen order, every time: * the same table always serializes to the same bytes. Anything the columns do * not support is `false` / `0`, never a missing key and never a new key. */ import { DATA_PROFILE_KEYS } from '../spec/data-profile.js'; import { classifyColumns, distinctCount } from './roles.js'; import { tableRowCount } from './table.js'; const PROFILE_KEY_SET = new Set(DATA_PROFILE_KEYS); export function profileColumns(table, env = {}) { const { columns, isControlTable } = classifyColumns(table); const rowCount = tableRowCount(table); const controls = countControls(columns, isControlTable); const category = columns.find((entry) => entry.role === 'category'); const hasLinks = has(columns, 'link'); const hasTemporal = has(columns, 'temporal'); const facts = { hasCategory: category !== undefined, hasNumericMetric: has(columns, 'metric'), categoryCardinality: category ? distinctCount(category.column) : 0, hasGeo: has(columns, 'geo'), hasTemporal, // Link endpoints are nodes, so a link table is a node table. hasNodes: has(columns, 'node') || hasLinks, hasLinks, // A control table describes controls, not dataset rows. hasTabularRows: !isControlTable && rowCount >= 1 && table.columns.length >= 1, rowCount, hasEntityId: has(columns, 'entity'), hasClaim: has(columns, 'claim'), // event-timeline needs both; an event column with no clock is not a timeline. hasEvents: has(columns, 'event') && hasTemporal, controlCount: controls.count, allControlsLabelled: controls.allLabelled, hasMapToken: env.hasMapToken === true, }; const profile = {}; for (const key of DATA_PROFILE_KEYS) { profile[key] = facts[key]; } assertClosedProfile(profile); return profile; } /** * Guard for anything claiming to be a DataProfile — profiler output and * fixtures alike. An extra key fails; it is never dropped and never ignored. */ export function assertClosedProfile(profile, label = 'profile') { for (const key of Object.keys(profile)) { if (!PROFILE_KEY_SET.has(key)) { throw new Error(`${label} has unknown DataProfile key "${key}"`); } } } function has(columns, role) { return columns.some((entry) => entry.role === role); } /** * Controls come from a control table: one control per row, named by the * `control` column and named for a screen reader by the `label` column. * * With no controls `allControlsLabelled` is false, not vacuously true — the * `form` entry disqualifies only on `!allControlsLabelled`, so a vacuous true * would make a form eligible for every dataset that has no controls at all. */ function countControls(columns, isControlTable) { if (!isControlTable) return { count: 0, allLabelled: false }; const controlColumn = columns.find((entry) => entry.role === 'control')?.column; const labelColumn = columns.find((entry) => entry.role === 'label')?.column; if (!controlColumn) return { count: 0, allLabelled: false }; let count = 0; let labelled = 0; for (const [index, value] of controlColumn.values.entries()) { if (value.trim() === '') continue; count += 1; if ((labelColumn?.values[index] ?? '').trim() !== '') labelled += 1; } return { count, allLabelled: count > 0 && labelled === count }; }